parent
3eb7c9f7ec
commit
dc6bc8f51d
@ -0,0 +1,74 @@ |
||||
defmodule Explorer.Token.BalanceReader do |
||||
@moduledoc """ |
||||
Reads Token's balances using Smart Contract functions from the blockchain. |
||||
""" |
||||
|
||||
alias Explorer.SmartContract.Reader |
||||
|
||||
@balance_function_abi [ |
||||
%{ |
||||
"type" => "function", |
||||
"stateMutability" => "view", |
||||
"payable" => false, |
||||
"outputs" => [ |
||||
%{ |
||||
"type" => "uint256", |
||||
"name" => "balance" |
||||
} |
||||
], |
||||
"name" => "balanceOf", |
||||
"inputs" => [ |
||||
%{ |
||||
"type" => "address", |
||||
"name" => "tokenOwner" |
||||
} |
||||
], |
||||
"constant" => true |
||||
} |
||||
] |
||||
|
||||
@doc """ |
||||
Fetches the token balances that were fetched without error and have balances more than 0. |
||||
""" |
||||
def fetch_token_balances_without_error(tokens, address_hash_string) do |
||||
tokens |
||||
|> fetch_token_balances_from_blockchain(address_hash_string) |
||||
|> Stream.filter(&token_without_error?/1) |
||||
|> Stream.map(&format_result/1) |
||||
|> Enum.filter(&tokens_with_no_zero_balance?/1) |
||||
end |
||||
|
||||
defp token_without_error?({:ok, _token}), do: true |
||||
defp token_without_error?({:error, _token}), do: false |
||||
defp format_result({:ok, token}), do: token |
||||
defp tokens_with_no_zero_balance?(%{balance: balance}), do: balance != 0 |
||||
|
||||
@doc """ |
||||
Fetches the token balances given the tokens and the address hash as string. |
||||
|
||||
This function is going to perform one request async for each token inside a list of tokens in |
||||
order to fetch the balance. |
||||
""" |
||||
@spec fetch_token_balances_from_blockchain([], String.t()) :: [] |
||||
def fetch_token_balances_from_blockchain(tokens, address_hash_string) do |
||||
tokens |
||||
|> Task.async_stream(&fetch_from_blockchain(&1, address_hash_string)) |
||||
|> Enum.map(&blockchain_result_from_tasks/1) |
||||
end |
||||
|
||||
defp fetch_from_blockchain(%{contract_address_hash: address_hash} = token, address_hash_string) do |
||||
address_hash |
||||
|> Reader.query_unverified_contract(@balance_function_abi, %{"balanceOf" => [address_hash_string]}) |
||||
|> format_blockchain_result(token) |
||||
end |
||||
|
||||
defp format_blockchain_result(%{"balanceOf" => {:ok, balance}}, token) do |
||||
{:ok, Map.put(token, :balance, balance)} |
||||
end |
||||
|
||||
defp format_blockchain_result(%{"balanceOf" => {:error, error}}, token) do |
||||
{:error, Map.put(token, :balance, error)} |
||||
end |
||||
|
||||
defp blockchain_result_from_tasks({:ok, blockchain_result}), do: blockchain_result |
||||
end |
@ -0,0 +1,130 @@ |
||||
defmodule Explorer.Token.BalanceReaderTest do |
||||
use EthereumJSONRPC.Case |
||||
use Explorer.DataCase |
||||
|
||||
doctest Explorer.Token.BalanceReader |
||||
|
||||
alias Explorer.Token.{BalanceReader} |
||||
alias Explorer.Chain.Hash |
||||
|
||||
import Mox |
||||
|
||||
setup :verify_on_exit! |
||||
setup :set_mox_global |
||||
|
||||
describe "fetch_token_balances_from_blockchain/2" do |
||||
test "fetches balances of tokens given the address hash" do |
||||
address = insert(:address) |
||||
token = insert(:token, contract_address: build(:contract_address)) |
||||
address_hash_string = Hash.to_string(address.hash) |
||||
|
||||
get_balance_from_blockchain() |
||||
|
||||
result = |
||||
[token] |
||||
|> BalanceReader.fetch_token_balances_from_blockchain(address_hash_string) |
||||
|> List.first() |
||||
|
||||
assert result == {:ok, Map.put(token, :balance, 1_000_000_000_000_000_000_000_000)} |
||||
end |
||||
|
||||
test "does not ignore calls that were returned with error" do |
||||
address = insert(:address) |
||||
token = insert(:token, contract_address: build(:contract_address)) |
||||
address_hash_string = Hash.to_string(address.hash) |
||||
|
||||
get_balance_from_blockchain_with_error() |
||||
|
||||
result = |
||||
[token] |
||||
|> BalanceReader.fetch_token_balances_from_blockchain(address_hash_string) |
||||
|> List.first() |
||||
|
||||
assert result == {:error, Map.put(token, :balance, "(-32015) VM execution error.")} |
||||
end |
||||
end |
||||
|
||||
describe "fetch_token_balances_without_error/2" do |
||||
test "filters token balances that were fetched without error" do |
||||
address = insert(:address) |
||||
token_a = insert(:token, contract_address: build(:contract_address)) |
||||
token_b = insert(:token, contract_address: build(:contract_address)) |
||||
address_hash_string = Hash.to_string(address.hash) |
||||
|
||||
get_balance_from_blockchain() |
||||
get_balance_from_blockchain_with_error() |
||||
|
||||
results = |
||||
[token_a, token_b] |
||||
|> BalanceReader.fetch_token_balances_without_error(address_hash_string) |
||||
|
||||
assert Enum.count(results) == 1 |
||||
assert List.first(results) == Map.put(token_a, :balance, 1_000_000_000_000_000_000_000_000) |
||||
end |
||||
|
||||
test "does not considers balances equal 0" do |
||||
address = insert(:address) |
||||
token = insert(:token, contract_address: build(:contract_address)) |
||||
address_hash_string = Hash.to_string(address.hash) |
||||
|
||||
get_balance_from_blockchain_with_balance_zero() |
||||
|
||||
results = |
||||
[token] |
||||
|> BalanceReader.fetch_token_balances_without_error(address_hash_string) |
||||
|
||||
assert Enum.count(results) == 0 |
||||
end |
||||
end |
||||
|
||||
defp get_balance_from_blockchain() do |
||||
expect( |
||||
EthereumJSONRPC.Mox, |
||||
:json_rpc, |
||||
fn [%{id: _, method: _, params: [%{data: _, to: _}]}], _options -> |
||||
{:ok, |
||||
[ |
||||
%{ |
||||
id: "balanceOf", |
||||
jsonrpc: "2.0", |
||||
result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" |
||||
} |
||||
]} |
||||
end |
||||
) |
||||
end |
||||
|
||||
defp get_balance_from_blockchain_with_balance_zero() do |
||||
expect( |
||||
EthereumJSONRPC.Mox, |
||||
:json_rpc, |
||||
fn [%{id: _, method: _, params: [%{data: _, to: _}]}], _options -> |
||||
{:ok, |
||||
[ |
||||
%{ |
||||
id: "balanceOf", |
||||
jsonrpc: "2.0", |
||||
result: "0x0000000000000000000000000000000000000000000000000000000000000000" |
||||
} |
||||
]} |
||||
end |
||||
) |
||||
end |
||||
|
||||
defp get_balance_from_blockchain_with_error() do |
||||
expect( |
||||
EthereumJSONRPC.Mox, |
||||
:json_rpc, |
||||
fn [%{id: _, method: _, params: [%{data: _, to: _}]}], _options -> |
||||
{:ok, |
||||
[ |
||||
%{ |
||||
error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, |
||||
id: "balanceOf", |
||||
jsonrpc: "2.0" |
||||
} |
||||
]} |
||||
end |
||||
) |
||||
end |
||||
end |
Loading…
Reference in new issue