Add read only functions to 'SmartContract.Reader'

pull/279/head
Amanda Sposito 7 years ago
parent 9532c6e9fe
commit 73856a72df
  1. 144
      apps/explorer/lib/explorer/smart_contract/reader.ex
  2. 153
      apps/explorer/test/explorer/smart_contract/reader_test.exs

@ -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

@ -5,12 +5,11 @@ defmodule Explorer.SmartContract.ReaderTest do
doctest Explorer.SmartContract.Reader
alias Explorer.SmartContract.Reader
alias Plug.Conn
alias Explorer.Chain.Hash
@ethereum_jsonrpc_original Application.get_env(:ethereum_jsonrpc, :url)
describe "query_contract/2" do
setup do
bypass = Bypass.open()
@ -23,17 +22,20 @@ defmodule Explorer.SmartContract.ReaderTest do
{:ok, bypass: bypass}
end
test "correctly returns the result of a smart contract function", %{bypass: bypass} do
blockchain_resp =
"[{\"jsonrpc\":\"2.0\",\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"id\":\"get\"}]\n"
Bypass.expect(bypass, fn conn -> Conn.resp(conn, 200, blockchain_resp) end)
describe "query_contract/2" do
test "correctly returns the results of the smart contract functions", %{bypass: bypass} do
Bypass.expect(bypass, fn conn ->
Conn.resp(
conn,
200,
~s[{"jsonrpc":"2.0","result":"0x0000000000000000000000000000000000000000000000000000000000000000","id":"get"}]
)
end)
hash =
:smart_contract
|> insert()
|> Map.get(:address_hash)
|> Hash.to_string()
assert Reader.query_contract(hash, %{"get" => []}) == %{"get" => [0]}
end
@ -51,4 +53,139 @@ defmodule Explorer.SmartContract.ReaderTest do
) == %{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", %{bypass: bypass} 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"
}
]
)
Bypass.expect(bypass, fn conn ->
Conn.resp(
conn,
200,
~s[{"jsonrpc":"2.0","result":"0x0000000000000000000000000000000000000000000000000000000000000000","id":"get"}]
)
end)
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", %{bypass: bypass} do
smart_contract = insert(:smart_contract)
Bypass.expect(bypass, fn conn ->
Conn.resp(
conn,
200,
~s[{"jsonrpc":"2.0","result":"0x0000000000000000000000000000000000000000000000000000000000000000","id":"get"}]
)
end)
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
end

Loading…
Cancel
Save