Merge pull request #1560 from poanetwork/gs-batch-token-balance-requests

Allow executing smart contract functions in arbitrarily sized batches
pull/1562/head
Victor Baranov 6 years ago committed by GitHub
commit 5d5b8fa84d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .dialyzer-ignore
  2. 31
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
  3. 94
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex
  4. 65
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex
  5. 134
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs
  6. 133
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs
  7. 87
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs
  8. 88
      apps/explorer/lib/explorer/smart_contract/reader.ex
  9. 36
      apps/explorer/lib/explorer/token/balance_reader.ex
  10. 8
      apps/explorer/test/explorer/chain/supply/token_bridge_test.exs
  11. 15
      apps/explorer/test/explorer/smart_contract/reader_test.exs
  12. 32
      apps/explorer/test/explorer/token/balance_reader_test.exs
  13. 423
      apps/explorer/test/explorer/token/metadata_retriever_test.exs
  14. 16
      apps/explorer/test/explorer/validator/metadata_retriever_test.exs
  15. 3
      apps/indexer/lib/indexer/token_balance/fetcher.ex
  16. 61
      apps/indexer/lib/indexer/token_balances.ex
  17. 49
      apps/indexer/test/indexer/token/fetcher_test.exs
  18. 100
      apps/indexer/test/indexer/token/metadata_updater_test.exs
  19. 8
      apps/indexer/test/indexer/token_balance/fetcher_test.exs
  20. 55
      apps/indexer/test/indexer/token_balances_test.exs

@ -1,6 +1,6 @@
:0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3 :0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3
:0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2 :0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2
:0: Unknown type 'Elixir.Map':t/0 :0: Unknown type 'Elixir.Map':t/0
apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:413: Function timestamp_to_datetime/1 has no local return apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:390: Function timestamp_to_datetime/1 has no local return
apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: Function microseconds_time/1 has no local return apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: Function microseconds_time/1 has no local return
apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: The call 'Elixir.System':convert_time_unit(__@1::any(),'native','microseconds') breaks the contract (integer(),time_unit() | 'native',time_unit() | 'native') -> integer() apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: The call 'Elixir.System':convert_time_unit(__@1::any(),'native','microseconds') breaks the contract (integer(),time_unit() | 'native',time_unit() | 'native') -> integer()

@ -28,6 +28,7 @@ defmodule EthereumJSONRPC do
alias EthereumJSONRPC.{ alias EthereumJSONRPC.{
Block, Block,
Blocks, Blocks,
Contract,
FetchedBalances, FetchedBalances,
FetchedBeneficiaries, FetchedBeneficiaries,
FetchedCodes, FetchedCodes,
@ -160,33 +161,9 @@ defmodule EthereumJSONRPC do
} }
]} ]}
""" """
@spec execute_contract_functions( @spec execute_contract_functions([Contract.call()], [map()], json_rpc_named_arguments) :: [Contract.call_result()]
[%{contract_address: String.t(), data: String.t(), id: String.t()}], def execute_contract_functions(functions, abi, json_rpc_named_arguments) do
json_rpc_named_arguments, Contract.execute_contract_functions(functions, abi, json_rpc_named_arguments)
[{:block_number, non_neg_integer()}]
) :: {:ok, list()} | {:error, term()}
def execute_contract_functions(functions, json_rpc_named_arguments, opts \\ []) do
block_number = Keyword.get(opts, :block_number)
functions
|> Enum.map(&build_eth_call_payload(&1, block_number))
|> json_rpc(json_rpc_named_arguments)
end
defp build_eth_call_payload(
%{contract_address: address, data: data, id: id},
nil = _block_number
) do
params = [%{to: address, data: data}, "latest"]
request(%{id: id, method: "eth_call", params: params})
end
defp build_eth_call_payload(
%{contract_address: address, data: data, id: id},
block_number
) do
params = [%{to: address, data: data}, integer_to_quantity(block_number)]
request(%{id: id, method: "eth_call", params: params})
end end
@doc """ @doc """

@ -0,0 +1,94 @@
defmodule EthereumJSONRPC.Contract do
@moduledoc """
Smart contract functions executed by `eth_call`.
"""
import EthereumJSONRPC, only: [integer_to_quantity: 1, json_rpc: 2, request: 1]
alias EthereumJSONRPC.Encoder
@typedoc """
Call to a smart contract function.
* `:block_number` - the block in which to execute the function. Defaults to the `nil` to indicate
the latest block as determined by the remote node, which may differ from the latest block number
in `Explorer.Chain`.
"""
@type call :: %{
required(:contract_address) => String.t(),
required(:function_name) => String.t(),
required(:args) => [term()],
optional(:block_number) => EthereumJSONRPC.block_number()
}
@typedoc """
Result of calling a smart contract function.
"""
@type call_result :: {:ok, term()} | {:error, String.t()}
@spec execute_contract_functions([call()], [map()], EthereumJSONRPC.json_rpc_named_arguments()) :: [call_result()]
def execute_contract_functions(requests, abi, json_rpc_named_arguments) do
functions =
abi
|> ABI.parse_specification()
|> Enum.into(%{}, &{&1.function, &1})
requests_with_index = Enum.with_index(requests)
indexed_responses =
requests_with_index
|> Enum.map(fn {%{contract_address: contract_address, function_name: function_name, args: args} = request, index} ->
functions[function_name]
|> Encoder.encode_function_call(args)
|> eth_call_request(contract_address, index, Map.get(request, :block_number))
end)
|> json_rpc(json_rpc_named_arguments)
|> case do
{:ok, responses} -> responses
{:error, {:bad_gateway, _request_url}} -> raise "Bad gateway"
{:error, error} -> raise error
end
|> Enum.into(%{}, &{&1.id, &1})
Enum.map(requests_with_index, fn {%{function_name: function_name}, index} ->
indexed_responses[index]
|> case do
nil ->
{:error, "No result"}
response ->
{^index, result} = Encoder.decode_result(response, functions[function_name])
result
end
end)
rescue
error ->
Enum.map(requests, fn _ -> format_error(error) end)
end
defp eth_call_request(data, contract_address, id, block_number) do
block =
case block_number do
nil -> "latest"
block_number -> integer_to_quantity(block_number)
end
request(%{
id: id,
method: "eth_call",
params: [%{to: contract_address, data: data}, block]
})
end
defp format_error(message) when is_binary(message) do
{:error, message}
end
defp format_error(%{message: error_message}) do
format_error(error_message)
end
defp format_error(error) do
format_error(Exception.message(error))
end
end

