Merge pull request #279 from poanetwork/ln-read-smart-contract-back-end
(Feature) Smart contract function readingpull/426/head
commit
6620dca137
@ -0,0 +1,110 @@ |
|||||||
|
defmodule EthereumJSONRPC.Encoder do |
||||||
|
@moduledoc """ |
||||||
|
Deals with encoding and decoding data to be sent to, or that is |
||||||
|
received from, the blockchain. |
||||||
|
""" |
||||||
|
|
||||||
|
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 econded 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 |
||||||
|
encoded_args = |
||||||
|
function_selector |
||||||
|
|> ABI.encode(parse_args(args)) |
||||||
|
|> Base.encode16(case: :lower) |
||||||
|
|
||||||
|
{function_selector.function, "0x" <> encoded_args} |
||||||
|
end |
||||||
|
|
||||||
|
defp parse_args(args) do |
||||||
|
args |
||||||
|
|> Enum.map(fn |
||||||
|
<<"0x", hexadecimal_digits::binary>> -> |
||||||
|
Base.decode16!(hexadecimal_digits, case: :mixed) |
||||||
|
|
||||||
|
item -> |
||||||
|
item |
||||||
|
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({any(), [map()]}, [map()], %{String.t() => [any()]}) :: map() |
||||||
|
def decode_abi_results({:ok, 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{}}) :: {String.t(), [String.t()]} |
||||||
|
def decode_result({%{error: %{code: code, message: message}, id: id}, _selector}) do |
||||||
|
{id, ["#{code} => #{message}"]} |
||||||
|
end |
||||||
|
|
||||||
|
def decode_result({%{id: id, result: result}, function_selector}) do |
||||||
|
types_list = format_list_types(function_selector.returns) |
||||||
|
|
||||||
|
decoded_result = |
||||||
|
result |
||||||
|
|> String.slice(2..-1) |
||||||
|
|> Base.decode16!(case: :lower) |
||||||
|
|> TypeDecoder.decode_raw(types_list) |
||||||
|
|
||||||
|
{id, decoded_result} |
||||||
|
end |
||||||
|
|
||||||
|
defp format_list_types(:string), do: [{:array, :string, 1}] |
||||||
|
defp format_list_types(return_types), do: List.wrap(return_types) |
||||||
|
end |
@ -0,0 +1,207 @@ |
|||||||
|
defmodule EthereumJSONRPC.EncoderTest do |
||||||
|
use ExUnit.Case, async: true |
||||||
|
|
||||||
|
doctest EthereumJSONRPC.Encoder |
||||||
|
|
||||||
|
alias EthereumJSONRPC.Encoder |
||||||
|
|
||||||
|
describe "encode_function_call/2" do |
||||||
|
test "generates the correct encoding with no arguments" do |
||||||
|
function_selector = %ABI.FunctionSelector{ |
||||||
|
function: "get", |
||||||
|
returns: {:uint, 256}, |
||||||
|
types: [] |
||||||
|
} |
||||||
|
|
||||||
|
assert Encoder.encode_function_call({function_selector, []}) == {"get", "0x6d4ce63c"} |
||||||
|
end |
||||||
|
|
||||||
|
test "generates the correct encoding with arguments" do |
||||||
|
function_selector = %ABI.FunctionSelector{ |
||||||
|
function: "get", |
||||||
|
returns: {:uint, 256}, |
||||||
|
types: [{:uint, 256}] |
||||||
|
} |
||||||
|
|
||||||
|
assert Encoder.encode_function_call({function_selector, [10]}) == |
||||||
|
{"get", "0x9507d39a000000000000000000000000000000000000000000000000000000000000000a"} |
||||||
|
end |
||||||
|
|
||||||
|
test "generates the correct encoding with addresses arguments" do |
||||||
|
function_selector = %ABI.FunctionSelector{ |
||||||
|
function: "tokens", |
||||||
|
returns: {:uint, 256}, |
||||||
|
types: [:address, :address] |
||||||
|
} |
||||||
|
|
||||||
|
args = ["0xdab1c67232f92b7707f49c08047b96a4db7a9fc6", "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"] |
||||||
|
|
||||||
|
assert Encoder.encode_function_call({function_selector, args}) == |
||||||
|
{"tokens", |
||||||
|
"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 = |
||||||
|
{:ok, |
||||||
|
[ |
||||||
|
%{ |
||||||
|
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" => [42], |
||||||
|
"get2" => [42], |
||||||
|
"get3" => [32] |
||||||
|
} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "decode_result/1" do |
||||||
|
test "correclty decodes the blockchain result" do |
||||||
|
result = %{ |
||||||
|
id: "sum", |
||||||
|
jsonrpc: "2.0", |
||||||
|
result: "0x000000000000000000000000000000000000000000000000000000000000002a" |
||||||
|
} |
||||||
|
|
||||||
|
selector = %ABI.FunctionSelector{ |
||||||
|
function: "get", |
||||||
|
returns: {:uint, 256}, |
||||||
|
types: [{:uint, 256}] |
||||||
|
} |
||||||
|
|
||||||
|
assert Encoder.decode_result({result, selector}) == {"sum", [42]} |
||||||
|
end |
||||||
|
|
||||||
|
test "correclty handles the blockchain error response" do |
||||||
|
result = %{ |
||||||
|
error: %{ |
||||||
|
code: -32602, |
||||||
|
message: "Invalid params: Invalid hex: Invalid character 'x' at position 134." |
||||||
|
}, |
||||||
|
id: "sum", |
||||||
|
jsonrpc: "2.0" |
||||||
|
} |
||||||
|
|
||||||
|
selector = %ABI.FunctionSelector{ |
||||||
|
function: "get", |
||||||
|
returns: {:uint, 256}, |
||||||
|
types: [{:uint, 256}] |
||||||
|
} |
||||||
|
|
||||||
|
assert Encoder.decode_result({result, selector}) == |
||||||
|
{"sum", ["-32602 => Invalid params: Invalid hex: Invalid character 'x' at position 134."]} |
||||||
|
end |
||||||
|
|
||||||
|
test "correclty decodes string types" do |
||||||
|
result = |
||||||
|
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000441494f4e00000000000000000000000000000000000000000000000000000000" |
||||||
|
|
||||||
|
selector = %ABI.FunctionSelector{function: "name", types: [], returns: :string} |
||||||
|
|
||||||
|
assert Encoder.decode_result({%{id: "storedName", result: result}, selector}) == {"storedName", [["AION"]]} |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,12 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.HTTP, |
||||||
|
transport_options: [ |
||||||
|
http: EthereumJSONRPC.HTTP.HTTPoison, |
||||||
|
url: "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY", |
||||||
|
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Geth |
||||||
|
] |
@ -0,0 +1,16 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.HTTP, |
||||||
|
transport_options: [ |
||||||
|
http: EthereumJSONRPC.HTTP.HTTPoison, |
||||||
|
url: "https://sokol.poa.network", |
||||||
|
method_to_url: [ |
||||||
|
eth_getBalance: "https://sokol-trace.poa.network", |
||||||
|
trace_replayTransaction: "https://sokol-trace.poa.network" |
||||||
|
], |
||||||
|
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Parity |
||||||
|
] |
@ -0,0 +1,12 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.HTTP, |
||||||
|
transport_options: [ |
||||||
|
http: EthereumJSONRPC.HTTP.HTTPoison, |
||||||
|
url: "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY", |
||||||
|
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Geth |
||||||
|
] |
@ -0,0 +1,16 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.HTTP, |
||||||
|
transport_options: [ |
||||||
|
http: EthereumJSONRPC.HTTP.HTTPoison, |
||||||
|
url: "https://sokol.poa.network", |
||||||
|
method_to_url: [ |
||||||
|
eth_getBalance: "https://sokol-trace.poa.network", |
||||||
|
trace_replayTransaction: "https://sokol-trace.poa.network" |
||||||
|
], |
||||||
|
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Parity |
||||||
|
] |
@ -0,0 +1,8 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.Mox, |
||||||
|
transport_options: [], |
||||||
|
variant: EthereumJSONRPC.Geth |
||||||
|
] |
@ -0,0 +1,9 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
transport: EthereumJSONRPC.HTTP, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.Mox, |
||||||
|
transport_options: [], |
||||||
|
variant: EthereumJSONRPC.Parity |
||||||
|
] |
@ -0,0 +1,220 @@ |
|||||||
|
defmodule Explorer.SmartContract.Reader do |
||||||
|
@moduledoc """ |
||||||
|
Reads Smart Contract functions from the blockchain. |
||||||
|
|
||||||
|
For information on smart contract's Application Binary Interface (ABI), visit the |
||||||
|
[wiki](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI). |
||||||
|
""" |
||||||
|
|
||||||
|
alias Explorer.Chain |
||||||
|
alias EthereumJSONRPC.Encoder |
||||||
|
alias Explorer.Chain.Hash |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Queries the contract functions on the blockchain and returns the call results. |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
Note that for this example to work the database must be up to date with the |
||||||
|
information available in the blockchain. |
||||||
|
|
||||||
|
``` |
||||||
|
$ Explorer.SmartContract.Reader.query_contract( |
||||||
|
%Explorer.Chain.Hash{ |
||||||
|
byte_count: 20, |
||||||
|
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> |
||||||
|
}, |
||||||
|
%{"sum" => [20, 22]} |
||||||
|
) |
||||||
|
# => %{"sum" => [42]} |
||||||
|
|
||||||
|
$ Explorer.SmartContract.Reader.query_contract( |
||||||
|
%Explorer.Chain.Hash{ |
||||||
|
byte_count: 20, |
||||||
|
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> |
||||||
|
}, |
||||||
|
%{"sum" => [1, "abc"]} |
||||||
|
) |
||||||
|
# => %{"sum" => ["Data overflow encoding int, data `abc` cannot fit in 256 bits"]} |
||||||
|
``` |
||||||
|
""" |
||||||
|
@spec query_contract(%Explorer.Chain.Hash{}, %{String.t() => [term()]}) :: map() |
||||||
|
def query_contract(address_hash, functions) do |
||||||
|
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) |
||||||
|
|
||||||
|
contract_address = Hash.to_string(address_hash) |
||||||
|
|
||||||
|
abi = |
||||||
|
address_hash |
||||||
|
|> Chain.address_hash_to_smart_contract() |
||||||
|
|> Map.get(:abi) |
||||||
|
|
||||||
|
try do |
||||||
|
blockchain_result = |
||||||
|
abi |
||||||
|
|> Encoder.encode_abi(functions) |
||||||
|
|> Enum.map(&setup_call_payload(&1, contract_address)) |
||||||
|
|> EthereumJSONRPC.execute_contract_functions(json_rpc_named_arguments) |
||||||
|
|
||||||
|
Encoder.decode_abi_results(blockchain_result, abi, functions) |
||||||
|
rescue |
||||||
|
error -> |
||||||
|
format_error(functions, error.message) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp format_error(functions, message) do |
||||||
|
functions |
||||||
|
|> Enum.map(fn {function_name, _args} -> |
||||||
|
%{function_name => [message]} |
||||||
|
end) |
||||||
|
|> List.first() |
||||||
|
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. |
||||||
|
""" |
||||||
|
@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 |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
List all the smart contract functions with its current value from the |
||||||
|
blockchain, following the ABI order. |
||||||
|
|
||||||
|
Functions that require arguments can be queryable but won't list the current |
||||||
|
value at this moment. |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
$ Explorer.SmartContract.Reader.read_only_functions("0x798465571ae21a184a272f044f991ad1d5f87a3f") |
||||||
|
=> [ |
||||||
|
%{ |
||||||
|
"constant" => true, |
||||||
|
"inputs" => [], |
||||||
|
"name" => "get", |
||||||
|
"outputs" => [%{"name" => "", "type" => "uint256", "value" => 0}], |
||||||
|
"payable" => false, |
||||||
|
"stateMutability" => "view", |
||||||
|
"type" => "function" |
||||||
|
}, |
||||||
|
%{ |
||||||
|
"constant" => true, |
||||||
|
"inputs" => [%{"name" => "x", "type" => "uint256"}], |
||||||
|
"name" => "with_arguments", |
||||||
|
"outputs" => [%{"name" => "", "type" => "bool", "value" => ""}], |
||||||
|
"payable" => false, |
||||||
|
"stateMutability" => "view", |
||||||
|
"type" => "function" |
||||||
|
} |
||||||
|
] |
||||||
|
""" |
||||||
|
@spec read_only_functions(%Explorer.Chain.Hash{}) :: [%{}] |
||||||
|
def read_only_functions(contract_address_hash) do |
||||||
|
contract_address_hash |
||||||
|
|> Chain.address_hash_to_smart_contract() |
||||||
|
|> Map.get(:abi, []) |
||||||
|
|> Enum.filter(& &1["constant"]) |
||||||
|
|> fetch_current_value_from_blockchain(contract_address_hash, []) |
||||||
|
|> Enum.reverse() |
||||||
|
end |
||||||
|
|
||||||
|
def fetch_current_value_from_blockchain([%{"inputs" => []} = function | tail], contract_address_hash, acc) do |
||||||
|
values = |
||||||
|
fetch_from_blockchain(contract_address_hash, %{ |
||||||
|
name: function["name"], |
||||||
|
args: function["inputs"], |
||||||
|
outputs: function["outputs"] |
||||||
|
}) |
||||||
|
|
||||||
|
formatted = Map.replace!(function, "outputs", values) |
||||||
|
|
||||||
|
fetch_current_value_from_blockchain(tail, contract_address_hash, [formatted | acc]) |
||||||
|
end |
||||||
|
|
||||||
|
def fetch_current_value_from_blockchain([function | tail], contract_address_hash, acc) do |
||||||
|
values = link_outputs_and_values(%{}, Map.get(function, "outputs", []), function["name"]) |
||||||
|
|
||||||
|
formatted = Map.replace!(function, "outputs", values) |
||||||
|
|
||||||
|
fetch_current_value_from_blockchain(tail, contract_address_hash, [formatted | acc]) |
||||||
|
end |
||||||
|
|
||||||
|
def fetch_current_value_from_blockchain([], _contract_address_hash, acc), do: acc |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Fetches the blockchain value of a function that requires arguments. |
||||||
|
""" |
||||||
|
@spec query_function(String.t(), %{name: String.t(), args: nil}) :: [%{}] |
||||||
|
def query_function(contract_address_hash, %{name: name, args: nil}) do |
||||||
|
query_function(contract_address_hash, %{name: name, args: []}) |
||||||
|
end |
||||||
|
|
||||||
|
@spec query_function(%Explorer.Chain.Hash{}, %{name: String.t(), args: [term()]}) :: [%{}] |
||||||
|
def query_function(contract_address_hash, %{name: name, args: args}) do |
||||||
|
function = |
||||||
|
contract_address_hash |
||||||
|
|> Chain.address_hash_to_smart_contract() |
||||||
|
|> Map.get(:abi, []) |
||||||
|
|> Enum.filter(fn function -> function["name"] == name end) |
||||||
|
|> List.first() |
||||||
|
|
||||||
|
fetch_from_blockchain(contract_address_hash, %{name: name, args: args, outputs: function["outputs"]}) |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_from_blockchain(contract_address_hash, %{name: name, args: args, outputs: outputs}) do |
||||||
|
contract_address_hash |
||||||
|
|> query_contract(%{name => normalize_args(args)}) |
||||||
|
|> link_outputs_and_values(outputs, name) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
The type of the arguments passed to the blockchain interferes in the output, |
||||||
|
but we always get strings from the front, so it is necessary to normalize it. |
||||||
|
""" |
||||||
|
def normalize_args(args) do |
||||||
|
Enum.map(args, &parse_item/1) |
||||||
|
end |
||||||
|
|
||||||
|
defp parse_item("true"), do: true |
||||||
|
defp parse_item("false"), do: false |
||||||
|
|
||||||
|
defp parse_item(item) do |
||||||
|
response = Integer.parse(item) |
||||||
|
|
||||||
|
case response do |
||||||
|
{integer, remainder_of_binary} when remainder_of_binary == "" -> integer |
||||||
|
_ -> item |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def link_outputs_and_values(blockchain_values, outputs, function_name) do |
||||||
|
values = Map.get(blockchain_values, function_name, [""]) |
||||||
|
|
||||||
|
for output <- outputs, value <- values do |
||||||
|
new_value(output, value) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp new_value(%{"type" => "address"} = output, value) do |
||||||
|
Map.put_new(output, "value", bytes_to_string(value)) |
||||||
|
end |
||||||
|
|
||||||
|
defp new_value(%{"type" => "bytes" <> _number} = output, value) do |
||||||
|
Map.put_new(output, "value", bytes_to_string(value)) |
||||||
|
end |
||||||
|
|
||||||
|
defp new_value(output, value) do |
||||||
|
Map.put_new(output, "value", value) |
||||||
|
end |
||||||
|
|
||||||
|
@spec bytes_to_string(<<_::_*8>>) :: String.t() |
||||||
|
defp bytes_to_string(value) do |
||||||
|
Hash.to_string(%Hash{byte_count: byte_size(value), bytes: value}) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,199 @@ |
|||||||
|
defmodule Explorer.SmartContract.ReaderTest do |
||||||
|
use EthereumJSONRPC.Case |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
doctest Explorer.SmartContract.Reader |
||||||
|
|
||||||
|
alias Explorer.SmartContract.Reader |
||||||
|
|
||||||
|
import Mox |
||||||
|
|
||||||
|
setup :verify_on_exit! |
||||||
|
|
||||||
|
describe "query_contract/2" do |
||||||
|
test "correctly returns the results of the smart contract functions" do |
||||||
|
hash = |
||||||
|
:smart_contract |
||||||
|
|> insert() |
||||||
|
|> Map.get(:address_hash) |
||||||
|
|
||||||
|
blockchain_get_function_mock() |
||||||
|
|
||||||
|
assert Reader.query_contract(hash, %{"get" => []}) == %{"get" => [0]} |
||||||
|
end |
||||||
|
|
||||||
|
test "won't raise error when there is a problem with the params to consult the blockchain" do |
||||||
|
smart_contract = |
||||||
|
insert( |
||||||
|
:smart_contract, |
||||||
|
abi: [ |
||||||
|
%{ |
||||||
|
"constant" => true, |
||||||
|
"inputs" => [ |
||||||
|
%{"name" => "a", "type" => "int256"}, |
||||||
|
%{"name" => "b", "type" => "int256"}, |
||||||
|
%{"name" => "c", "type" => "int256"}, |
||||||
|
%{"name" => "d", "type" => "int256"} |
||||||
|
], |
||||||
|
"name" => "sum", |
||||||
|
"outputs" => [%{"name" => "", "type" => "int256"}], |
||||||
|
"payable" => false, |
||||||
|
"stateMutability" => "pure", |
||||||
|
"type" => "function" |
||||||
|
} |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
wrong_args = %{"sum" => [1, 1, 1, "abc"]} |
||||||
|
|
||||||
|
assert %{"sum" => ["Data overflow encoding int, data `abc` cannot fit in 256 bits"]} = |
||||||
|
Reader.query_contract(smart_contract.address_hash, wrong_args) |
||||||
|
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 = |
||||||
|
insert( |
||||||
|
:smart_contract, |
||||||
|
abi: [ |
||||||
|
%{ |
||||||
|
"constant" => true, |
||||||
|
"inputs" => [], |
||||||
|
"name" => "get", |
||||||
|
"outputs" => [%{"name" => "", "type" => "uint256"}], |
||||||
|
"payable" => false, |
||||||
|
"stateMutability" => "view", |
||||||
|
"type" => "function" |
||||||
|
}, |
||||||
|
%{ |
||||||
|
"constant" => true, |
||||||
|
"inputs" => [%{"name" => "x", "type" => "uint256"}], |
||||||
|
"name" => "with_arguments", |
||||||
|
"outputs" => [%{"name" => "", "type" => "bool"}], |
||||||
|
"payable" => false, |
||||||
|
"stateMutability" => "view", |
||||||
|
"type" => "function" |
||||||
|
} |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
blockchain_get_function_mock() |
||||||
|
|
||||||
|
response = Reader.read_only_functions(smart_contract.address_hash) |
||||||
|
|
||||||
|
assert [ |
||||||
|
%{ |
||||||
|
"constant" => true, |
||||||
|
"inputs" => [], |
||||||
|
"name" => "get", |
||||||
|
"outputs" => [%{"name" => "", "type" => "uint256", "value" => 0}], |
||||||
|
"payable" => _, |
||||||
|
"stateMutability" => _, |
||||||
|
"type" => _ |
||||||
|
}, |
||||||
|
%{ |
||||||
|
"constant" => true, |
||||||
|
"inputs" => [%{"name" => "x", "type" => "uint256"}], |
||||||
|
"name" => "with_arguments", |
||||||
|
"outputs" => [%{"name" => "", "type" => "bool", "value" => ""}], |
||||||
|
"payable" => _, |
||||||
|
"stateMutability" => _, |
||||||
|
"type" => _ |
||||||
|
} |
||||||
|
] = response |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "query_function/2" do |
||||||
|
test "given the arguments, fetches the function value from the blockchain" do |
||||||
|
smart_contract = insert(:smart_contract) |
||||||
|
|
||||||
|
blockchain_get_function_mock() |
||||||
|
|
||||||
|
assert [ |
||||||
|
%{ |
||||||
|
"name" => "", |
||||||
|
"type" => "uint256", |
||||||
|
"value" => 0 |
||||||
|
} |
||||||
|
] = Reader.query_function(smart_contract.address_hash, %{name: "get", args: []}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "normalize_args/1" do |
||||||
|
test "converts argument when is a number" do |
||||||
|
assert [0] = Reader.normalize_args(["0"]) |
||||||
|
|
||||||
|
assert ["0x798465571ae21a184a272f044f991ad1d5f87a3f"] = |
||||||
|
Reader.normalize_args(["0x798465571ae21a184a272f044f991ad1d5f87a3f"]) |
||||||
|
end |
||||||
|
|
||||||
|
test "converts argument when is a boolean" do |
||||||
|
assert [true] = Reader.normalize_args(["true"]) |
||||||
|
assert [false] = Reader.normalize_args(["false"]) |
||||||
|
|
||||||
|
assert ["some string"] = Reader.normalize_args(["some string"]) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "link_outputs_and_values/2" do |
||||||
|
test "links the ABI outputs with the values retrieved from the blockchain" do |
||||||
|
blockchain_values = %{ |
||||||
|
"getOwner" => [ |
||||||
|
<<105, 55, 203, 37, 235, 84, 188, 1, 59, 156, 19, 196, 122, 179, 142, 182, 62, 221, 20, 147>> |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
outputs = [%{"name" => "", "type" => "address"}] |
||||||
|
|
||||||
|
function_name = "getOwner" |
||||||
|
|
||||||
|
assert [%{"name" => "", "type" => "address", "value" => "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"}] = |
||||||
|
Reader.link_outputs_and_values(blockchain_values, outputs, function_name) |
||||||
|
end |
||||||
|
|
||||||
|
test "correctly shows returns of 'bytes' type" do |
||||||
|
blockchain_values = %{ |
||||||
|
"get" => [ |
||||||
|
<<0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
outputs = [%{"name" => "", "type" => "bytes32"}] |
||||||
|
|
||||||
|
function_name = "get" |
||||||
|
|
||||||
|
assert [ |
||||||
|
%{ |
||||||
|
"name" => "", |
||||||
|
"type" => "bytes32", |
||||||
|
"value" => "0x000a000000000000000000000000000000000000000000000000000000000000" |
||||||
|
} |
||||||
|
] = Reader.link_outputs_and_values(blockchain_values, outputs, function_name) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp blockchain_get_function_mock() do |
||||||
|
EthereumJSONRPC.Mox |
||||||
|
|> expect( |
||||||
|
:json_rpc, |
||||||
|
fn [%{id: id, method: _, params: [%{data: _, to: _}]}], _options -> |
||||||
|
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x0000000000000000000000000000000000000000000000000000000000000000"}]} |
||||||
|
end |
||||||
|
) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,31 @@ |
|||||||
|
import $ from 'jquery' |
||||||
|
|
||||||
|
const readFunction = (element) => { |
||||||
|
const $element = $(element) |
||||||
|
const $form = $element.find('[data-function-form]') |
||||||
|
|
||||||
|
const $responseContainer = $element.find('[data-function-response]') |
||||||
|
|
||||||
|
$form.on('submit', (event) => { |
||||||
|
event.preventDefault() |
||||||
|
|
||||||
|
const url = $form.data('url') |
||||||
|
const $functionName = $form.find('input[name=function_name]') |
||||||
|
const $functionInputs = $form.find('input[name=function_input]') |
||||||
|
|
||||||
|
const args = $.map($functionInputs, element => { |
||||||
|
return $(element).val() |
||||||
|
}) |
||||||
|
|
||||||
|
const data = { |
||||||
|
function_name: $functionName.val(), |
||||||
|
args |
||||||
|
} |
||||||
|
|
||||||
|
$.get(url, data, response => $responseContainer.html(response)) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
$('[data-function]').each((_, element) => { |
||||||
|
readFunction(element) |
||||||
|
}) |
@ -0,0 +1,60 @@ |
|||||||
|
defmodule ExplorerWeb.AddressReadContractController do |
||||||
|
use ExplorerWeb, :controller |
||||||
|
|
||||||
|
alias Explorer.{Chain, Market} |
||||||
|
alias Explorer.ExchangeRates.Token |
||||||
|
alias Explorer.SmartContract.Reader |
||||||
|
|
||||||
|
import ExplorerWeb.AddressController, only: [transaction_count: 1] |
||||||
|
|
||||||
|
def index(conn, %{"address_id" => address_hash_string}) do |
||||||
|
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), |
||||||
|
{:ok, address} <- Chain.find_contract_address(address_hash) do |
||||||
|
read_only_functions = Reader.read_only_functions(address_hash) |
||||||
|
|
||||||
|
render( |
||||||
|
conn, |
||||||
|
"index.html", |
||||||
|
read_only_functions: read_only_functions, |
||||||
|
address: address, |
||||||
|
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), |
||||||
|
transaction_count: transaction_count(address) |
||||||
|
) |
||||||
|
else |
||||||
|
:error -> |
||||||
|
not_found(conn) |
||||||
|
|
||||||
|
{:error, :not_found} -> |
||||||
|
not_found(conn) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def show(conn, params) do |
||||||
|
with true <- ajax?(conn), |
||||||
|
{:ok, address_hash} <- Chain.string_to_address_hash(params["address_id"]), |
||||||
|
outputs = |
||||||
|
Reader.query_function( |
||||||
|
address_hash, |
||||||
|
%{name: params["function_name"], args: params["args"]} |
||||||
|
) do |
||||||
|
conn |
||||||
|
|> put_status(200) |
||||||
|
|> put_layout(false) |
||||||
|
|> render( |
||||||
|
"_function_response.html", |
||||||
|
function_name: params["function_name"], |
||||||
|
outputs: outputs |
||||||
|
) |
||||||
|
else |
||||||
|
_ -> |
||||||
|
not_found(conn) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp ajax?(conn) do |
||||||
|
case get_req_header(conn, "x-requested-with") do |
||||||
|
[value] -> value in ["XMLHttpRequest", "xmlhttprequest"] |
||||||
|
[] -> false |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,7 @@ |
|||||||
|
<span> |
||||||
|
[ <%= @function_name %> method Response ] <br /> |
||||||
|
|
||||||
|
<%= for item <- @outputs do %> |
||||||
|
<i class="fa fa-angle-double-right"></i> <%= item["type"] %> : <%= item["value"] %> |
||||||
|
<% end %> |
||||||
|
</span> |
@ -0,0 +1,99 @@ |
|||||||
|
<section class="container-fluid"> |
||||||
|
|
||||||
|
<%= render ExplorerWeb.AddressView, "overview.html", assigns %> |
||||||
|
|
||||||
|
<div class="card"> |
||||||
|
<div class="card-header"> |
||||||
|
<ul class="nav nav-tabs card-header-tabs"> |
||||||
|
<li class="nav-item"> |
||||||
|
<%= link( |
||||||
|
gettext("Transactions"), |
||||||
|
class: "nav-link", |
||||||
|
to: address_transaction_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]) |
||||||
|
) %> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<%= link( |
||||||
|
gettext("Internal Transactions"), |
||||||
|
class: "nav-link", |
||||||
|
"data-test": "internal_transactions_tab_link", |
||||||
|
to: address_internal_transaction_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]) |
||||||
|
) %> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<%= link( |
||||||
|
to: address_contract_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]), |
||||||
|
class: "nav-link") do %> |
||||||
|
<%= gettext("Code") %> |
||||||
|
|
||||||
|
<%= if smart_contract_verified?(@address) do %> |
||||||
|
<i class="far fa-check-circle"></i> |
||||||
|
<% end %> |
||||||
|
<% end %> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<%= link( |
||||||
|
gettext("Read Contract"), |
||||||
|
to: address_read_contract_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]), |
||||||
|
class: "nav-link active")%> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="card-body"> |
||||||
|
<h2> |
||||||
|
<i class="fas fa-book"></i> |
||||||
|
<%= gettext("Read Contract Information") %> |
||||||
|
</h2> |
||||||
|
|
||||||
|
<%= for {function, counter} <- Enum.with_index(@read_only_functions, 1) do %> |
||||||
|
<div class="d-flex py-2 flex-wrap border-bottom" data-function> |
||||||
|
<div class="py-2 pr-2"> |
||||||
|
<%= counter %>. |
||||||
|
|
||||||
|
<%= function["name"] %> |
||||||
|
|
||||||
|
→ |
||||||
|
</div> |
||||||
|
|
||||||
|
<%= if queryable?(function["inputs"]) do %> |
||||||
|
<form class="form-inline" data-function-form data-url="<%= address_read_contract_path(@conn, :show, :en, @address, @address.hash) %>"> |
||||||
|
<input type="hidden" name="function_name" value='<%= function["name"] %>' /> |
||||||
|
|
||||||
|
<%= for input <- function["inputs"] do %> |
||||||
|
<div class="form-group pr-2"> |
||||||
|
<input type="text" name="function_input" class="form-control form-control-sm mb-1" placeholder='<%= input["name"] %>(<%= input["type"] %>)' /> |
||||||
|
</div> |
||||||
|
<% end %> |
||||||
|
|
||||||
|
<input type="submit" value='<%= gettext("Query")%>' class="button button--secondary button--xsmall py-0" /> |
||||||
|
</form> |
||||||
|
<% else %> |
||||||
|
<span class="py-2"> |
||||||
|
<%= for output <- function["outputs"] do %> |
||||||
|
<%= if address?(output["type"]) do %> |
||||||
|
<%= link( |
||||||
|
output["value"], |
||||||
|
to: address_path(@conn, :show, @conn.assigns.locale, output["value"]) |
||||||
|
) %> |
||||||
|
<% else %> |
||||||
|
<%= output["value"] %> |
||||||
|
<% end %> |
||||||
|
<% end %> |
||||||
|
</span> |
||||||
|
<% end %> |
||||||
|
|
||||||
|
<div class='p-2 text-muted <%= if (queryable?(function["inputs"]) == true), do: "w-100" %>'> |
||||||
|
<%= if (queryable?(function["inputs"])), do: raw "↳" %> |
||||||
|
|
||||||
|
<%= for output <- function["outputs"] do %> |
||||||
|
<%= output["type"] %> |
||||||
|
<% end %> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div data-function-response></div> |
||||||
|
</div> |
||||||
|
<% end %> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
@ -0,0 +1,9 @@ |
|||||||
|
defmodule ExplorerWeb.AddressReadContractView do |
||||||
|
use ExplorerWeb, :view |
||||||
|
|
||||||
|
import ExplorerWeb.AddressView, only: [smart_contract_verified?: 1] |
||||||
|
|
||||||
|
def queryable?(inputs), do: Enum.any?(inputs) |
||||||
|
|
||||||
|
def address?(type), do: type == "address" |
||||||
|
end |
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"lockfileVersion": 1 |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
defmodule ExplorerWeb.AddressReadContractControllerTest do |
||||||
|
use ExplorerWeb.ConnCase |
||||||
|
|
||||||
|
describe "GET show/3" do |
||||||
|
test "only responds to ajax requests", %{conn: conn} do |
||||||
|
smart_contract = insert(:smart_contract) |
||||||
|
|
||||||
|
path = |
||||||
|
address_read_contract_path( |
||||||
|
ExplorerWeb.Endpoint, |
||||||
|
:show, |
||||||
|
:en, |
||||||
|
smart_contract.address_hash, |
||||||
|
smart_contract.address_hash, |
||||||
|
function_name: "get", |
||||||
|
args: [] |
||||||
|
) |
||||||
|
|
||||||
|
conn = get(conn, path) |
||||||
|
|
||||||
|
assert conn.status == 404 |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,19 @@ |
|||||||
|
defmodule ExplorerWeb.AddressReadContractViewTest do |
||||||
|
use ExplorerWeb.ConnCase, async: true |
||||||
|
|
||||||
|
alias ExplorerWeb.AddressReadContractView |
||||||
|
|
||||||
|
describe "queryable?/1" do |
||||||
|
test "returns true if list of inputs is not empty" do |
||||||
|
assert AddressReadContractView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true |
||||||
|
assert AddressReadContractView.queryable?([]) == false |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "address?/1" do |
||||||
|
test "returns true if type equals `address`" do |
||||||
|
assert AddressReadContractView.address?("address") == true |
||||||
|
assert AddressReadContractView.address?("uint256") == false |
||||||
|
end |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue