defmodule Web3.Contract do
@moduledoc """
Smart contract functions executed by Dispatcher
"""
require Logger
alias Web3.Dispatcher
@spec execute(Keyword.t(), Keyword.t(), Keyword.t()) :: {:ok, [{:ok, any()}, {:error, any()}]}
def execute(requests, abi, options) do
parsed_abi =
abi
|> Enum.map(&Web3.ABI.Compiler.parse_abi/1)
|> Enum.reject(&is_nil/1)
functions = Enum.into(parsed_abi, %{}, &{&1.name, &1})
requests_with_index = Enum.with_index(requests)
indexed_responses =
requests_with_index
|> Enum.map(fn {%{contract_address: contract_address, method_name: method_name, args: args} = request, index} ->
function = define_function(functions, method_name)
func_selector =
function
|> Web3.Type.Function.method_signature()
|> Web3.keccak256()
|> binary_part(0, 10)
case function.state_mutability do
value when value in [:pure, :view] ->
tco = Web3.Type.Function.tco(args, function, func_selector, to: contract_address)
block = Map.get(request, :block, :latest)
%{id: index, jsonrpc: "2.0", method: :eth_call, params: [tco, block]}
value when value in [:nonpayable, :payable] ->
tco = Web3.Type.Function.tco(args, function, func_selector, Keyword.merge([to: contract_address], Map.to_list(request)))
priv_key = Map.get(request, :priv_key, nil)
{:ok, priv_key} = Web3.parse_privkey(priv_key)
signed_txn = Web3.ABI.Signer.sign_transaction(tco, priv_key)
%{id: index, jsonrpc: "2.0", method: :eth_sendRawTransaction, params: [signed_txn]}
_ ->
raise "Unsupported state mutability: #{function.state_mutability}"
end
end)
|> build_payload(options)
|> Dispatcher.dispatch()
|> case do
{:ok, responses} -> responses
{:error, {:bad_gateway, _request_url}} -> raise "Bad gateway"
{:error, reason} when is_atom(reason) -> raise Atom.to_string(reason)
{:error, error} -> raise error
end
|> Enum.into(%{}, &{&1.id, &1})
Enum.map(requests_with_index, fn {%{method_name: method_name, args: _args}, index} ->
indexed_responses[index]
|> case do
%{error: error} ->
{index, {:error, error}}
response ->
function = define_function(functions, method_name)
{^index, result} = decode_result(response, function)
{index, result}
end
end)
rescue
error ->
Enum.map(requests, fn _ -> format_error(error) end)
end
defp build_payload(params, options) do
json_rpc_arguments = [
http: Keyword.get(options, :http),
http_options: Keyword.get(options, :http_options),
rpc_endpoint: Keyword.get(options, :rpc_endpoint)
]
%Dispatcher.Payload{
json_rpc_arguments: json_rpc_arguments,
request: params,
method: :__skip_parser__,
middleware: Keyword.get(options, :middleware)
}
end
defp define_function(functions, target_method_name) do
{_, function} =
Enum.find(functions, fn {method_name, _func} ->
method_name == target_method_name
end)
function
end
def decode_result(%{error: %{code: code, data: data, message: message}, id: id}), do: {id, {:error, "(#{code}) #{message} (#{data})"}}
def decode_result(%{error: %{code: code, message: message}, id: id}), do: {id, {:error, "(#{code}) #{message}"}}
def decode_result(%{id: id, result: result}, function) do
return_types = function.outputs |> Enum.map(&elem(&1, 1))
decoded_data =
result
|> Web3.Middleware.Parser.decode_value(return_types)
|> Web3.Middleware.Parser.unwrap()
{id, {:ok, decoded_data}}
rescue
MatchError ->
{id, {:error, :invalid_data}}
end
# format error
defp format_error(message) when is_binary(message), do: {:error, message}
defp format_error(%{message: error_message}), do: format_error(error_message)
defp format_error(error), do: format_error(Exception.message(error))
end