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.CaseTemplate':'__proxy__'/2
: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: 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.{
Block,
Blocks,
Contract,
FetchedBalances,
FetchedBeneficiaries,
FetchedCodes,
@ -160,33 +161,9 @@ defmodule EthereumJSONRPC do
}
]}
"""
@spec execute_contract_functions(
[%{contract_address: String.t(), data: String.t(), id: String.t()}],
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})
@spec execute_contract_functions([Contract.call()], [map()], json_rpc_named_arguments) :: [Contract.call_result()]
def execute_contract_functions(functions, abi, json_rpc_named_arguments) do
Contract.execute_contract_functions(functions, abi, json_rpc_named_arguments)
end
@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
@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 """
Given a function selector and a list of arguments, returns their encoded versions.
This is what is expected on the Json RPC data parameter.
"""
@spec encode_function_call({%ABI.FunctionSelector{}, [term()]}) :: {String.t(), String.t()}
def encode_function_call({function_selector, args}) do
@spec encode_function_call(%ABI.FunctionSelector{}, [term()]) :: String.t()
def encode_function_call(function_selector, args) do
encoded_args =
function_selector
|> ABI.encode(parse_args(args))
|> Base.encode16(case: :lower)
{function_selector.function, "0x" <> encoded_args}
"0x" <> encoded_args
end
defp parse_args(args) do
@ -62,39 +32,16 @@ defmodule EthereumJSONRPC.Encoder do
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 """
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}}
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}"}}
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)
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: []
}
assert Encoder.encode_function_call({function_selector, []}) == {"get", "0x6d4ce63c"}
assert Encoder.encode_function_call(function_selector, []) == "0x6d4ce63c"
end
test "generates the correct encoding with arguments" do
@ -23,8 +23,8 @@ defmodule EthereumJSONRPC.EncoderTest do
types: [{:uint, 256}]
}
assert Encoder.encode_function_call({function_selector, [10]}) ==
{"get", "0x9507d39a000000000000000000000000000000000000000000000000000000000000000a"}
assert Encoder.encode_function_call(function_selector, [10]) ==
"0x9507d39a000000000000000000000000000000000000000000000000000000000000000a"
end
test "generates the correct encoding with addresses arguments" do
@ -36,127 +36,12 @@ defmodule EthereumJSONRPC.EncoderTest do
args = ["0xdab1c67232f92b7707f49c08047b96a4db7a9fc6", "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"]
assert Encoder.encode_function_call({function_selector, args}) ==
{"tokens",
"0x508493bc000000000000000000000000dab1c67232f92b7707f49c08047b96a4db7a9fc60000000000000000000000006937cb25eb54bc013b9c13c47ab38eb63edd1493"}
assert Encoder.encode_function_call(function_selector, args) ==
"0x508493bc000000000000000000000000dab1c67232f92b7707f49c08047b96a4db7a9fc60000000000000000000000006937cb25eb54bc013b9c13c47ab38eb63edd1493"
end
end
describe "get_selectors/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
describe "decode_result/2" do
test "correctly decodes the blockchain result" do
result = %{
id: "sum",
@ -170,7 +55,7 @@ defmodule EthereumJSONRPC.EncoderTest do
types: [{:uint, 256}]
}
assert Encoder.decode_result({result, selector}) == {"sum", {:ok, [42]}}
assert Encoder.decode_result(result, selector) == {"sum", {:ok, [42]}}
end
test "correctly handles the blockchain error response" do
@ -189,7 +74,7 @@ defmodule EthereumJSONRPC.EncoderTest do
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."}}
end
@ -199,7 +84,7 @@ defmodule EthereumJSONRPC.EncoderTest do
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

@ -886,93 +886,6 @@ defmodule EthereumJSONRPCTest do
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
receive do
_ -> clear_mailbox()

