Add module TokenBalanceReader

pull/534/head
Felipe Renan 6 years ago
parent 3eb7c9f7ec
commit dc6bc8f51d
  1. 74
      apps/explorer/lib/explorer/token/balance_reader.ex
  2. 130
      apps/explorer/test/explorer/token/balance_reader_test.exs

@ -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…
Cancel
Save