Why: * For users to be able to get the transactions for a given address hash. Support for optional parameters will be added in an upcoming PR. * Issue link: https://github.com/poanetwork/poa-explorer/issues/138 This change addresses the need by: * Adding `Explorer.Etherscan` context, where we want to keep etherscan-only logic within the Explorer app. This is because we want to mimic the way their RPC API works. The only function in this module for now is `list_transactions/1`, which gets all transactions for a given address hash. * Adding `txlist/2` action to `API.RPC.AddressController`, to handle account transactions requests. Example usage: ``` api?module=account&action=txlist \ &address=0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a ``` * Editing `API.RPC.AddressView` and `API.RPC.RPCView` to format responses for new API endpoint as required. * Editing `.credo.exs` to allow for `Hash.Address.t()` alias usage in `Explorer.Etherscan` without Credo complaining with: ``` Software Design ┃ ┃ [D] ↓ Nested modules could be aliased at the top of the invoking module. ┃ apps/explorer/lib/explorer/etherscan.ex:19:51 ```pull/427/head
parent
9af847f7bb
commit
e8a9b63d7b
@ -0,0 +1,67 @@ |
|||||||
|
defmodule Explorer.Etherscan do |
||||||
|
@moduledoc """ |
||||||
|
The etherscan context. |
||||||
|
""" |
||||||
|
|
||||||
|
import Ecto.Query, |
||||||
|
only: [ |
||||||
|
from: 2 |
||||||
|
] |
||||||
|
|
||||||
|
alias Explorer.{Repo, Chain} |
||||||
|
alias Explorer.Chain.{Hash, Transaction} |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Gets a list of transactions for a given `t:Explorer.Chain.Hash.Address`. |
||||||
|
|
||||||
|
""" |
||||||
|
@spec list_transactions(Hash.Address.t()) :: [map()] |
||||||
|
def list_transactions(%Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash) do |
||||||
|
case Chain.max_block_number() do |
||||||
|
{:ok, max_block_number} -> |
||||||
|
list_transactions(address_hash, max_block_number) |
||||||
|
|
||||||
|
_ -> |
||||||
|
[] |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@transaction_fields [ |
||||||
|
:block_number, |
||||||
|
:hash, |
||||||
|
:nonce, |
||||||
|
:block_hash, |
||||||
|
:index, |
||||||
|
:from_address_hash, |
||||||
|
:to_address_hash, |
||||||
|
:value, |
||||||
|
:gas, |
||||||
|
:gas_price, |
||||||
|
:status, |
||||||
|
:input, |
||||||
|
:cumulative_gas_used, |
||||||
|
:gas_used |
||||||
|
] |
||||||
|
|
||||||
|
defp list_transactions(address_hash, max_block_number) do |
||||||
|
query = |
||||||
|
from( |
||||||
|
t in Transaction, |
||||||
|
inner_join: b in assoc(t, :block), |
||||||
|
left_join: it in assoc(t, :internal_transactions), |
||||||
|
where: t.to_address_hash == ^address_hash, |
||||||
|
or_where: t.from_address_hash == ^address_hash, |
||||||
|
or_where: it.transaction_hash == t.hash and it.type == ^"create", |
||||||
|
order_by: [asc: t.block_number], |
||||||
|
limit: 10_000, |
||||||
|
select: |
||||||
|
merge(map(t, ^@transaction_fields), %{ |
||||||
|
block_timestamp: b.timestamp, |
||||||
|
created_contract_address_hash: it.created_contract_address_hash, |
||||||
|
confirmations: fragment("? - ?", ^max_block_number, t.block_number) |
||||||
|
}) |
||||||
|
) |
||||||
|
|
||||||
|
Repo.all(query) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,175 @@ |
|||||||
|
defmodule Explorer.EtherscanTest do |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
import Explorer.Factory |
||||||
|
|
||||||
|
alias Explorer.{Etherscan, Chain} |
||||||
|
alias Explorer.Chain.Transaction |
||||||
|
|
||||||
|
describe "list_transactions/1" do |
||||||
|
test "with empty db" do |
||||||
|
address = build(:address) |
||||||
|
|
||||||
|
assert Etherscan.list_transactions(address.hash) == [] |
||||||
|
end |
||||||
|
|
||||||
|
test "with from address" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
transaction = |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
[found_transaction] = Etherscan.list_transactions(address.hash) |
||||||
|
|
||||||
|
assert transaction.hash == found_transaction.hash |
||||||
|
end |
||||||
|
|
||||||
|
test "with to address" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
transaction = |
||||||
|
:transaction |
||||||
|
|> insert(to_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
[found_transaction] = Etherscan.list_transactions(address.hash) |
||||||
|
|
||||||
|
assert transaction.hash == found_transaction.hash |
||||||
|
end |
||||||
|
|
||||||
|
test "with same to and from address" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
_transaction = |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address, to_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
found_transactions = Etherscan.list_transactions(address.hash) |
||||||
|
|
||||||
|
assert length(found_transactions) == 1 |
||||||
|
end |
||||||
|
|
||||||
|
test "with created contract address" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
transaction = |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
%{created_contract_address_hash: contract_address_hash} = |
||||||
|
insert(:internal_transaction_create, transaction: transaction, index: 0) |
||||||
|
|
||||||
|
[found_transaction] = Etherscan.list_transactions(contract_address_hash) |
||||||
|
|
||||||
|
assert found_transaction.hash == transaction.hash |
||||||
|
end |
||||||
|
|
||||||
|
test "with address with 0 transactions" do |
||||||
|
address1 = insert(:address) |
||||||
|
address2 = insert(:address) |
||||||
|
|
||||||
|
:transaction |
||||||
|
|> insert(from_address: address2) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
assert Etherscan.list_transactions(address1.hash) == [] |
||||||
|
end |
||||||
|
|
||||||
|
test "with address with multiple transactions" do |
||||||
|
address1 = insert(:address) |
||||||
|
address2 = insert(:address) |
||||||
|
|
||||||
|
3 |
||||||
|
|> insert_list(:transaction, from_address: address1) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
:transaction |
||||||
|
|> insert(from_address: address2) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
found_transactions = Etherscan.list_transactions(address1.hash) |
||||||
|
|
||||||
|
assert length(found_transactions) == 3 |
||||||
|
|
||||||
|
for found_transaction <- found_transactions do |
||||||
|
assert found_transaction.from_address_hash == address1.hash |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
test "includes confirmations value" do |
||||||
|
insert(:block) |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
transaction = |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
insert(:block) |
||||||
|
|
||||||
|
[found_transaction] = Etherscan.list_transactions(address.hash) |
||||||
|
|
||||||
|
{:ok, max_block_number} = Chain.max_block_number() |
||||||
|
expected_confirmations = max_block_number - transaction.block_number |
||||||
|
|
||||||
|
assert found_transaction.confirmations == expected_confirmations |
||||||
|
end |
||||||
|
|
||||||
|
test "loads created_contract_address_hash if available" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
transaction = |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
%{created_contract_address_hash: contract_hash} = |
||||||
|
insert(:internal_transaction_create, transaction: transaction, index: 0) |
||||||
|
|
||||||
|
[found_transaction] = Etherscan.list_transactions(address.hash) |
||||||
|
|
||||||
|
assert found_transaction.created_contract_address_hash == contract_hash |
||||||
|
end |
||||||
|
|
||||||
|
test "loads block_timestamp" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
%Transaction{block: block} = |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
[found_transaction] = Etherscan.list_transactions(address.hash) |
||||||
|
|
||||||
|
assert found_transaction.block_timestamp == block.timestamp |
||||||
|
end |
||||||
|
|
||||||
|
test "orders transactions by block, in ascending order" do |
||||||
|
first_block = insert(:block) |
||||||
|
second_block = insert(:block) |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
2 |
||||||
|
|> insert_list(:transaction, from_address: address) |
||||||
|
|> with_block(second_block) |
||||||
|
|
||||||
|
2 |
||||||
|
|> insert_list(:transaction, from_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
2 |
||||||
|
|> insert_list(:transaction, from_address: address) |
||||||
|
|> with_block(first_block) |
||||||
|
|
||||||
|
found_transactions = Etherscan.list_transactions(address.hash) |
||||||
|
|
||||||
|
block_numbers_order = Enum.map(found_transactions, & &1.block_number) |
||||||
|
|
||||||
|
assert block_numbers_order == Enum.sort(block_numbers_order) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue