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. 34
      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. 30
      apps/explorer/test/explorer/token/balance_reader_test.exs
  13. 261
      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. 27
      apps/indexer/test/indexer/token/fetcher_test.exs
  18. 56
      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
defp format_error(functions, message) when is_binary(message) do
def query_contract(contract_address, abi, functions) do
requests =
functions
|> Enum.map(fn {function_name, _args} ->
%{function_name => {:error, message}}
|> Enum.map(fn {function_name, args} ->
%{
contract_address: contract_address,
function_name: function_name,
args: args
}
end)
|> List.first()
end
defp format_error(functions, %{message: error_message}) do
format_error(functions, error_message)
end
defp format_error(functions, error) do
format_error(functions, Exception.message(error))
requests
|> query_contracts(abi)
|> Enum.zip(requests)
|> Enum.into(%{}, fn {response, request} ->
{request.function_name, response}
end)
end
@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.
Runs batch of contract functions on given addresses for smart contract with an expected ABI and functions.
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.
## 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,
@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
defp format_balance_request(%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_contract_address_hash
}) do
%{
"balanceOf" => [address_hash]
},
contract_address: token_contract_address_hash,
function_name: "balanceOf",
args: [address_hash],
block_number: block_number
)
format_balance_result(result)
}
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)
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}
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: "decimals",
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: "name",
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: "symbol",
id: id,
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "totalSupply",
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: id,
error: %{code: -32015, data: "something", message: "some error"},
id: "decimals",
jsonrpc: "2.0"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: "name",
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: id,
error: %{code: -32015, data: "something", message: "some error"},
id: "symbol",
jsonrpc: "2.0"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "totalSupply",
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: id,
error: %{code: -32015, data: "something", message: "some error"},
id: "decimals",
jsonrpc: "2.0"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: id,
error: %{code: -32015, data: "something", message: "some error"},
id: "symbol",
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: "decimals",
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: "name",
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: "symbol",
id: id,
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "totalSupply",
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: "decimals",
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
},
%{
id: "name",
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
},
}
%{id: _id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
nil
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: "symbol",
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001aa796568616e7a652067676761202075797575206e6e6e6e6e200000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "totalSupply",
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,
[
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: "name",
# this is how the token name would come from the blockchain unshortened.
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,
[
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: "symbol",
id: id,
jsonrpc: "2.0"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "decimals",
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: "symbol",
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,
[
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"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: "name",
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
error: %{code: -32015, data: "something", message: "some error"},
id: "symbol",
id: id,
jsonrpc: "2.0"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "totalSupply",
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,
[
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"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
error: %{code: -32015, data: "something", message: "some error"},
id: "symbol",
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: "decimals",
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: "name",
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: "symbol",
id: id,
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "totalSupply",
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: "decimals",
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: "name",
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: "symbol",
id: id,
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "totalSupply",
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,
[
Enum.map(requests, fn
%{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
%{
id: "decimals",
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000012"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
%{
id: "name",
id: id,
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000642616e636f720000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
%{
id: "symbol",
id: id,
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
},
}
%{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
%{
id: "totalSupply",
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