Merge pull request #1560 from poanetwork/gs-batch-token-balance-requests
Allow executing smart contract functions in arbitrarily sized batchespull/1562/head
commit
5d5b8fa84d
@ -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() |
@ -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 |
@ -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 |
Loading…
Reference in new issue