Skip to content

Commit

Permalink
Merge pull request blockscout#1560 from poanetwork/gs-batch-token-bal…
Browse files Browse the repository at this point in the history
…ance-requests

Allow executing smart contract functions in arbitrarily sized batches
  • Loading branch information
vbaranov authored Mar 13, 2019
2 parents 76d4cc6 + 3060264 commit 5d5b8fa
Show file tree
Hide file tree
Showing 20 changed files with 700 additions and 740 deletions.
2 changes: 1 addition & 1 deletion .dialyzer-ignore
Original file line number Diff line number Diff line change
@@ -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()
31 changes: 4 additions & 27 deletions apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ defmodule EthereumJSONRPC do
alias EthereumJSONRPC.{
Block,
Blocks,
Contract,
FetchedBalances,
FetchedBeneficiaries,
FetchedCodes,
Expand Down Expand Up @@ -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 """
Expand Down
94 changes: 94 additions & 0 deletions apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex
Original file line number Diff line number Diff line change
@@ -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
65 changes: 6 additions & 59 deletions apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down
134 changes: 134 additions & 0 deletions apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 5d5b8fa

Please sign in to comment.