@ -6,49 +6,19 @@ defmodule EthereumJSONRPC.Encoder do
alias ABI.TypeDecoder alias ABI.TypeDecoder
@doc """
Given an ABI and a set of functions, returns the data the blockchain expects.
"""
@spec encode_abi([map()], %{String.t() => [any()]}) :: map()
def encode_abi(abi, functions) do
abi
|> ABI.parse_specification()
|> get_selectors(functions)
|> Enum.map(&encode_function_call/1)
|> Map.new()
end
@doc """
Given a list of function selectors from the ABI lib, and a list of functions names with their arguments, returns a list of selectors with their functions.
"""
@spec get_selectors([%ABI.FunctionSelector{}], %{String.t() => [term()]}) :: [{%ABI.FunctionSelector{}, [term()]}]
def get_selectors(abi, functions) do
Enum.map(functions, fn {function_name, args} ->
{get_selector_from_name(abi, function_name), args}
end)
end
@doc """
Given a list of function selectors from the ABI lib, and a function name, get the selector for that function.
"""
@spec get_selector_from_name([%ABI.FunctionSelector{}], String.t()) :: %ABI.FunctionSelector{}
def get_selector_from_name(abi, function_name) do
Enum.find(abi, fn selector -> function_name == selector.function end)
end
@doc """ @doc """
Given a function selector and a list of arguments, returns their encoded versions. Given a function selector and a list of arguments, returns their encoded versions.
This is what is expected on the Json RPC data parameter. This is what is expected on the Json RPC data parameter.
""" """
@spec encode_function_call({%ABI.FunctionSelector{}, [term()]}) :: {String.t(), String.t()} @spec encode_function_call(%ABI.FunctionSelector{}, [term()]) :: String.t()
def encode_function_call({function_selector, args}) do def encode_function_call(function_selector, args) do
encoded_args = encoded_args =
function_selector function_selector
|> ABI.encode(parse_args(args)) |> ABI.encode(parse_args(args))
|> Base.encode16(case: :lower) |> Base.encode16(case: :lower)
{function_selector.function, "0x" <> encoded_args} "0x" <> encoded_args
end end
defp parse_args(args) do defp parse_args(args) do
@ -62,39 +32,16 @@ defmodule EthereumJSONRPC.Encoder do
end) end)
end end
@doc """
Given a result set from the blockchain, and the functions selectors, returns the results decoded.
This functions assumes the result["id"] is the name of the function the result is for.
"""
@spec decode_abi_results([map()], [map()], %{String.t() => [any()]}) :: map()
def decode_abi_results(results, abi, functions) do
selectors =
abi
|> ABI.parse_specification()
|> get_selectors(functions)
|> Enum.map(fn {selector, _args} -> selector end)
results
|> Stream.map(&join_result_and_selector(&1, selectors))
|> Stream.map(&decode_result/1)
|> Map.new()
end
defp join_result_and_selector(result, selectors) do
{result, Enum.find(selectors, &(&1.function == result[:id]))}
end
@doc """ @doc """
Given a result from the blockchain, and the function selector, returns the result decoded. Given a result from the blockchain, and the function selector, returns the result decoded.
""" """
@spec decode_result({map(), %ABI.FunctionSelector{}}) :: @spec decode_result(map(), %ABI.FunctionSelector{}) ::
{String.t(), {:ok, any()} | {:error, String.t() | :invalid_data}} {String.t(), {:ok, any()} | {:error, String.t() | :invalid_data}}
def decode_result({%{error: %{code: code, message: message}, id: id}, _selector}) do def decode_result(%{error: %{code: code, message: message}, id: id}, _selector) do
{id, {:error, "(#{code}) #{message}"}} {id, {:error, "(#{code}) #{message}"}}
end end
def decode_result({%{id: id, result: result}, function_selector}) do def decode_result(%{id: id, result: result}, function_selector) do
types_list = List.wrap(function_selector.returns) types_list = List.wrap(function_selector.returns)
decoded_data = decoded_data =

