Merge pull request #558 from poanetwork/sa-api-account-module-txlistinternal-action

Add API txlistinternal action for account module
pull/562/head
Sebastian Abondano 6 years ago committed by GitHub
commit 5e8ac9fb49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
  2. 109
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  3. 22
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
  4. 163
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs
  5. 12
      apps/explorer/lib/explorer/chain.ex
  6. 84
      apps/explorer/lib/explorer/etherscan.ex
  7. 26
      apps/explorer/test/explorer/chain_test.exs
  8. 105
      apps/explorer/test/explorer/etherscan_test.exs

@ -49,10 +49,35 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
end
end
def txlistinternal(conn, params) do
with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params),
{:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param),
{:ok, internal_transactions} <- list_internal_transactions(transaction_hash) do
render(conn, :txlistinternal, %{internal_transactions: internal_transactions})
else
{:txhash_param, :error} ->
conn
|> put_status(200)
|> render(:error, error: "Query parameter txhash is required")
{:format, :error} ->
conn
|> put_status(200)
|> render(:error, error: "Invalid txhash format")
{:error, :not_found} ->
render(conn, :error, error: "No internal transactions found", data: [])
end
end
defp fetch_address(params) do
{:address_param, Map.fetch(params, "address")}
end
defp fetch_txhash(params) do
{:txhash_param, Map.fetch(params, "txhash")}
end
defp to_address_hashes(address_param) when is_binary(address_param) do
address_param
|> String.split(",")
@ -112,6 +137,10 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
{:format, Chain.string_to_address_hash(address_hash_string)}
end
defp to_transaction_hash(transaction_hash_string) do
{:format, Chain.string_to_transaction_hash(transaction_hash_string)}
end
defp optional_params(params) do
%{}
|> put_order_by_direction(params)
@ -175,4 +204,11 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
transactions -> {:ok, transactions}
end
end
defp list_internal_transactions(transaction_hash) do
case Etherscan.list_internal_transactions(transaction_hash) do
[] -> {:error, :not_found}
internal_transactions -> {:ok, internal_transactions}
end
end
end

