|
|
|
@ -8,9 +8,10 @@ defmodule Explorer.SmartContract.Reader do |
|
|
|
|
|
|
|
|
|
alias Explorer.Chain |
|
|
|
|
alias EthereumJSONRPC.Encoder |
|
|
|
|
alias Explorer.Chain.Hash |
|
|
|
|
|
|
|
|
|
@doc """ |
|
|
|
|
Queries a contract function on the blockchain and returns the call result. |
|
|
|
|
Queries the contract functions on the blockchain and returns the call results. |
|
|
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
|
@ -23,9 +24,9 @@ defmodule Explorer.SmartContract.Reader do |
|
|
|
|
) |
|
|
|
|
# => %{"sum" => [42]} |
|
|
|
|
""" |
|
|
|
|
@spec query_contract(String.t(), %{String.t() => [term()]}) :: map() |
|
|
|
|
def query_contract(contract_address, functions) do |
|
|
|
|
{:ok, address_hash} = Chain.string_to_address_hash(contract_address) |
|
|
|
|
@spec query_contract(%Explorer.Chain.Hash{}, %{String.t() => [term()]}) :: map() |
|
|
|
|
def query_contract(address_hash, functions) do |
|
|
|
|
contract_address = Hash.to_string(address_hash) |
|
|
|
|
|
|
|
|
|
abi = |
|
|
|
|
address_hash |
|
|
|
@ -52,4 +53,139 @@ defmodule Explorer.SmartContract.Reader do |
|
|
|
|
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 => 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 |
|
|
|
|