@ -0,0 +1,134 @@
defmodule EthereumJSONRPC.ContractTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Contract
import Mox
describe "execute_contract_functions/3" do
test "executes the functions with and without the block_number, returns results in order" do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
abi = [
%{
"constant" => false,
"inputs" => [],
"name" => "get1",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "nonpayable",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [],
"name" => "get2",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [],
"name" => "get3",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
contract_address = "0x0000000000000000000000000000000000000000"
functions = [
%{contract_address: contract_address, function_name: "get1", args: []},
%{contract_address: contract_address, function_name: "get2", args: [], block_number: 1000},
%{contract_address: contract_address, function_name: "get3", args: []}
]
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _options ->
{:ok,
requests
|> Enum.map(fn
%{id: id, method: "eth_call", params: [%{data: "0x054c1a75", to: ^contract_address}, "latest"]} ->
%{
id: id,
result: "0x000000000000000000000000000000000000000000000000000000000000002a"
}
%{id: id, method: "eth_call", params: [%{data: "0xd2178b08", to: ^contract_address}, "0x3E8"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000034"
}
%{id: id, method: "eth_call", params: [%{data: "0x8321045c", to: ^contract_address}, "latest"]} ->
%{
id: id,
error: %{code: -32015, data: "something", message: "Some error"}
}
end)
|> Enum.shuffle()}
end
)
blockchain_result = [
{:ok, [42]},
{:ok, [52]},
{:error, "(-32015) Some error"}
]
assert EthereumJSONRPC.execute_contract_functions(
functions,
abi,
json_rpc_named_arguments
) == blockchain_result
end
test "returns errors if JSONRPC request fails" do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
abi = [
%{
"constant" => false,
"inputs" => [],
"name" => "get",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "nonpayable",
"type" => "function"
}
]
contract_address = "0x0000000000000000000000000000000000000000"
functions = [
%{contract_address: contract_address, function_name: "get", args: []},
%{contract_address: contract_address, function_name: "get", args: [], block_number: 1000}
]
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _requests, _options ->
{:error, "Some error"}
end
)
blockchain_result = [
{:error, "Some error"},
{:error, "Some error"}
]
assert EthereumJSONRPC.execute_contract_functions(
functions,
abi,
json_rpc_named_arguments
) == blockchain_result
end
end
end

@ -13,7 +13,7 @@ defmodule EthereumJSONRPC.EncoderTest do
types: [] types: []
} }
assert Encoder.encode_function_call({function_selector, []}) == {"get", "0x6d4ce63c"} assert Encoder.encode_function_call(function_selector, []) == "0x6d4ce63c"
end end
test "generates the correct encoding with arguments" do test "generates the correct encoding with arguments" do
@ -23,8 +23,8 @@ defmodule EthereumJSONRPC.EncoderTest do
types: [{:uint, 256}] types: [{:uint, 256}]
} }
assert Encoder.encode_function_call({function_selector, [10]}) == assert Encoder.encode_function_call(function_selector, [10]) ==
{"get", "0x9507d39a000000000000000000000000000000000000000000000000000000000000000a"} "0x9507d39a000000000000000000000000000000000000000000000000000000000000000a"
end end
test "generates the correct encoding with addresses arguments" do test "generates the correct encoding with addresses arguments" do
@ -36,127 +36,12 @@ defmodule EthereumJSONRPC.EncoderTest do
args = ["0xdab1c67232f92b7707f49c08047b96a4db7a9fc6", "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"] args = ["0xdab1c67232f92b7707f49c08047b96a4db7a9fc6", "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"]
assert Encoder.encode_function_call({function_selector, args}) == assert Encoder.encode_function_call(function_selector, args) ==
{"tokens", "0x508493bc000000000000000000000000dab1c67232f92b7707f49c08047b96a4db7a9fc60000000000000000000000006937cb25eb54bc013b9c13c47ab38eb63edd1493"
"0x508493bc000000000000000000000000dab1c67232f92b7707f49c08047b96a4db7a9fc60000000000000000000000006937cb25eb54bc013b9c13c47ab38eb63edd1493"}
end end
end end
describe "get_selectors/2" do describe "decode_result/2" do
test "return the selectors of the desired functions with their arguments" do
abi = [
%ABI.FunctionSelector{
function: "fn1",
returns: {:uint, 256},
types: [uint: 256]
},
%ABI.FunctionSelector{
function: "fn2",
returns: {:uint, 256},
types: [uint: 256]
}
]
fn1 = %ABI.FunctionSelector{
function: "fn1",
returns: {:uint, 256},
types: [uint: 256]
}
assert Encoder.get_selectors(abi, %{"fn1" => [10]}) == [{fn1, [10]}]
end
end
describe "get_selector_from_name/2" do
test "return the selector of the desired function" do
abi = [
%ABI.FunctionSelector{
function: "fn1",
returns: {:uint, 256},
types: [uint: 256]
},
%ABI.FunctionSelector{
function: "fn2",
returns: {:uint, 256},
types: [uint: 256]
}
]
fn1 = %ABI.FunctionSelector{
function: "fn1",
returns: {:uint, 256},
types: [uint: 256]
}
assert Encoder.get_selector_from_name(abi, "fn1") == fn1
end
end
describe "decode_abi_results/3" do
test "separates the selectors and map the results" do
result = [
%{
id: "get1",
jsonrpc: "2.0",
result: "0x000000000000000000000000000000000000000000000000000000000000002a"
},
%{
id: "get2",
jsonrpc: "2.0",
result: "0x000000000000000000000000000000000000000000000000000000000000002a"
},
%{
id: "get3",
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000020"
}
]
abi = [
%{
"constant" => false,
"inputs" => [],
"name" => "get1",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "nonpayable",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [],
"name" => "get2",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [],
"name" => "get3",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
functions = %{
"get1" => [],
"get2" => [],
"get3" => []
}
assert Encoder.decode_abi_results(result, abi, functions) == %{
"get1" => {:ok, [42]},
"get2" => {:ok, [42]},
"get3" => {:ok, [32]}
}
end
end
describe "decode_result/1" do
test "correctly decodes the blockchain result" do test "correctly decodes the blockchain result" do
result = %{ result = %{
id: "sum", id: "sum",
@ -170,7 +55,7 @@ defmodule EthereumJSONRPC.EncoderTest do
types: [{:uint, 256}] types: [{:uint, 256}]
} }
assert Encoder.decode_result({result, selector}) == {"sum", {:ok, [42]}} assert Encoder.decode_result(result, selector) == {"sum", {:ok, [42]}}
end end
test "correctly handles the blockchain error response" do test "correctly handles the blockchain error response" do
@ -189,7 +74,7 @@ defmodule EthereumJSONRPC.EncoderTest do
types: [{:uint, 256}] types: [{:uint, 256}]
} }
assert Encoder.decode_result({result, selector}) == assert Encoder.decode_result(result, selector) ==
{"sum", {:error, "(-32602) Invalid params: Invalid hex: Invalid character 'x' at position 134."}} {"sum", {:error, "(-32602) Invalid params: Invalid hex: Invalid character 'x' at position 134."}}
end end
@ -199,7 +84,7 @@ defmodule EthereumJSONRPC.EncoderTest do
selector = %ABI.FunctionSelector{function: "name", types: [], returns: [:string]} selector = %ABI.FunctionSelector{function: "name", types: [], returns: [:string]}
assert Encoder.decode_result({%{id: "storedName", result: result}, selector}) == {"storedName", {:ok, ["AION"]}} assert Encoder.decode_result(%{id: "storedName", result: result}, selector) == {"storedName", {:ok, ["AION"]}}
end end
end end
end end