@ -37,7 +37,7 @@ defmodule BlockScoutWeb.Etherscan do
@account_txlist_example_value %{
"status" => "1",
"message" => "OK",
result: [
"result" => [
%{
"blockNumber" => "65204",
"timeStamp" => "1439232889",
@ -68,10 +68,37 @@ defmodule BlockScoutWeb.Etherscan do
"result" => []
}
@account_txlistinternal_example_value %{
"status" => "1",
"message" => "OK",
"result" => [
%{
"blockNumber" => "6153702",
"timeStamp" => "1534362606",
"from" => "0x2ca1e3f250f56f1761b9a52bc42db53986085eff",
"to" => "",
"value" => "5488334153118633",
"contractAddress" => "0x883103875d905c11f9ac7dacbfc16deb39655361",
"input" => "",
"type" => "create",
"gas" => "814937",
"gasUsed" => "536262",
"isError" => "0",
"errCode" => ""
}
]
}
@account_txlistinternal_example_value_error %{
"status" => "0",
"message" => "No internal transactions found",
"result" => []
}
@logs_getlogs_example_value %{
"status" => "1",
"message" => "OK",
result: [
"result" => [
%{
"address" => "0x33990122638b9132ca29c723bdf037f1a891a70c",
"topics" => [
@ -199,6 +226,44 @@ defmodule BlockScoutWeb.Etherscan do
}
}
@internal_transaction %{
name: "InternalTransaction",
fields: %{
blockNumber: @block_number_type,
timeStamp: %{
type: "timestamp",
definition: "The transaction's block-timestamp.",
example: ~s("1439232889")
},
from: @address_hash_type,
to: @address_hash_type,
value: @wei_type,
contractAddress: @address_hash_type,
input: %{
type: "input",
definition: "Data sent along with the call. A variable-byte-length binary.",
example: ~s("0x797af627d02e23b68e085092cd0d47d6cfb54be025f37b5989c0264398f534c08af7dea9")
},
type: %{
type: "type",
definition: ~s(Possible values: "create", "call", "reward", or "suicide"),
example: ~s("create")
},
gas: @gas_type,
gasUsed: @gas_type,
isError: %{
type: "error",
enum: ~s(["0", "1"]),
enum_interpretation: %{"0" => "ok", "1" => "rejected/cancelled"}
},
errCode: %{
type: "string",
definition: "Error message when call type error.",
example: ~s("Out of gas")
}
}
}
@log %{
name: "Log",
fields: %{
@ -383,6 +448,43 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@account_txlistinternal_action %{
name: "txlistinternal",
description: "Get internal transactions by transaction hash. Up to a maximum of 10,000 internal transactions.",
required_params: [
%{
key: "txhash",
placeholder: "transactionHash",
type: "string",
description: "Transaction hash. Hash of contents of the transaction."
}
],
optional_params: [],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@account_txlistinternal_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "array",
array_type: @internal_transaction
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@account_txlistinternal_example_value_error)
}
]
}
@logs_getlogs_action %{
name: "getLogs",
description: "Get event logs for an address and/or topics. Up to a maximum of 1,000 event logs.",
@ -503,7 +605,8 @@ defmodule BlockScoutWeb.Etherscan do
actions: [
@account_balance_action,
@account_balancemulti_action,
@account_txlist_action
@account_txlist_action,
@account_txlistinternal_action
]
}

@ -28,6 +28,11 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
RPCView.render("show.json", data: data)
end
def render("txlistinternal.json", %{internal_transactions: internal_transactions}) do
data = Enum.map(internal_transactions, &prepare_internal_transaction/1)
RPCView.render("show.json", data: data)
end
def render("error.json", assigns) do
RPCView.render("error.json", assigns)
end
@ -54,4 +59,21 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
"confirmations" => "#{transaction.confirmations}"
}
end
defp prepare_internal_transaction(internal_transaction) do
%{
"blockNumber" => "#{internal_transaction.block_number}",
"timeStamp" => "#{DateTime.to_unix(internal_transaction.block_timestamp)}",
"from" => "#{internal_transaction.from_address_hash}",
"to" => "#{internal_transaction.to_address_hash}",
"value" => "#{internal_transaction.value.value}",
"contractAddress" => "#{internal_transaction.created_contract_address_hash}",
"input" => "#{internal_transaction.input}",
"type" => "#{internal_transaction.type}",
"gas" => "#{internal_transaction.gas}",
"gasUsed" => "#{internal_transaction.gas_used}",
"isError" => if(internal_transaction.error, do: "1", else: "0"),
"errCode" => "#{internal_transaction.error}"
}
end
end

@ -1010,4 +1010,167 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
assert get_response == post_response
end
end
describe "txlistinternal" do
test "with missing txhash", %{conn: conn} do
params = %{
"module" => "account",
"action" => "txlistinternal"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "txhash is required"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with an invalid txhash", %{conn: conn} do
params = %{
"module" => "account",
"action" => "txlistinternal",
"txhash" => "badhash"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "Invalid txhash format"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with an txhash that doesn't exist", %{conn: conn} do
params = %{
"module" => "account",
"action" => "txlistinternal",
"txhash" => "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == []
assert response["status"] == "0"
assert response["message"] == "No internal transactions found"
end
test "response includes all the expected fields", %{conn: conn} do
address = insert(:address)
contract_address = insert(:contract_address)
block = insert(:block)
transaction =
:transaction
|> insert(from_address: address, to_address: nil)
|> with_contract_creation(contract_address)
|> with_block(block)
internal_transaction =
:internal_transaction_create
|> insert(transaction: transaction, index: 0, from_address: address)
|> with_contract_creation(contract_address)
params = %{
"module" => "account",
"action" => "txlistinternal",
"txhash" => "#{transaction.hash}"
}
expected_result = [
%{
"blockNumber" => "#{transaction.block_number}",
"timeStamp" => "#{DateTime.to_unix(block.timestamp)}",
"from" => "#{internal_transaction.from_address_hash}",
"to" => "#{internal_transaction.to_address_hash}",
"value" => "#{internal_transaction.value.value}",
"contractAddress" => "#{contract_address.hash}",
"input" => "",
"type" => "#{internal_transaction.type}",
"gas" => "#{internal_transaction.gas}",
"gasUsed" => "#{internal_transaction.gas_used}",
"isError" => "0",
"errCode" => "#{internal_transaction.error}"
}
]
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "isError is true if internal transaction has an error", %{conn: conn} do
transaction =
:transaction
|> insert()
|> with_block()
internal_transaction_details = [
transaction: transaction,
index: 0,
type: :reward,
error: "some error"
]
insert(:internal_transaction_create, internal_transaction_details)
params = %{
"module" => "account",
"action" => "txlistinternal",
"txhash" => "#{transaction.hash}"
}
assert %{"result" => [found_internal_transaction]} =
response =
conn
|> get("/api", params)
|> json_response(200)
assert found_internal_transaction["isError"] == "1"
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with transaction with multiple internal transactions", %{conn: conn} do
transaction =
:transaction
|> insert()
|> with_block()
for index <- 0..2 do
insert(:internal_transaction_create, transaction: transaction, index: index)
end
params = %{
"module" => "account",
"action" => "txlistinternal",
"txhash" => "#{transaction.hash}"
}
assert %{"result" => found_internal_transactions} =
response =
conn
|> get("/api", params)
|> json_response(200)
assert length(found_internal_transactions) == 3
assert response["status"] == "1"
assert response["message"] == "OK"
end
end
end

@ -1461,7 +1461,17 @@ defmodule Explorer.Chain do
where(query, [transaction], transaction.index < ^index)
end
defp where_transaction_has_multiple_internal_transactions(query) do
@doc """
Ensures the following conditions are true:
* excludes internal transactions of type call with no siblings in the
transaction
* includes internal transactions of type create, reward, or suicide
even when they are alone in the parent transaction
"""
@spec where_transaction_has_multiple_internal_transactions(Ecto.Query.t()) :: Ecto.Query.t()
def where_transaction_has_multiple_internal_transactions(query) do
where(
query,
[internal_transaction, transaction],

@ -7,7 +7,7 @@ defmodule Explorer.Etherscan do
alias Explorer.Etherscan.Logs
alias Explorer.{Repo, Chain}
alias Explorer.Chain.{Hash, Transaction}
alias Explorer.Chain.{Hash, InternalTransaction, Transaction}
@default_options %{
order_by_direction: :asc,
@ -27,7 +27,7 @@ defmodule Explorer.Etherscan do
end
@doc """
Gets a list of transactions for a given `t:Explorer.Chain.Hash.Address`.
Gets a list of transactions for a given `t:Explorer.Chain.Hash.Address.t/0`.
"""
@spec list_transactions(Hash.Address.t()) :: [map()]
@ -45,23 +45,69 @@ defmodule Explorer.Etherscan do
end
end
@transaction_fields [
:block_hash,
:block_number,
:created_contract_address_hash,
:cumulative_gas_used,
:from_address_hash,
:gas,
:gas_price,
:gas_used,
:hash,
:index,
:input,
:nonce,
:status,
:to_address_hash,
:value
]
@internal_transaction_fields ~w(
from_address_hash
to_address_hash
value
created_contract_address_hash
input
type
gas
gas_used
error
)a
@doc """
Gets a list of internal transactions for a given transaction hash
(`t:Explorer.Chain.Hash.Full.t/0`).
Note that this function relies on `Explorer.Chain` to exclude/include
internal transactions as follows:
* exclude internal transactions of type call with no siblings in the
transaction
* include internal transactions of type create, reward, or suicide
even when they are alone in the parent transaction
"""
@spec list_internal_transactions(Hash.Full.t()) :: [map()]
def list_internal_transactions(%Hash{byte_count: unquote(Hash.Full.byte_count())} = transaction_hash) do
query =
from(
it in InternalTransaction,
inner_join: t in assoc(it, :transaction),
inner_join: b in assoc(t, :block),
where: it.transaction_hash == ^transaction_hash,
limit: 10_000,
select:
merge(map(it, ^@internal_transaction_fields), %{
block_timestamp: b.timestamp,
block_number: b.number
})
)
query
|> Chain.where_transaction_has_multiple_internal_transactions()
|> Repo.all()
end
@transaction_fields ~w(
block_hash
block_number
created_contract_address_hash
cumulative_gas_used
from_address_hash
gas
gas_price
gas_used
hash
index
input
nonce
status
to_address_hash
value
)a
defp list_transactions(address_hash, max_block_number, options) do
query =

@ -853,6 +853,32 @@ defmodule Explorer.ChainTest do
assert actual.id == expected.id
end
test "includes internal transactions of type `reward` even when they are alone in the parent transaction" do
transaction =
:transaction
|> insert()
|> with_block()
expected = insert(:internal_transaction, index: 0, transaction: transaction, type: :reward)
actual = Enum.at(Chain.transaction_to_internal_transactions(transaction), 0)
assert actual.id == expected.id
end
test "includes internal transactions of type `suicide` even when they are alone in the parent transaction" do
transaction =
:transaction
|> insert()
|> with_block()
expected = insert(:internal_transaction, index: 0, transaction: transaction, gas: nil, type: :suicide)
actual = Enum.at(Chain.transaction_to_internal_transactions(transaction), 0)
assert actual.id == expected.id
end
test "returns the internal transactions in ascending index order" do
transaction =
:transaction

@ -6,7 +6,7 @@ defmodule Explorer.EtherscanTest do
alias Explorer.{Etherscan, Chain}
alias Explorer.Chain.Transaction
describe "list_transactions/1" do
describe "list_transactions/2" do
test "with empty db" do
address = build(:address)
@ -344,4 +344,107 @@ defmodule Explorer.EtherscanTest do
end
end
end
describe "list_internal_transactions/1" do
test "with empty db" do
transaction = build(:transaction)
assert Etherscan.list_internal_transactions(transaction.hash) == []
end
test "response includes all the expected fields" do
address = insert(:address)
contract_address = insert(:contract_address)
block = insert(:block)
transaction =
:transaction
|> insert(from_address: address, to_address: nil)
|> with_contract_creation(contract_address)
|> with_block(block)
internal_transaction =
:internal_transaction_create
|> insert(transaction: transaction, index: 0, from_address: address)
|> with_contract_creation(contract_address)
[found_internal_transaction] = Etherscan.list_internal_transactions(transaction.hash)
assert found_internal_transaction.block_number == block.number
assert found_internal_transaction.block_timestamp == block.timestamp
assert found_internal_transaction.from_address_hash == internal_transaction.from_address_hash
assert found_internal_transaction.to_address_hash == internal_transaction.to_address_hash
assert found_internal_transaction.value == internal_transaction.value
assert found_internal_transaction.created_contract_address_hash ==
internal_transaction.created_contract_address_hash
assert found_internal_transaction.input == internal_transaction.input
assert found_internal_transaction.type == internal_transaction.type
assert found_internal_transaction.gas == internal_transaction.gas
assert found_internal_transaction.gas_used == internal_transaction.gas_used
assert found_internal_transaction.error == internal_transaction.error
end
test "with transaction with 0 internal transactions" do
transaction =
:transaction
|> insert()
|> with_block()
assert Etherscan.list_internal_transactions(transaction.hash) == []
end
test "with transaction with multiple internal transactions" do
transaction =
:transaction
|> insert()
|> with_block()
for index <- 0..2 do
insert(:internal_transaction, transaction: transaction, index: index)
end
found_internal_transactions = Etherscan.list_internal_transactions(transaction.hash)
assert length(found_internal_transactions) == 3
end
test "only returns internal transactions that belong to the transaction" do
transaction1 =
:transaction
|> insert()
|> with_block()
transaction2 =
:transaction
|> insert()
|> with_block()
insert(:internal_transaction, transaction: transaction1, index: 0)
insert(:internal_transaction, transaction: transaction1, index: 1)
insert(:internal_transaction, transaction: transaction2, index: 0, type: :reward)
internal_transactions1 = Etherscan.list_internal_transactions(transaction1.hash)
assert length(internal_transactions1) == 2
internal_transactions2 = Etherscan.list_internal_transactions(transaction2.hash)
assert length(internal_transactions2) == 1
end
# Note that `list_internal_transactions/1` relies on
# `Chain.where_transaction_has_multiple_transactions/1` to ensure the
# following behavior:
#
# * exclude internal transactions of type call with no siblings in the
# transaction
#
# * include internal transactions of type create, reward, or suicide
# even when they are alone in the parent transaction
#
# These two requirements are tested in `Explorer.ChainTest`.
end
end

Loading…
Cancel
Save