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