From dc6bc8f51d1ec8af076450082eed7cba1be019c1 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Thu, 9 Aug 2018 18:43:20 -0300 Subject: [PATCH] Add module TokenBalanceReader --- .../lib/explorer/token/balance_reader.ex | 74 ++++++++++ .../explorer/token/balance_reader_test.exs | 130 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 apps/explorer/lib/explorer/token/balance_reader.ex create mode 100644 apps/explorer/test/explorer/token/balance_reader_test.exs diff --git a/apps/explorer/lib/explorer/token/balance_reader.ex b/apps/explorer/lib/explorer/token/balance_reader.ex new file mode 100644 index 0000000000..c9ad273bc6 --- /dev/null +++ b/apps/explorer/lib/explorer/token/balance_reader.ex @@ -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 diff --git a/apps/explorer/test/explorer/token/balance_reader_test.exs b/apps/explorer/test/explorer/token/balance_reader_test.exs new file mode 100644 index 0000000000..8935cb5a96 --- /dev/null +++ b/apps/explorer/test/explorer/token/balance_reader_test.exs @@ -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