You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1486 lines
43 KiB
1486 lines
43 KiB
defmodule Explorer.EtherscanTest do
|
|
use Explorer.DataCase
|
|
|
|
import Explorer.Factory
|
|
|
|
alias Explorer.{Etherscan, Chain}
|
|
alias Explorer.Chain.{Transaction, Wei}
|
|
|
|
describe "list_transactions/2" 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)
|
|
contract_address = insert(:contract_address)
|
|
|
|
transaction =
|
|
:transaction
|
|
|> insert(from_address: address, to_address: nil)
|
|
|> with_contract_creation(contract_address)
|
|
|> with_block()
|
|
|
|
%{created_contract_address_hash: contract_address_hash} =
|
|
:internal_transaction_create
|
|
|> insert(
|
|
transaction: transaction,
|
|
index: 0,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index
|
|
)
|
|
|> with_contract_creation(contract_address)
|
|
|
|
[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)
|
|
|
|
block_height = Chain.block_height()
|
|
expected_confirmations = block_height - transaction.block_number
|
|
|
|
assert found_transaction.confirmations == expected_confirmations
|
|
end
|
|
|
|
test "loads created_contract_address_hash if available" do
|
|
address = insert(:address)
|
|
contract_address = insert(:contract_address)
|
|
|
|
transaction =
|
|
:transaction
|
|
|> insert(from_address: address, to_address: nil)
|
|
|> with_contract_creation(contract_address)
|
|
|> with_block()
|
|
|
|
%{created_contract_address_hash: contract_hash} =
|
|
:internal_transaction_create
|
|
|> insert(
|
|
transaction: transaction,
|
|
index: 0,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index
|
|
)
|
|
|> with_contract_creation(contract_address)
|
|
|
|
[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 (default)" 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
|
|
|
|
test "orders transactions by block, in descending 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)
|
|
|
|
options = %{order_by_direction: :desc}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
block_numbers_order = Enum.map(found_transactions, & &1.block_number)
|
|
|
|
assert block_numbers_order == Enum.sort(block_numbers_order, &(&1 >= &2))
|
|
end
|
|
|
|
test "with page_size and page_number options" do
|
|
first_block = insert(:block)
|
|
second_block = insert(:block)
|
|
third_block = insert(:block)
|
|
address = insert(:address)
|
|
|
|
second_block_transactions =
|
|
2
|
|
|> insert_list(:transaction, from_address: address)
|
|
|> with_block(second_block)
|
|
|
|
third_block_transactions =
|
|
2
|
|
|> insert_list(:transaction, from_address: address)
|
|
|> with_block(third_block)
|
|
|
|
first_block_transactions =
|
|
2
|
|
|> insert_list(:transaction, from_address: address)
|
|
|> with_block(first_block)
|
|
|
|
options = %{page_number: 1, page_size: 2}
|
|
|
|
page1_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
page1_hashes = Enum.map(page1_transactions, & &1.hash)
|
|
|
|
assert length(page1_transactions) == 2
|
|
|
|
for transaction <- first_block_transactions do
|
|
assert transaction.hash in page1_hashes
|
|
end
|
|
|
|
options = %{page_number: 2, page_size: 2}
|
|
|
|
page2_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
page2_hashes = Enum.map(page2_transactions, & &1.hash)
|
|
|
|
assert length(page2_transactions) == 2
|
|
|
|
for transaction <- second_block_transactions do
|
|
assert transaction.hash in page2_hashes
|
|
end
|
|
|
|
options = %{page_number: 3, page_size: 2}
|
|
|
|
page3_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
page3_hashes = Enum.map(page3_transactions, & &1.hash)
|
|
|
|
assert length(page3_transactions) == 2
|
|
|
|
for transaction <- third_block_transactions do
|
|
assert transaction.hash in page3_hashes
|
|
end
|
|
|
|
options = %{page_number: 4, page_size: 2}
|
|
|
|
assert Etherscan.list_transactions(address.hash, options) == []
|
|
end
|
|
|
|
test "with start and end block options" do
|
|
blocks = [_, second_block, third_block, _] = insert_list(4, :block)
|
|
address = insert(:address)
|
|
|
|
for block <- blocks do
|
|
2
|
|
|> insert_list(:transaction, from_address: address)
|
|
|> with_block(block)
|
|
end
|
|
|
|
options = %{
|
|
start_block: second_block.number,
|
|
end_block: third_block.number
|
|
}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
expected_block_numbers = [second_block.number, third_block.number]
|
|
|
|
assert length(found_transactions) == 4
|
|
|
|
for transaction <- found_transactions do
|
|
assert transaction.block_number in expected_block_numbers
|
|
end
|
|
end
|
|
|
|
test "with start_block but no end_block option" do
|
|
blocks = [_, _, third_block, fourth_block] = insert_list(4, :block)
|
|
address = insert(:address)
|
|
|
|
for block <- blocks do
|
|
2
|
|
|> insert_list(:transaction, from_address: address)
|
|
|> with_block(block)
|
|
end
|
|
|
|
options = %{
|
|
start_block: third_block.number
|
|
}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
expected_block_numbers = [third_block.number, fourth_block.number]
|
|
|
|
assert length(found_transactions) == 4
|
|
|
|
for transaction <- found_transactions do
|
|
assert transaction.block_number in expected_block_numbers
|
|
end
|
|
end
|
|
|
|
test "with end_block but no start_block option" do
|
|
blocks = [first_block, second_block, _, _] = insert_list(4, :block)
|
|
address = insert(:address)
|
|
|
|
for block <- blocks do
|
|
2
|
|
|> insert_list(:transaction, from_address: address)
|
|
|> with_block(block)
|
|
end
|
|
|
|
options = %{
|
|
end_block: second_block.number
|
|
}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
expected_block_numbers = [first_block.number, second_block.number]
|
|
|
|
assert length(found_transactions) == 4
|
|
|
|
for transaction <- found_transactions do
|
|
assert transaction.block_number in expected_block_numbers
|
|
end
|
|
end
|
|
|
|
test "with start and end timestamp options" do
|
|
now = Timex.now()
|
|
timestamp1 = Timex.shift(now, hours: -1)
|
|
timestamp2 = Timex.shift(now, hours: -3)
|
|
timestamp3 = Timex.shift(now, hours: -6)
|
|
blocks1 = insert_list(2, :block, timestamp: timestamp1)
|
|
blocks2 = [third_block, fourth_block] = insert_list(2, :block, timestamp: timestamp2)
|
|
blocks3 = insert_list(2, :block, timestamp: timestamp3)
|
|
address = insert(:address)
|
|
|
|
for block <- Enum.concat([blocks1, blocks2, blocks3]) do
|
|
2
|
|
|> insert_list(:transaction, from_address: address)
|
|
|> with_block(block)
|
|
end
|
|
|
|
start_timestamp = Timex.shift(now, hours: -4)
|
|
end_timestamp = Timex.shift(now, hours: -2)
|
|
|
|
options = %{
|
|
start_timestamp: start_timestamp,
|
|
end_timestamp: end_timestamp
|
|
}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
expected_block_numbers = [third_block.number, fourth_block.number]
|
|
|
|
assert length(found_transactions) == 4
|
|
|
|
for transaction <- found_transactions do
|
|
assert transaction.block_number in expected_block_numbers
|
|
end
|
|
end
|
|
|
|
test "with filter_by: 'to' option with one matching transaction" do
|
|
address = insert(:address)
|
|
contract_address = insert(:contract_address)
|
|
|
|
:transaction
|
|
|> insert(to_address: address)
|
|
|> with_block()
|
|
|
|
:transaction
|
|
|> insert(from_address: address, to_address: nil)
|
|
|> with_contract_creation(contract_address)
|
|
|> with_block()
|
|
|
|
options = %{filter_by: "to"}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
assert length(found_transactions) == 1
|
|
end
|
|
|
|
test "with filter_by: 'to' option with non-matching transaction" do
|
|
address = insert(:address)
|
|
contract_address = insert(:contract_address)
|
|
|
|
:transaction
|
|
|> insert(from_address: address, to_address: nil)
|
|
|> with_contract_creation(contract_address)
|
|
|> with_block()
|
|
|
|
options = %{filter_by: "to"}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
assert length(found_transactions) == 0
|
|
end
|
|
|
|
test "with filter_by: 'from' option with one matching transaction" do
|
|
address = insert(:address)
|
|
|
|
:transaction
|
|
|> insert(to_address: address)
|
|
|> with_block()
|
|
|
|
:transaction
|
|
|> insert(from_address: address)
|
|
|> with_block()
|
|
|
|
options = %{filter_by: "from"}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
assert length(found_transactions) == 1
|
|
end
|
|
|
|
test "with filter_by: 'from' option with non-matching transaction" do
|
|
address = insert(:address)
|
|
other_address = insert(:address)
|
|
|
|
:transaction
|
|
|> insert(from_address: other_address, to_address: nil)
|
|
|> with_block()
|
|
|
|
options = %{filter_by: "from"}
|
|
|
|
found_transactions = Etherscan.list_transactions(address.hash, options)
|
|
|
|
assert length(found_transactions) == 0
|
|
end
|
|
end
|
|
|
|
describe "list_internal_transactions/1 with transaction hash" 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,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index
|
|
)
|
|
|> 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,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.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,
|
|
block_number: transaction1.block_number,
|
|
transaction_index: transaction1.index
|
|
)
|
|
|
|
insert(:internal_transaction,
|
|
transaction: transaction1,
|
|
index: 1,
|
|
block_number: transaction1.block_number,
|
|
transaction_index: transaction1.index
|
|
)
|
|
|
|
insert(:internal_transaction,
|
|
transaction: transaction2,
|
|
index: 0,
|
|
type: :reward,
|
|
block_number: transaction2.block_number,
|
|
transaction_index: transaction2.index
|
|
)
|
|
|
|
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
|
|
|
|
describe "list_internal_transactions/2 with address hash" do
|
|
test "with empty db" do
|
|
address = build(:address)
|
|
|
|
assert Etherscan.list_internal_transactions(address.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,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index
|
|
)
|
|
|> with_contract_creation(contract_address)
|
|
|
|
[found_internal_transaction] = Etherscan.list_internal_transactions(address.hash)
|
|
|
|
expected = %{
|
|
block_number: block.number,
|
|
block_timestamp: block.timestamp,
|
|
from_address_hash: internal_transaction.from_address_hash,
|
|
to_address_hash: internal_transaction.to_address_hash,
|
|
value: internal_transaction.value,
|
|
created_contract_address_hash: internal_transaction.created_contract_address_hash,
|
|
input: internal_transaction.input,
|
|
index: internal_transaction.index,
|
|
transaction_hash: internal_transaction.transaction_hash,
|
|
type: internal_transaction.type,
|
|
gas: internal_transaction.gas,
|
|
gas_used: internal_transaction.gas_used,
|
|
error: internal_transaction.error
|
|
}
|
|
|
|
assert found_internal_transaction == expected
|
|
end
|
|
|
|
test "with address with 0 internal transactions" do
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
assert Etherscan.list_internal_transactions(transaction.from_address_hash) == []
|
|
end
|
|
|
|
test "with address with multiple internal transactions" do
|
|
address = insert(:address)
|
|
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
for index <- 0..2 do
|
|
internal_transaction_details = %{
|
|
transaction: transaction,
|
|
index: index,
|
|
from_address: address,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index
|
|
}
|
|
|
|
insert(:internal_transaction, internal_transaction_details)
|
|
end
|
|
|
|
found_internal_transactions = Etherscan.list_internal_transactions(address.hash)
|
|
|
|
assert length(found_internal_transactions) == 3
|
|
end
|
|
|
|
test "only returns internal transactions associated to the given address" do
|
|
address1 = insert(:address)
|
|
address2 = insert(:address)
|
|
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
insert(:internal_transaction,
|
|
transaction: transaction,
|
|
index: 0,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index,
|
|
created_contract_address: address1
|
|
)
|
|
|
|
insert(:internal_transaction,
|
|
transaction: transaction,
|
|
index: 1,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index,
|
|
from_address: address1
|
|
)
|
|
|
|
insert(:internal_transaction,
|
|
transaction: transaction,
|
|
index: 2,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index,
|
|
to_address: address1
|
|
)
|
|
|
|
insert(:internal_transaction,
|
|
transaction: transaction,
|
|
index: 3,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index,
|
|
from_address: address2
|
|
)
|
|
|
|
internal_transactions1 = Etherscan.list_internal_transactions(address1.hash)
|
|
|
|
assert length(internal_transactions1) == 3
|
|
|
|
internal_transactions2 = Etherscan.list_internal_transactions(address2.hash)
|
|
|
|
assert length(internal_transactions2) == 1
|
|
end
|
|
|
|
test "with pagination options" do
|
|
address = insert(:address)
|
|
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
for index <- 0..2 do
|
|
internal_transaction_details = %{
|
|
transaction: transaction,
|
|
index: index,
|
|
from_address: address,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index
|
|
}
|
|
|
|
insert(:internal_transaction, internal_transaction_details)
|
|
end
|
|
|
|
options1 = %{
|
|
page_number: 1,
|
|
page_size: 2
|
|
}
|
|
|
|
found_internal_transactions1 = Etherscan.list_internal_transactions(address.hash, options1)
|
|
|
|
assert length(found_internal_transactions1) == 2
|
|
|
|
options2 = %{
|
|
page_number: 2,
|
|
page_size: 2
|
|
}
|
|
|
|
found_internal_transactions2 = Etherscan.list_internal_transactions(address.hash, options2)
|
|
|
|
assert length(found_internal_transactions2) == 1
|
|
end
|
|
|
|
test "with start and end block options" do
|
|
blocks = [_, second_block, third_block, _] = insert_list(4, :block)
|
|
address = insert(:address)
|
|
|
|
for block <- blocks, index <- 0..1 do
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(block)
|
|
|
|
internal_transaction_details = %{
|
|
transaction: transaction,
|
|
index: index,
|
|
from_address: address,
|
|
block_number: transaction.block_number,
|
|
transaction_index: transaction.index
|
|
}
|
|
|
|
insert(:internal_transaction, internal_transaction_details)
|
|
end
|
|
|
|
options = %{
|
|
start_block: second_block.number,
|
|
end_block: third_block.number
|
|
}
|
|
|
|
found_internal_transactions = Etherscan.list_internal_transactions(address.hash, options)
|
|
|
|
expected_block_numbers = [second_block.number, third_block.number]
|
|
|
|
assert length(found_internal_transactions) == 4
|
|
|
|
for internal_transaction <- found_internal_transactions do
|
|
assert internal_transaction.block_number in expected_block_numbers
|
|
end
|
|
end
|
|
|
|
# Note that `list_internal_transactions/2` 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
|
|
|
|
describe "list_token_transfers/2" do
|
|
test "with empty db" do
|
|
address = build(:address)
|
|
|
|
assert Etherscan.list_token_transfers(address.hash, nil) == []
|
|
end
|
|
|
|
test "with from address" do
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
token_transfer = insert(:token_transfer, transaction: transaction)
|
|
|
|
[found_token_transfer] = Etherscan.list_token_transfers(token_transfer.from_address_hash, nil)
|
|
|
|
assert token_transfer.from_address_hash == found_token_transfer.from_address_hash
|
|
end
|
|
|
|
test "with to address" do
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
token_transfer = insert(:token_transfer, transaction: transaction)
|
|
|
|
[found_token_transfer] = Etherscan.list_token_transfers(token_transfer.to_address_hash, nil)
|
|
|
|
assert token_transfer.to_address_hash == found_token_transfer.to_address_hash
|
|
end
|
|
|
|
test "with address with 0 token transfers" do
|
|
address = insert(:address)
|
|
|
|
assert Etherscan.list_token_transfers(address.hash, nil) == []
|
|
end
|
|
|
|
test "with address with multiple token transfers" do
|
|
address1 = insert(:address)
|
|
address2 = insert(:address)
|
|
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
insert(:token_transfer, from_address: address1, transaction: transaction)
|
|
insert(:token_transfer, from_address: address1, transaction: transaction)
|
|
insert(:token_transfer, from_address: address2, transaction: transaction)
|
|
|
|
found_token_transfers = Etherscan.list_token_transfers(address1.hash, nil)
|
|
|
|
assert length(found_token_transfers) == 2
|
|
|
|
for found_token_transfer <- found_token_transfers do
|
|
assert found_token_transfer.from_address_hash == address1.hash
|
|
end
|
|
end
|
|
|
|
test "confirmations value is calculated correctly" do
|
|
insert(:block)
|
|
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
token_transfer = insert(:token_transfer, transaction: transaction)
|
|
|
|
insert(:block)
|
|
|
|
[found_token_transfer] = Etherscan.list_token_transfers(token_transfer.from_address_hash, nil)
|
|
|
|
block_height = Chain.block_height()
|
|
expected_confirmations = block_height - transaction.block_number
|
|
|
|
assert found_token_transfer.confirmations == expected_confirmations
|
|
end
|
|
|
|
test "returns all required fields" do
|
|
transaction =
|
|
%{block: block} =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
token_transfer = insert(:token_transfer, transaction: transaction)
|
|
|
|
{:ok, token} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash)
|
|
|
|
[found_token_transfer] = Etherscan.list_token_transfers(token_transfer.from_address_hash, nil)
|
|
|
|
assert found_token_transfer.block_number == transaction.block_number
|
|
assert found_token_transfer.block_timestamp == block.timestamp
|
|
assert found_token_transfer.transaction_hash == token_transfer.transaction_hash
|
|
assert found_token_transfer.transaction_nonce == transaction.nonce
|
|
assert found_token_transfer.block_hash == block.hash
|
|
assert found_token_transfer.from_address_hash == token_transfer.from_address_hash
|
|
assert found_token_transfer.token_contract_address_hash == token_transfer.token_contract_address_hash
|
|
assert found_token_transfer.to_address_hash == token_transfer.to_address_hash
|
|
assert found_token_transfer.amount == token_transfer.amount
|
|
assert found_token_transfer.token_name == token.name
|
|
assert found_token_transfer.token_symbol == token.symbol
|
|
assert found_token_transfer.token_decimals == token.decimals
|
|
assert found_token_transfer.transaction_index == transaction.index
|
|
assert found_token_transfer.transaction_gas == transaction.gas
|
|
assert found_token_transfer.transaction_gas_price == transaction.gas_price
|
|
assert found_token_transfer.transaction_gas_used == transaction.gas_used
|
|
assert found_token_transfer.transaction_cumulative_gas_used == transaction.cumulative_gas_used
|
|
assert found_token_transfer.transaction_input == transaction.input
|
|
# There is a separate test to ensure confirmations are calculated correctly.
|
|
assert found_token_transfer.confirmations
|
|
end
|
|
|
|
test "orders token transfers by block, in ascending order (default)" do
|
|
address = insert(:address)
|
|
|
|
first_block = insert(:block)
|
|
second_block = insert(:block)
|
|
|
|
transaction1 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(second_block)
|
|
|
|
transaction2 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
transaction3 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(first_block)
|
|
|
|
insert(:token_transfer, from_address: address, transaction: transaction2)
|
|
insert(:token_transfer, from_address: address, transaction: transaction1)
|
|
insert(:token_transfer, from_address: address, transaction: transaction3)
|
|
|
|
found_token_transfers = Etherscan.list_token_transfers(address.hash, nil)
|
|
|
|
block_numbers_order = Enum.map(found_token_transfers, & &1.block_number)
|
|
|
|
assert block_numbers_order == Enum.sort(block_numbers_order)
|
|
end
|
|
|
|
test "orders token transfers by block, in descending order" do
|
|
address = insert(:address)
|
|
|
|
first_block = insert(:block)
|
|
second_block = insert(:block)
|
|
|
|
transaction1 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(second_block)
|
|
|
|
transaction2 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
transaction3 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(first_block)
|
|
|
|
insert(:token_transfer, from_address: address, transaction: transaction2)
|
|
insert(:token_transfer, from_address: address, transaction: transaction1)
|
|
insert(:token_transfer, from_address: address, transaction: transaction3)
|
|
|
|
options = %{order_by_direction: :desc}
|
|
|
|
found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options)
|
|
|
|
block_numbers_order = Enum.map(found_token_transfers, & &1.block_number)
|
|
|
|
assert block_numbers_order == Enum.sort(block_numbers_order, &(&1 >= &2))
|
|
end
|
|
|
|
test "with page_size and page_number options" do
|
|
address = insert(:address)
|
|
|
|
first_block = insert(:block)
|
|
second_block = insert(:block)
|
|
third_block = insert(:block)
|
|
|
|
transaction1 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(first_block)
|
|
|
|
transaction2 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(second_block)
|
|
|
|
transaction3 =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(third_block)
|
|
|
|
second_block_token_transfers = insert_list(2, :token_transfer, from_address: address, transaction: transaction2)
|
|
|
|
third_block_token_transfers = insert_list(2, :token_transfer, from_address: address, transaction: transaction3)
|
|
|
|
first_block_token_transfers = insert_list(2, :token_transfer, from_address: address, transaction: transaction1)
|
|
|
|
options1 = %{page_number: 1, page_size: 2}
|
|
|
|
page1_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options1)
|
|
|
|
page1_hashes = Enum.map(page1_token_transfers, & &1.transaction_hash)
|
|
|
|
assert length(page1_token_transfers) == 2
|
|
|
|
for token_transfer <- first_block_token_transfers do
|
|
assert token_transfer.transaction_hash in page1_hashes
|
|
end
|
|
|
|
options2 = %{page_number: 2, page_size: 2}
|
|
|
|
page2_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options2)
|
|
|
|
page2_hashes = Enum.map(page2_token_transfers, & &1.transaction_hash)
|
|
|
|
assert length(page2_token_transfers) == 2
|
|
|
|
for token_transfer <- second_block_token_transfers do
|
|
assert token_transfer.transaction_hash in page2_hashes
|
|
end
|
|
|
|
options3 = %{page_number: 3, page_size: 2}
|
|
|
|
page3_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options3)
|
|
|
|
page3_hashes = Enum.map(page3_token_transfers, & &1.transaction_hash)
|
|
|
|
assert length(page3_token_transfers) == 2
|
|
|
|
for token_transfer <- third_block_token_transfers do
|
|
assert token_transfer.transaction_hash in page3_hashes
|
|
end
|
|
|
|
options4 = %{page_number: 4, page_size: 2}
|
|
|
|
assert Etherscan.list_token_transfers(address.hash, nil, options4) == []
|
|
end
|
|
|
|
test "with start and end block options" do
|
|
blocks = [_, second_block, third_block, _] = insert_list(4, :block)
|
|
address = insert(:address)
|
|
|
|
for block <- blocks do
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(block)
|
|
|
|
insert(:token_transfer, from_address: address, transaction: transaction)
|
|
end
|
|
|
|
options = %{
|
|
start_block: second_block.number,
|
|
end_block: third_block.number
|
|
}
|
|
|
|
found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options)
|
|
|
|
expected_block_numbers = [second_block.number, third_block.number]
|
|
|
|
assert length(found_token_transfers) == 2
|
|
|
|
for token_transfer <- found_token_transfers do
|
|
assert token_transfer.block_number in expected_block_numbers
|
|
end
|
|
end
|
|
|
|
test "with start_block but no end_block option" do
|
|
blocks = [_, _, third_block, fourth_block] = insert_list(4, :block)
|
|
address = insert(:address)
|
|
|
|
for block <- blocks do
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(block)
|
|
|
|
insert(:token_transfer, from_address: address, transaction: transaction)
|
|
end
|
|
|
|
options = %{start_block: third_block.number}
|
|
|
|
found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options)
|
|
|
|
expected_block_numbers = [third_block.number, fourth_block.number]
|
|
|
|
assert length(found_token_transfers) == 2
|
|
|
|
for token_transfer <- found_token_transfers do
|
|
assert token_transfer.block_number in expected_block_numbers
|
|
end
|
|
end
|
|
|
|
test "with end_block but no start_block option" do
|
|
blocks = [first_block, second_block, _, _] = insert_list(4, :block)
|
|
address = insert(:address)
|
|
|
|
for block <- blocks do
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block(block)
|
|
|
|
insert(:token_transfer, from_address: address, transaction: transaction)
|
|
end
|
|
|
|
options = %{end_block: second_block.number}
|
|
|
|
found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options)
|
|
|
|
expected_block_numbers = [first_block.number, second_block.number]
|
|
|
|
assert length(found_token_transfers) == 2
|
|
|
|
for token_transfer <- found_token_transfers do
|
|
assert token_transfer.block_number in expected_block_numbers
|
|
end
|
|
end
|
|
|
|
test "with contract_address option" do
|
|
address = insert(:address)
|
|
|
|
contract_address = insert(:contract_address)
|
|
|
|
insert(:token, contract_address: contract_address)
|
|
|
|
transaction =
|
|
:transaction
|
|
|> insert()
|
|
|> with_block()
|
|
|
|
insert(:token_transfer, from_address: address, transaction: transaction)
|
|
insert(:token_transfer, from_address: address, token_contract_address: contract_address, transaction: transaction)
|
|
|
|
[found_token_transfer] = Etherscan.list_token_transfers(address.hash, contract_address.hash)
|
|
|
|
assert found_token_transfer.token_contract_address_hash == contract_address.hash
|
|
end
|
|
end
|
|
|
|
describe "list_blocks/1" do
|
|
test "it returns all required fields" do
|
|
%{block_range: range} = emission_reward = insert(:emission_reward)
|
|
|
|
block = insert(:block, number: Enum.random(Range.new(range.from, range.to)))
|
|
|
|
# irrelevant transaction
|
|
insert(:transaction)
|
|
|
|
:transaction
|
|
|> insert(gas_price: 1)
|
|
|> with_block(block, gas_used: 1)
|
|
|
|
expected_reward =
|
|
emission_reward.reward
|
|
|> Wei.to(:wei)
|
|
|> Decimal.add(Decimal.new(1))
|
|
|> Wei.from(:wei)
|
|
|
|
expected = [
|
|
%{
|
|
number: block.number,
|
|
timestamp: block.timestamp,
|
|
reward: expected_reward
|
|
}
|
|
]
|
|
|
|
assert Etherscan.list_blocks(block.miner_hash) == expected
|
|
end
|
|
|
|
test "with block containing multiple transactions" do
|
|
%{block_range: range} = emission_reward = insert(:emission_reward)
|
|
|
|
block = insert(:block, number: Enum.random(Range.new(range.from, range.to)))
|
|
|
|
# irrelevant transaction
|
|
insert(:transaction)
|
|
|
|
:transaction
|
|
|> insert(gas_price: 1)
|
|
|> with_block(block, gas_used: 1)
|
|
|
|
:transaction
|
|
|> insert(gas_price: 1)
|
|
|> with_block(block, gas_used: 2)
|
|
|
|
expected_reward =
|
|
emission_reward.reward
|
|
|> Wei.to(:wei)
|
|
|> Decimal.add(Decimal.new(3))
|
|
|> Wei.from(:wei)
|
|
|
|
expected = [
|
|
%{
|
|
number: block.number,
|
|
timestamp: block.timestamp,
|
|
reward: expected_reward
|
|
}
|
|
]
|
|
|
|
assert Etherscan.list_blocks(block.miner_hash) == expected
|
|
end
|
|
|
|
test "with block without transactions" do
|
|
%{block_range: range} = emission_reward = insert(:emission_reward)
|
|
|
|
block = insert(:block, number: Enum.random(Range.new(range.from, range.to)))
|
|
|
|
# irrelevant transaction
|
|
insert(:transaction)
|
|
|
|
expected = [
|
|
%{
|
|
number: block.number,
|
|
timestamp: block.timestamp,
|
|
reward: emission_reward.reward
|
|
}
|
|
]
|
|
|
|
assert Etherscan.list_blocks(block.miner_hash) == expected
|
|
end
|
|
|
|
test "with multiple blocks" do
|
|
%{block_range: range} = emission_reward = insert(:emission_reward)
|
|
|
|
block_numbers = Range.new(range.from, range.to)
|
|
|
|
[block_number1, block_number2] = Enum.take(block_numbers, 2)
|
|
|
|
address = insert(:address)
|
|
|
|
block1 = insert(:block, number: block_number1, miner: address)
|
|
block2 = insert(:block, number: block_number2, miner: address)
|
|
|
|
# irrelevant transaction
|
|
insert(:transaction)
|
|
|
|
:transaction
|
|
|> insert(gas_price: 2)
|
|
|> with_block(block1, gas_used: 2)
|
|
|
|
:transaction
|
|
|> insert(gas_price: 2)
|
|
|> with_block(block1, gas_used: 2)
|
|
|
|
:transaction
|
|
|> insert(gas_price: 3)
|
|
|> with_block(block2, gas_used: 3)
|
|
|
|
:transaction
|
|
|> insert(gas_price: 3)
|
|
|> with_block(block2, gas_used: 3)
|
|
|
|
expected_reward_block1 =
|
|
emission_reward.reward
|
|
|> Wei.to(:wei)
|
|
|> Decimal.add(Decimal.new(8))
|
|
|> Wei.from(:wei)
|
|
|
|
expected_reward_block2 =
|
|
emission_reward.reward
|
|
|> Wei.to(:wei)
|
|
|> Decimal.add(Decimal.new(18))
|
|
|> Wei.from(:wei)
|
|
|
|
expected = [
|
|
%{
|
|
number: block2.number,
|
|
timestamp: block2.timestamp,
|
|
reward: expected_reward_block2
|
|
},
|
|
%{
|
|
number: block1.number,
|
|
timestamp: block1.timestamp,
|
|
reward: expected_reward_block1
|
|
}
|
|
]
|
|
|
|
assert Etherscan.list_blocks(address.hash) == expected
|
|
end
|
|
|
|
test "with pagination options" do
|
|
%{block_range: range} = emission_reward = insert(:emission_reward)
|
|
|
|
block_numbers = Range.new(range.from, range.to)
|
|
|
|
[block_number1, block_number2] = Enum.take(block_numbers, 2)
|
|
|
|
address = insert(:address)
|
|
|
|
block1 = insert(:block, number: block_number1, miner: address)
|
|
block2 = insert(:block, number: block_number2, miner: address)
|
|
|
|
:transaction
|
|
|> insert(gas_price: 2)
|
|
|> with_block(block1, gas_used: 2)
|
|
|
|
expected_reward =
|
|
emission_reward.reward
|
|
|> Wei.to(:wei)
|
|
|> Decimal.add(Decimal.new(4))
|
|
|> Wei.from(:wei)
|
|
|
|
expected1 = [
|
|
%{
|
|
number: block2.number,
|
|
timestamp: block2.timestamp,
|
|
reward: emission_reward.reward
|
|
}
|
|
]
|
|
|
|
expected2 = [
|
|
%{
|
|
number: block1.number,
|
|
timestamp: block1.timestamp,
|
|
reward: expected_reward
|
|
}
|
|
]
|
|
|
|
options1 = %{page_number: 1, page_size: 1}
|
|
options2 = %{page_number: 2, page_size: 1}
|
|
options3 = %{page_number: 3, page_size: 1}
|
|
|
|
assert Etherscan.list_blocks(address.hash, options1) == expected1
|
|
assert Etherscan.list_blocks(address.hash, options2) == expected2
|
|
assert Etherscan.list_blocks(address.hash, options3) == []
|
|
end
|
|
end
|
|
|
|
describe "get_token_balance/2" do
|
|
test "with a single matching token_balance record" do
|
|
token_balance =
|
|
%{token_contract_address_hash: contract_address_hash, address_hash: address_hash} = insert(:token_balance)
|
|
|
|
found_token_balance = Etherscan.get_token_balance(contract_address_hash, address_hash)
|
|
|
|
assert found_token_balance.id == token_balance.id
|
|
end
|
|
|
|
test "returns token balance in latest block" do
|
|
token = insert(:token)
|
|
|
|
contract_address_hash = token.contract_address_hash
|
|
|
|
address = insert(:address)
|
|
|
|
token_details1 = %{
|
|
token_contract_address_hash: contract_address_hash,
|
|
address: address,
|
|
block_number: 5
|
|
}
|
|
|
|
token_details2 = %{
|
|
token_contract_address_hash: contract_address_hash,
|
|
address: address,
|
|
block_number: 15
|
|
}
|
|
|
|
token_details3 = %{
|
|
token_contract_address_hash: contract_address_hash,
|
|
address: address,
|
|
block_number: 10
|
|
}
|
|
|
|
_token_balance1 = insert(:token_balance, token_details1)
|
|
token_balance2 = insert(:token_balance, token_details2)
|
|
_token_balance3 = insert(:token_balance, token_details3)
|
|
|
|
found_token_balance = Etherscan.get_token_balance(contract_address_hash, address.hash)
|
|
|
|
assert found_token_balance.id == token_balance2.id
|
|
end
|
|
end
|
|
|
|
describe "list_tokens/1" do
|
|
test "returns the tokens owned by an address hash" do
|
|
address = insert(:address)
|
|
|
|
token_balance =
|
|
:token_balance
|
|
|> insert(address: address)
|
|
|> Repo.preload(:token)
|
|
|
|
insert(:token_balance, address: build(:address))
|
|
|
|
token_list = Etherscan.list_tokens(address.hash)
|
|
|
|
expected_tokens = [
|
|
%{
|
|
balance: token_balance.value,
|
|
contract_address_hash: token_balance.token_contract_address_hash,
|
|
name: token_balance.token.name,
|
|
decimals: token_balance.token.decimals,
|
|
symbol: token_balance.token.symbol
|
|
}
|
|
]
|
|
|
|
assert token_list == expected_tokens
|
|
end
|
|
|
|
test "returns the latest known balance per token" do
|
|
# The latest balance is the one with the latest block number
|
|
address = insert(:address)
|
|
token = insert(:token)
|
|
|
|
token_balance_details1 = %{
|
|
address: address,
|
|
token_contract_address_hash: token.contract_address.hash,
|
|
block_number: 1
|
|
}
|
|
|
|
token_balance_details2 = %{
|
|
address: address,
|
|
token_contract_address_hash: token.contract_address.hash,
|
|
block_number: 2
|
|
}
|
|
|
|
token_balance_details3 = %{
|
|
address: address,
|
|
token_contract_address_hash: token.contract_address.hash,
|
|
block_number: 3
|
|
}
|
|
|
|
insert(:token_balance, token_balance_details1)
|
|
|
|
token_balance =
|
|
:token_balance
|
|
|> insert(token_balance_details3)
|
|
|> Repo.preload(:token)
|
|
|
|
insert(:token_balance, token_balance_details2)
|
|
|
|
token_list = Etherscan.list_tokens(address.hash)
|
|
|
|
expected_tokens = [
|
|
%{
|
|
balance: token_balance.value,
|
|
contract_address_hash: token_balance.token_contract_address_hash,
|
|
name: token_balance.token.name,
|
|
decimals: token_balance.token.decimals,
|
|
symbol: token_balance.token.symbol
|
|
}
|
|
]
|
|
|
|
assert token_list == expected_tokens
|
|
end
|
|
|
|
test "returns an empty list when there are no token balances" do
|
|
address = insert(:address)
|
|
|
|
insert(:token_balance, address: build(:address))
|
|
|
|
assert Etherscan.list_tokens(address.hash) == []
|
|
end
|
|
end
|
|
end
|
|
|