@ -886,93 +886,6 @@ defmodule EthereumJSONRPCTest do
end end
end end
describe "execute_contract_functions/3" do
test "executes the functions with the block_number" do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
functions = [
%{
contract_address: "0x0000000000000000000000000000000000000000",
data: "0x6d4ce63c",
id: "get"
}
]
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
]}
end
)
blockchain_result =
{:ok,
[
%{
id: "get",
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
]}
assert EthereumJSONRPC.execute_contract_functions(
functions,
json_rpc_named_arguments,
block_number: 1000
) == blockchain_result
end
test "executes the functions even when the block_number is not given" do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
functions = [
%{
contract_address: "0x0000000000000000000000000000000000000000",
data: "0x6d4ce63c",
id: "get"
}
]
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: _, params: [%{data: _, to: _}, "latest"]}], _options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
]}
end
)
blockchain_result =
{:ok,
[
%{
id: "get",
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
]}
assert EthereumJSONRPC.execute_contract_functions(
functions,
json_rpc_named_arguments
) == blockchain_result
end
end
defp clear_mailbox do defp clear_mailbox do
receive do receive do
_ -> clear_mailbox() _ -> clear_mailbox()

@ -6,9 +6,9 @@ defmodule Explorer.SmartContract.Reader do
[wiki](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI). [wiki](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI).
""" """
alias EthereumJSONRPC.Encoder alias EthereumJSONRPC.Contract
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.{Block, Hash} alias Explorer.Chain.Hash
@typedoc """ @typedoc """
Map of functions to call with the values for the function to be called with. Map of functions to call with the values for the function to be called with.
@ -18,24 +18,17 @@ defmodule Explorer.SmartContract.Reader do
@typedoc """ @typedoc """
Map of function call to function call results. Map of function call to function call results.
""" """
@type functions_results :: %{String.t() => {:ok, term()} | {:error, String.t()}} @type functions_results :: %{String.t() => Contract.call_result()}
@typedoc """ @typedoc """
Options that can be forwarded when calling the Ethereum JSON RPC. Options that can be forwarded when calling the Ethereum JSON RPC.
## Required
* `:json_rpc_named_arguments` - the named arguments to `EthereumJSONRPC.json_rpc/2`.
## Optional ## Optional
* `:block_number` - the block in which to execute the function. Defaults to the `nil` to indicate * `:json_rpc_named_arguments` - the named arguments to `EthereumJSONRPC.json_rpc/2`.
the latest block as determined by the remote node, which may differ from the latest block number
in `Explorer.Chain`.
""" """
@type contract_call_options :: [ @type contract_call_options :: [
{:json_rpc_named_arguments, EthereumJSONRPC.json_rpc_named_arguments()}, {:json_rpc_named_arguments, EthereumJSONRPC.json_rpc_named_arguments()}
{:block_number, Block.block_number()}
] ]
@doc """ @doc """
@ -90,55 +83,44 @@ defmodule Explorer.SmartContract.Reader do
@spec query_contract( @spec query_contract(
String.t(), String.t(),
term(), term(),
functions(), functions()
contract_call_options()
) :: functions_results() ) :: functions_results()
def query_contract(contract_address, abi, functions, opts \\ []) do def query_contract(contract_address, abi, functions) do
json_rpc_named_arguments = requests =
Keyword.get(opts, :json_rpc_named_arguments) || Application.get_env(:explorer, :json_rpc_named_arguments) functions
|> Enum.map(fn {function_name, args} ->
abi %{
|> Encoder.encode_abi(functions) contract_address: contract_address,
|> Enum.map(&setup_call_payload(&1, contract_address)) function_name: function_name,
|> EthereumJSONRPC.execute_contract_functions(json_rpc_named_arguments, opts) args: args
|> decode_results(abi, functions) }
rescue end)
error ->
format_error(functions, error)
end
defp decode_results({:ok, results}, abi, functions), do: Encoder.decode_abi_results(results, abi, functions)
defp decode_results({:error, {:bad_gateway, _request_url}}, _abi, functions) do
format_error(functions, "Bad Gateway")
end
defp format_error(functions, message) when is_binary(message) do requests
functions |> query_contracts(abi)
|> Enum.map(fn {function_name, _args} -> |> Enum.zip(requests)
%{function_name => {:error, message}} |> Enum.into(%{}, fn {response, request} ->
{request.function_name, response}
end) end)
|> List.first()
end end
defp format_error(functions, %{message: error_message}) do @doc """
format_error(functions, error_message) Runs batch of contract functions on given addresses for smart contract with an expected ABI and functions.
end
defp format_error(functions, error) do This function can be used to read data from smart contracts that are not verified (like token contracts)
format_error(functions, Exception.message(error)) since it receives the ABI as an argument.
end
@doc """ ## Options
Given the encoded data that references a function and its arguments in the blockchain, as well as the contract address, returns what EthereumJSONRPC.execute_contract_functions expects.
* `:json_rpc_named_arguments` - Options to forward for calling the Ethereum JSON RPC. See
`t:EthereumJSONRPC.json_rpc_named_arguments.t/0` for full list of options.
""" """
@spec setup_call_payload({%ABI.FunctionSelector{}, [term()]}, String.t()) :: map() @spec query_contracts([Contract.call()], term(), contract_call_options()) :: [Contract.call_result()]
def setup_call_payload({function_name, data}, contract_address) do def query_contracts(requests, abi, opts \\ []) do
%{ json_rpc_named_arguments =
contract_address: contract_address, Keyword.get(opts, :json_rpc_named_arguments) || Application.get_env(:explorer, :json_rpc_named_arguments)
data: data,
id: function_name EthereumJSONRPC.execute_contract_functions(requests, abi, json_rpc_named_arguments)
}
end end
@doc """ @doc """

@ -27,26 +27,34 @@ defmodule Explorer.Token.BalanceReader do
} }
] ]
@spec get_balance_of(String.t(), String.t(), non_neg_integer()) :: {atom(), non_neg_integer() | String.t()} @spec get_balances_of([
def get_balance_of(token_contract_address_hash, address_hash, block_number) do %{token_contract_address_hash: String.t(), address_hash: String.t(), block_number: non_neg_integer()}
result = ]) :: [{:ok, non_neg_integer()} | {:error, String.t()}]
Reader.query_contract( def get_balances_of(token_balance_requests) do
token_contract_address_hash, token_balance_requests
@balance_function_abi, |> Enum.map(&format_balance_request/1)
%{ |> Reader.query_contracts(@balance_function_abi)
"balanceOf" => [address_hash] |> Enum.map(&format_balance_result/1)
}, end
block_number: block_number
)
format_balance_result(result) defp format_balance_request(%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_contract_address_hash
}) do
%{
contract_address: token_contract_address_hash,
function_name: "balanceOf",
args: [address_hash],
block_number: block_number
}
end end
defp format_balance_result(%{"balanceOf" => {:ok, [balance]}}) do defp format_balance_result({:ok, [balance]}) do
{:ok, balance} {:ok, balance}
end end
defp format_balance_result(%{"balanceOf" => {:error, error_message}}) do defp format_balance_result({:error, error_message}) do
{:error, error_message} {:error, error_message}
end end
end end

