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