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