@ -19,7 +19,7 @@ defmodule Explorer.Chain.Supply.TokenBridgeTest do
EthereumJSONRPC.Mox EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [ |> expect(:json_rpc, fn [
%{ %{
id: "mintedTotally", id: id,
method: "eth_call", method: "eth_call",
params: [ params: [
%{data: "0x553a5c85", to: "0x867305d19606aadba405ce534e303d0e225f9556"}, %{data: "0x553a5c85", to: "0x867305d19606aadba405ce534e303d0e225f9556"},
@ -31,7 +31,7 @@ defmodule Explorer.Chain.Supply.TokenBridgeTest do
{:ok, {:ok,
[ [
%{ %{
id: "mintedTotally", id: id,
jsonrpc: "2.0", jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000042aa8fe57ebb112dcc8" result: "0x00000000000000000000000000000000000000000000042aa8fe57ebb112dcc8"
} }
@ -39,7 +39,7 @@ defmodule Explorer.Chain.Supply.TokenBridgeTest do
end) end)
|> expect(:json_rpc, fn [ |> expect(:json_rpc, fn [
%{ %{
id: "totalBurntCoins", id: id,
jsonrpc: "2.0", jsonrpc: "2.0",
method: "eth_call", method: "eth_call",
params: [ params: [
@ -52,7 +52,7 @@ defmodule Explorer.Chain.Supply.TokenBridgeTest do
{:ok, {:ok,
[ [
%{ %{
id: "totalBurntCoins", id: id,
jsonrpc: "2.0", jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000033cc192839185166fc6" result: "0x00000000000000000000000000000000000000000000033cc192839185166fc6"
} }

@ -80,7 +80,7 @@ defmodule Explorer.SmartContract.ReaderTest do
response = Reader.query_contract(contract_address_hash, abi, %{"get" => []}) response = Reader.query_contract(contract_address_hash, abi, %{"get" => []})
assert %{"get" => {:error, "Bad Gateway"}} = response assert %{"get" => {:error, "Bad gateway"}} = response
end end
test "handles other types of errors" do test "handles other types of errors" do
@ -115,19 +115,6 @@ defmodule Explorer.SmartContract.ReaderTest do
end end
end end
describe "setup_call_payload/2" do
test "returns the expected payload" do
function_name = "get"
contract_address = "0x123789abc"
data = "0x6d4ce63c"
assert Reader.setup_call_payload(
{function_name, data},
contract_address
) == %{contract_address: "0x123789abc", data: "0x6d4ce63c", id: "get"}
end
end
describe "read_only_functions/1" do describe "read_only_functions/1" do
test "fetches the smart contract read only functions with the blockchain value" do test "fetches the smart contract read only functions with the blockchain value" do
smart_contract = smart_contract =

@ -27,9 +27,16 @@ defmodule Explorer.Token.BalanceReaderTest do
get_balance_from_blockchain() get_balance_from_blockchain()
result = BalanceReader.get_balance_of(token_contract_address_hash, address_hash, block_number) result =
BalanceReader.get_balances_of([
assert result == {:ok, 1_000_000_000_000_000_000_000_000} %{
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
block_number: block_number
}
])
assert result == [{:ok, 1_000_000_000_000_000_000_000_000}]
end end
test "returns the error message when there is one", %{address: address, token: token} do test "returns the error message when there is one", %{address: address, token: token} do
@ -39,9 +46,16 @@ defmodule Explorer.Token.BalanceReaderTest do
get_balance_from_blockchain_with_error() get_balance_from_blockchain_with_error()
result = BalanceReader.get_balance_of(token_contract_address_hash, address_hash, block_number) result =
BalanceReader.get_balances_of([
%{
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
block_number: block_number
}
])
assert result == {:error, "(-32015) VM execution error."} assert result == [{:error, "(-32015) VM execution error."}]
end end
end end
@ -49,11 +63,11 @@ defmodule Explorer.Token.BalanceReaderTest do
expect( expect(
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
fn [%{id: _, method: _, params: _}], _options -> fn [%{id: id, method: "eth_call", params: _}], _options ->
{:ok, {:ok,
[ [
%{ %{
id: "balanceOf", id: id,
jsonrpc: "2.0", jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
} }
@ -66,12 +80,12 @@ defmodule Explorer.Token.BalanceReaderTest do
expect( expect(
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
fn [%{id: _, method: _, params: _}], _options -> fn [%{id: id, method: "eth_call", params: _}], _options ->
{:ok, {:ok,
[ [
%{ %{
error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."},
id: "balanceOf", id: id,
jsonrpc: "2.0" jsonrpc: "2.0"
} }
]} ]}

@ -17,28 +17,35 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
id: "decimals", %{
result: "0x0000000000000000000000000000000000000000000000000000000000000012" id: id,
}, result: "0x0000000000000000000000000000000000000000000000000000000000000012"
%{ }
id: "name",
result: %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" %{
}, id: id,
%{ result:
id: "symbol", "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
result: }
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
}, %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{ %{
id: "totalSupply", id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" result:
} "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
]} }
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )
@ -59,29 +66,36 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
error: %{code: -32015, data: "something", message: "some error"}, %{
id: "decimals", id: id,
jsonrpc: "2.0" error: %{code: -32015, data: "something", message: "some error"},
}, jsonrpc: "2.0"
%{ }
id: "name",
result: %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" %{
}, id: id,
%{ result:
error: %{code: -32015, data: "something", message: "some error"}, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
id: "symbol", }
jsonrpc: "2.0"
}, %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{ %{
id: "totalSupply", id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" error: %{code: -32015, data: "something", message: "some error"},
} jsonrpc: "2.0"
]} }
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )
@ -89,20 +103,23 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
2, 2,
fn [%{id: "decimals"}, %{id: "symbol"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
error: %{code: -32015, data: "something", message: "some error"}, %{
id: "decimals", id: id,
jsonrpc: "2.0" error: %{code: -32015, data: "something", message: "some error"},
}, jsonrpc: "2.0"
%{ }
error: %{code: -32015, data: "something", message: "some error"},
id: "symbol", %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
jsonrpc: "2.0" %{
} id: id,
]} error: %{code: -32015, data: "something", message: "some error"},
jsonrpc: "2.0"
}
end)}
end end
) )
@ -122,28 +139,35 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
id: "decimals", %{
result: "0x0000000000000000000000000000000000000000000000000000000000000012" id: id,
}, result: "0x0000000000000000000000000000000000000000000000000000000000000012"
%{ }
id: "name",
result: %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000" %{
}, id: id,
%{ result:
id: "symbol", "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000"
result: }
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
}, %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{ %{
id: "totalSupply", id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" result:
} "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
]} }
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )
@ -158,45 +182,54 @@ defmodule Explorer.Token.MetadataRetrieverTest do
end end
test "considers the symbol nil when it is an invalid string" do test "considers the symbol nil when it is an invalid string" do
original = Application.get_env(:explorer, :token_functions_reader_max_retries)
Application.put_env(:explorer, :token_functions_reader_max_retries, 1)
token = insert(:token, contract_address: build(:contract_address)) token = insert(:token, contract_address: build(:contract_address))
expect( expect(
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
id: "decimals", %{
result: "0x0000000000000000000000000000000000000000000000000000000000000012" id: id,
}, result: "0x0000000000000000000000000000000000000000000000000000000000000012"
%{ }
id: "name",
result: %{id: _id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" nil
},
%{ %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
id: "symbol", %{
result: id: id,
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000" result:
}, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000"
%{ }
id: "totalSupply",
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
} %{
]} id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)
|> Enum.reject(&is_nil/1)}
end end
) )
expected = %{ expected = %{
name: "BNT",
decimals: 18, decimals: 18,
total_supply: 1_000_000_000_000_000_000, total_supply: 1_000_000_000_000_000_000,
symbol: nil symbol: nil
} }
assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected
Application.put_env(:explorer, :token_functions_reader_max_retries, original)
end end
test "shortens strings larger than 255 characters" do test "shortens strings larger than 255 characters" do
@ -209,20 +242,46 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
id: "name", %{
# this is how the token name would come from the blockchain unshortened. id: id,
result: result: "0x0000000000000000000000000000000000000000000000000000000000000012"
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010c3c627574746f6e20636c6173733d226e61766261722d746f67676c65722220747970653d22627574746f6e2220646174612d746f67676c653d22636f6c6c617073652220646174612d7461726765743d22236e6176626172537570706f72746564436f6e74656e742220617269612d636f6e74726f6c733d226e6176626172537570706f72746564436f6e74656e742220617269612d657870616e6465643d2266616c73652220617269612d6c6162656c3d223c253d20676574746578742822546f67676c65206e617669676174696f6e222920253e223e203c7370616e20636c6173733d226e61766261722d746f67676c65722d69636f6e223e3c2f7370616e3e203c2f627574746f6e3e0000000000000000000000000000000000000000" }
}
]} %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010c3c627574746f6e20636c6173733d226e61766261722d746f67676c65722220747970653d22627574746f6e2220646174612d746f67676c653d22636f6c6c617073652220646174612d7461726765743d22236e6176626172537570706f72746564436f6e74656e742220617269612d636f6e74726f6c733d226e6176626172537570706f72746564436f6e74656e742220617269612d657870616e6465643d2266616c73652220617269612d6c6162656c3d223c253d20676574746578742822546f67676c65206e617669676174696f6e222920253e223e203c7370616e20636c6173733d226e61766261722d746f67676c65722d69636f6e223e3c2f7370616e3e203c2f627574746f6e3e0000000000000000000000000000000000000000"
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: id,
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )
assert MetadataRetriever.get_functions_of(token.contract_address_hash) == %{name: long_token_name_shortened} expected = %{
name: long_token_name_shortened,
decimals: 18,
total_supply: 1_000_000_000_000_000_000,
symbol: "BNT"
}
assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected
end end
test "retries when some function gave error" do test "retries when some function gave error" do
@ -232,19 +291,35 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
error: %{code: -32015, data: "something", message: "some error"}, %{
id: "symbol", id: id,
jsonrpc: "2.0" result: "0x0000000000000000000000000000000000000000000000000000000000000012"
}, }
%{
id: "decimals", %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
result: "0x0000000000000000000000000000000000000000000000000000000000000012" %{
} id: id,
]} result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
error: %{code: -32015, data: "something", message: "some error"},
id: id,
jsonrpc: "2.0"
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )
@ -252,19 +327,27 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "symbol"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
id: "symbol", %{
result: id: id,
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" result:
} "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
]} }
end)}
end end
) )
assert MetadataRetriever.get_functions_of(token.contract_address_hash) == %{decimals: 18, symbol: "BNT"} expected = %{
name: "Bancor",
symbol: "BNT",
total_supply: 1_000_000_000_000_000_000,
decimals: 18
}
assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected
end end
test "retries according to the configured number" do test "retries according to the configured number" do
@ -278,29 +361,36 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
error: %{code: -32015, data: "something", message: "some error"}, %{
id: "decimals", error: %{code: -32015, data: "something", message: "some error"},
jsonrpc: "2.0" id: id,
}, jsonrpc: "2.0"
%{ }
id: "name",
result: %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" %{
}, id: id,
%{ result:
error: %{code: -32015, data: "something", message: "some error"}, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
id: "symbol", }
jsonrpc: "2.0"
}, %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{ %{
id: "totalSupply", error: %{code: -32015, data: "something", message: "some error"},
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" id: id,
} jsonrpc: "2.0"
]} }
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )
@ -308,20 +398,23 @@ defmodule Explorer.Token.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "symbol"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
error: %{code: -32015, data: "something", message: "some error"}, %{
id: "decimals", error: %{code: -32015, data: "something", message: "some error"},
jsonrpc: "2.0" id: id,
}, jsonrpc: "2.0"
%{ }
error: %{code: -32015, data: "something", message: "some error"},
id: "symbol", %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
jsonrpc: "2.0" %{
} error: %{code: -32015, data: "something", message: "some error"},
]} id: id,
jsonrpc: "2.0"
}
end)}
end end
) )