@ -6,9 +6,9 @@ defmodule Explorer.SmartContract.Reader do
[wiki](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI).
"""
alias EthereumJSONRPC.Encoder
alias EthereumJSONRPC.Contract
alias Explorer.Chain
alias Explorer.Chain.{Block, Hash}
alias Explorer.Chain.Hash
@typedoc """
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 """
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 """
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
* `: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`.
* `:json_rpc_named_arguments` - the named arguments to `EthereumJSONRPC.json_rpc/2`.
"""
@type contract_call_options :: [
{:json_rpc_named_arguments, EthereumJSONRPC.json_rpc_named_arguments()},
{:block_number, Block.block_number()}
{:json_rpc_named_arguments, EthereumJSONRPC.json_rpc_named_arguments()}
]
@doc """
@ -90,55 +83,44 @@ defmodule Explorer.SmartContract.Reader do
@spec query_contract(
String.t(),
term(),
functions(),
contract_call_options()
functions()
) :: functions_results()
def query_contract(contract_address, abi, functions, opts \\ []) do
json_rpc_named_arguments =
Keyword.get(opts, :json_rpc_named_arguments) || Application.get_env(:explorer, :json_rpc_named_arguments)
abi
|> Encoder.encode_abi(functions)
|> Enum.map(&setup_call_payload(&1, contract_address))
|> EthereumJSONRPC.execute_contract_functions(json_rpc_named_arguments, opts)
|> decode_results(abi, functions)
rescue
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
def query_contract(contract_address, abi, functions) do
requests =
functions
|> Enum.map(fn {function_name, args} ->
%{
contract_address: contract_address,
function_name: function_name,
args: args
}
end)
defp format_error(functions, message) when is_binary(message) do
functions
|> Enum.map(fn {function_name, _args} ->
%{function_name => {:error, message}}
requests
|> query_contracts(abi)
|> Enum.zip(requests)
|> Enum.into(%{}, fn {response, request} ->
{request.function_name, response}
end)
|> List.first()
end
defp format_error(functions, %{message: error_message}) do
format_error(functions, error_message)
end
@doc """
Runs batch of contract functions on given addresses for smart contract with an expected ABI and functions.
defp format_error(functions, error) do
format_error(functions, Exception.message(error))
end
This function can be used to read data from smart contracts that are not verified (like token contracts)
since it receives the ABI as an argument.
@doc """
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.
## Options
* `: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()
def setup_call_payload({function_name, data}, contract_address) do
%{
contract_address: contract_address,
data: data,
id: function_name
}
@spec query_contracts([Contract.call()], term(), contract_call_options()) :: [Contract.call_result()]
def query_contracts(requests, abi, opts \\ []) do
json_rpc_named_arguments =
Keyword.get(opts, :json_rpc_named_arguments) || Application.get_env(:explorer, :json_rpc_named_arguments)
EthereumJSONRPC.execute_contract_functions(requests, abi, json_rpc_named_arguments)
end
@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()}
def get_balance_of(token_contract_address_hash, address_hash, block_number) do
result =
Reader.query_contract(
token_contract_address_hash,
@balance_function_abi,
%{
"balanceOf" => [address_hash]
},
block_number: block_number
)
@spec get_balances_of([
%{token_contract_address_hash: String.t(), address_hash: String.t(), block_number: non_neg_integer()}
]) :: [{:ok, non_neg_integer()} | {:error, String.t()}]
def get_balances_of(token_balance_requests) do
token_balance_requests
|> Enum.map(&format_balance_request/1)
|> Reader.query_contracts(@balance_function_abi)
|> Enum.map(&format_balance_result/1)
end
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
defp format_balance_result(%{"balanceOf" => {:ok, [balance]}}) do
defp format_balance_result({:ok, [balance]}) do
{:ok, balance}
end
defp format_balance_result(%{"balanceOf" => {:error, error_message}}) do
defp format_balance_result({:error, error_message}) do
{:error, error_message}
end
end

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

@ -80,7 +80,7 @@ defmodule Explorer.SmartContract.ReaderTest do
response = Reader.query_contract(contract_address_hash, abi, %{"get" => []})
assert %{"get" => {:error, "Bad Gateway"}} = response
assert %{"get" => {:error, "Bad gateway"}} = response
end
test "handles other types of errors" do
@ -115,19 +115,6 @@ defmodule Explorer.SmartContract.ReaderTest do
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
test "fetches the smart contract read only functions with the blockchain value" do
smart_contract =

@ -27,9 +27,16 @@ defmodule Explorer.Token.BalanceReaderTest do
get_balance_from_blockchain()
result = BalanceReader.get_balance_of(token_contract_address_hash, address_hash, block_number)
assert result == {:ok, 1_000_000_000_000_000_000_000_000}
result =
BalanceReader.get_balances_of([
%{
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
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()
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
@ -49,11 +63,11 @@ defmodule Explorer.Token.BalanceReaderTest do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: _, method: _, params: _}], _options ->
fn [%{id: id, method: "eth_call", params: _}], _options ->
{:ok,
[
%{
id: "balanceOf",
id: id,
jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
}
@ -66,12 +80,12 @@ defmodule Explorer.Token.BalanceReaderTest do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: _, method: _, params: _}], _options ->
fn [%{id: id, method: "eth_call", params: _}], _options ->
{:ok,
[
%{
error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."},
id: "balanceOf",
id: id,
jsonrpc: "2.0"
}
]}

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

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

@ -7,8 +7,7 @@ defmodule Indexer.TokenBalance.Fetcher do
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
token balances will be fetched at the same time. Be aware that, for each token balance the indexer will make a request
to the Smart Contract.
token balances will be fetched at the same time.
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.

@ -12,19 +12,11 @@ defmodule Indexer.TokenBalances do
alias Explorer.Token.BalanceReader
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 """
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
timeout) during the RPC call the particular TokenBalance request is ignored and sent to
`TokenBalance.Fetcher` to be fetched again.
In case an exception is raised during the RPC call the particular TokenBalance request
is ignored and sent to `TokenBalance.Fetcher` to be fetched again.
## token_balances
@ -34,21 +26,17 @@ defmodule Indexer.TokenBalances do
* `address_hash` - The address_hash that we want to know 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)
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))
task_timeout = Keyword.get(opts, :timeout, @task_timeout)
task_callback = traced_fetch_token_balance_callback(Tracer.current_span())
requested_token_balances =
token_balances
|> Task.async_stream(task_callback, timeout: task_timeout, on_timeout: :kill_task)
|> Stream.map(&format_task_results/1)
|> Enum.filter(&ignore_killed_task/1)
|> BalanceReader.get_balances_of()
|> Stream.zip(token_balances)
|> 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)
@ -70,35 +58,6 @@ defmodule Indexer.TokenBalances do
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
Map.merge(token_balance, %{value: balance, value_fetched_at: DateTime.utc_now(), error: nil})
end
@ -128,12 +87,6 @@ defmodule Indexer.TokenBalances do
|> TokenBalance.Fetcher.async_fetch()
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(_token_balance), do: true

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

@ -17,32 +17,39 @@ defmodule Indexer.Token.MetadataUpdaterTest do
EthereumJSONRPC.Mox,
:json_rpc,
1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts ->
fn requests, _opts ->
{:ok,
[
%{
id: "decimals",
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
},
%{
id: "name",
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
},
%{
id: "symbol",
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
},
%{
id: "totalSupply",
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
]}
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
}
%{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
)
pid = start_supervised!({MetadataUpdater, %{update_interval: 0}})
pid = start_supervised!({MetadataUpdater, %{update_interval: 1}})
wait_for_results(fn ->
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,
:json_rpc,
1,
fn [%{id: "decimals"}, %{id: "name"}, %{id: "symbol"}, %{id: "totalSupply"}], _opts ->
fn requests, _opts ->
{:ok,
[
%{
id: "decimals",
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
},
%{
id: "name",
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
},
%{
id: "symbol",
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
},
%{
id: "totalSupply",
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
]}
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
}
%{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
)

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

@ -62,34 +62,6 @@ defmodule Indexer.TokenBalancesTest do
assert TokenBalances.fetch_token_balances_from_blockchain(token_balances) == {:ok, []}
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
describe "log_fetching_errors" do
@ -177,30 +149,11 @@ defmodule Indexer.TokenBalancesTest 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_timeout(timeout) do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options ->
:timer.sleep(timeout)
fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options ->
{:ok,
[
%{
id: "balanceOf",
id: id,
jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
}
@ -213,12 +166,12 @@ defmodule Indexer.TokenBalancesTest do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: _, method: _, params: [%{data: _, to: _}, _]}], _options ->
fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options ->
{:ok,
[
%{
error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."},
id: "balanceOf",
id: id,
jsonrpc: "2.0"
}
]}

Loading…
Cancel
Save