@ -32,22 +32,22 @@ defmodule Explorer.Validator.MetadataRetrieverTest do
end end
test "raise error when the first contract call fails" do test "raise error when the first contract call fails" do
contract_request_with_error("getValidators") contract_request_with_error()
assert_raise(MatchError, fn -> MetadataRetriever.fetch_data() end) assert_raise(MatchError, fn -> MetadataRetriever.fetch_data() end)
end end
test "raise error when a call to the metadatc contract fails" do test "raise error when a call to the metadatc contract fails" do
validators_list_mox_ok() validators_list_mox_ok()
contract_request_with_error("validators") contract_request_with_error()
assert_raise(MatchError, fn -> MetadataRetriever.fetch_data() end) assert_raise(MatchError, fn -> MetadataRetriever.fetch_data() end)
end end
end end
defp contract_request_with_error(id) do defp contract_request_with_error() do
expect( expect(
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
fn [%{id: ^id, method: _, params: _}], _options -> fn [%{id: id, method: _, params: _}], _options ->
{:ok, {:ok,
[ [
%{ %{
@ -65,11 +65,11 @@ defmodule Explorer.Validator.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "getValidators"}], _opts -> fn [%{id: id}], _opts ->
{:ok, {:ok,
[ [
%{ %{
id: "getValidators", id: id,
jsonrpc: "2.0", jsonrpc: "2.0",
result: result:
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"
@ -84,11 +84,11 @@ defmodule Explorer.Validator.MetadataRetrieverTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "validators"}], _opts -> fn [%{id: id}], _opts ->
{:ok, {:ok,
[ [
%{ %{
id: "validators", id: id,
jsonrpc: "2.0", jsonrpc: "2.0",
result: result:
"0x546573746e616d65000000000000000000000000000000000000000000000000556e69746172696f6e000000000000000000000000000000000000000000000030303030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140585800000000000000000000000000000000000000000000000000000000000030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003afe130e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058585858585858207374726565742058585858585800000000000000000000000000000000000000000000000000000000000000000000000000000000000000" "0x546573746e616d65000000000000000000000000000000000000000000000000556e69746172696f6e000000000000000000000000000000000000000000000030303030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140585800000000000000000000000000000000000000000000000000000000000030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003afe130e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058585858585858207374726565742058585858585800000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

@ -7,8 +7,7 @@ defmodule Indexer.TokenBalance.Fetcher do
only prepare the params, send they to `Indexer.TokenBalances` and relies on its return. only prepare the params, send they to `Indexer.TokenBalances` and relies on its return.
It behaves as a `BufferedTask`, so we can configure the `max_batch_size` and the `max_concurrency` to control how many It behaves as a `BufferedTask`, so we can configure the `max_batch_size` and the `max_concurrency` to control how many
token balances will be fetched at the same time. Be aware that, for each token balance the indexer will make a request token balances will be fetched at the same time.
to the Smart Contract.
Also, this module set a `retries_count` for each token balance and increment this number to avoid fetching the ones Also, this module set a `retries_count` for each token balance and increment this number to avoid fetching the ones
that always raise errors interacting with the Smart Contract. that always raise errors interacting with the Smart Contract.

@ -12,19 +12,11 @@ defmodule Indexer.TokenBalances do
alias Explorer.Token.BalanceReader alias Explorer.Token.BalanceReader
alias Indexer.{TokenBalance, Tracer} alias Indexer.{TokenBalance, Tracer}
# The timeout used for each process opened by Task.async_stream/3. Default 15s.
@task_timeout 15000
def fetch_token_balances_from_blockchain(token_balances) do
fetch_token_balances_from_blockchain(token_balances, [])
end
@doc """ @doc """
Fetches TokenBalances from specific Addresses and Blocks in the Blockchain Fetches TokenBalances from specific Addresses and Blocks in the Blockchain
Every `TokenBalance` is fetched asynchronously, but in case an exception is raised (such as a In case an exception is raised during the RPC call the particular TokenBalance request
timeout) during the RPC call the particular TokenBalance request is ignored and sent to is ignored and sent to `TokenBalance.Fetcher` to be fetched again.
`TokenBalance.Fetcher` to be fetched again.
## token_balances ## token_balances
@ -34,21 +26,17 @@ defmodule Indexer.TokenBalances do
* `address_hash` - The address_hash that we want to know the balance. * `address_hash` - The address_hash that we want to know the balance.
* `block_number` - The block number that the address_hash has the balance. * `block_number` - The block number that the address_hash has the balance.
""" """
def fetch_token_balances_from_blockchain([], _opts), do: {:ok, []} def fetch_token_balances_from_blockchain([]), do: {:ok, []}
@decorate span(tracer: Tracer) @decorate span(tracer: Tracer)
def fetch_token_balances_from_blockchain(token_balances, opts) do def fetch_token_balances_from_blockchain(token_balances) do
Logger.debug("fetching token balances", count: Enum.count(token_balances)) Logger.debug("fetching token balances", count: Enum.count(token_balances))
task_timeout = Keyword.get(opts, :timeout, @task_timeout)
task_callback = traced_fetch_token_balance_callback(Tracer.current_span())
requested_token_balances = requested_token_balances =
token_balances token_balances
|> Task.async_stream(task_callback, timeout: task_timeout, on_timeout: :kill_task) |> BalanceReader.get_balances_of()
|> Stream.map(&format_task_results/1) |> Stream.zip(token_balances)
|> Enum.filter(&ignore_killed_task/1) |> Enum.map(fn {result, token_balance} -> set_token_balance_value(result, token_balance) end)
fetched_token_balances = Enum.filter(requested_token_balances, &ignore_request_with_errors/1) fetched_token_balances = Enum.filter(requested_token_balances, &ignore_request_with_errors/1)
@ -70,35 +58,6 @@ defmodule Indexer.TokenBalances do
end) end)
end end
defp traced_fetch_token_balance_callback(%Spandex.Span{} = span) do
fn balance ->
try do
Tracer.continue_trace_from_span("traced_fetch_token_balance_callback/1", span)
fetch_token_balance(balance)
after
Tracer.finish_trace()
end
end
end
defp traced_fetch_token_balance_callback(_) do
&fetch_token_balance/1
end
@decorate span(tracer: Tracer)
defp fetch_token_balance(
%{
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
block_number: block_number
} = token_balance
) do
token_contract_address_hash
|> BalanceReader.get_balance_of(address_hash, block_number)
|> set_token_balance_value(token_balance)
end
defp set_token_balance_value({:ok, balance}, token_balance) do defp set_token_balance_value({:ok, balance}, token_balance) do
Map.merge(token_balance, %{value: balance, value_fetched_at: DateTime.utc_now(), error: nil}) Map.merge(token_balance, %{value: balance, value_fetched_at: DateTime.utc_now(), error: nil})
end end
@ -128,12 +87,6 @@ defmodule Indexer.TokenBalances do
|> TokenBalance.Fetcher.async_fetch() |> TokenBalance.Fetcher.async_fetch()
end end
defp format_task_results({:exit, :timeout}), do: {:error, :timeout}
defp format_task_results({:ok, token_balance}), do: token_balance
defp ignore_killed_task({:error, :timeout}), do: false
defp ignore_killed_task(_token_balance), do: true
defp ignore_request_with_errors(%{value: nil, value_fetched_at: nil, error: _error}), do: false defp ignore_request_with_errors(%{value: nil, value_fetched_at: nil, error: _error}), do: false
defp ignore_request_with_errors(_token_balance), do: true defp ignore_request_with_errors(_token_balance), do: true

@ -35,28 +35,35 @@ defmodule Indexer.Token.FetcherTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
id: "decimals", %{
result: "0x0000000000000000000000000000000000000000000000000000000000000012" id: id,
}, result: "0x0000000000000000000000000000000000000000000000000000000000000012"
%{ }
id: "name",
result: %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" %{
}, id: id,
%{ result:
id: "symbol", "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
result: }
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
}, %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{ %{
id: "totalSupply", id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" result:
} "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
]} }
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )

@ -17,32 +17,39 @@ defmodule Indexer.Token.MetadataUpdaterTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
id: "decimals", %{
result: "0x0000000000000000000000000000000000000000000000000000000000000012" id: id,
}, result: "0x0000000000000000000000000000000000000000000000000000000000000012"
%{ }
id: "name",
result: %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" %{
}, id: id,
%{ result:
id: "symbol", "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
result: }
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
}, %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{ %{
id: "totalSupply", id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" result:
} "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
]} }
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )
pid = start_supervised!({MetadataUpdater, %{update_interval: 0}}) pid = start_supervised!({MetadataUpdater, %{update_interval: 1}})
wait_for_results(fn -> wait_for_results(fn ->
updated = Repo.one!(from(t in Token, where: t.cataloged == true and not is_nil(t.name), limit: 1)) updated = Repo.one!(from(t in Token, where: t.cataloged == true and not is_nil(t.name), limit: 1))
@ -63,28 +70,35 @@ defmodule Indexer.Token.MetadataUpdaterTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts -> fn requests, _opts ->
{:ok, {:ok,
[ Enum.map(requests, fn
%{ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
id: "decimals", %{
result: "0x0000000000000000000000000000000000000000000000000000000000000012" id: id,
}, result: "0x0000000000000000000000000000000000000000000000000000000000000012"
%{ }
id: "name",
result: %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000" %{
}, id: id,
%{ result:
id: "symbol", "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
result: }
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
}, %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{ %{
id: "totalSupply", id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" result:
} "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
]} }
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
end)}
end end
) )

@ -45,11 +45,11 @@ defmodule Indexer.TokenBalance.FetcherTest do
expect( expect(
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options ->
{:ok, {:ok,
[ [
%{ %{
id: "balanceOf", id: id,
jsonrpc: "2.0", jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
} }
@ -80,12 +80,12 @@ defmodule Indexer.TokenBalance.FetcherTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
1, 1,
fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options ->
{:ok, {:ok,
[ [
%{ %{
error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."},
id: "balanceOf", id: id,
jsonrpc: "2.0" jsonrpc: "2.0"
} }
]} ]}

@ -62,34 +62,6 @@ defmodule Indexer.TokenBalancesTest do
assert TokenBalances.fetch_token_balances_from_blockchain(token_balances) == {:ok, []} assert TokenBalances.fetch_token_balances_from_blockchain(token_balances) == {:ok, []}
end end
test "ignores results that raised :timeout" do
address = insert(:address)
token = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
token_balance_params = [
%{
token_contract_address_hash: Hash.to_string(token.contract_address_hash),
address_hash: address_hash_string,
block_number: 1_000,
retries_count: 1
},
%{
token_contract_address_hash: Hash.to_string(token.contract_address_hash),
address_hash: address_hash_string,
block_number: 1_001,
retries_count: 1
}
]
get_balance_from_blockchain()
get_balance_from_blockchain_with_timeout(200)
{:ok, result} = TokenBalances.fetch_token_balances_from_blockchain(token_balance_params, timeout: 100)
assert length(result) == 1
end
end end
describe "log_fetching_errors" do describe "log_fetching_errors" do
@ -177,30 +149,11 @@ defmodule Indexer.TokenBalancesTest do
expect( expect(
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options ->
{:ok,
[
%{
id: "balanceOf",
jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
}
]}
end
)
end
defp get_balance_from_blockchain_with_timeout(timeout) do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options ->
:timer.sleep(timeout)
{:ok, {:ok,
[ [
%{ %{
id: "balanceOf", id: id,
jsonrpc: "2.0", jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
} }
@ -213,12 +166,12 @@ defmodule Indexer.TokenBalancesTest do
expect( expect(
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options -> fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options ->
{:ok, {:ok,
[ [
%{ %{
error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."},
id: "balanceOf", id: id,
jsonrpc: "2.0" jsonrpc: "2.0"
} }
]} ]}

Loading…
Cancel
Save