Current section
Files
Jump to
Current section
Files
lib/web3/types/function.ex
defmodule Web3.Type.Function do
@moduledoc false
alias Web3.Dispatcher
alias Web3.Type.{Address, Data, HexDigit, Full}
defstruct [
:name,
:inputs,
:outputs,
:constant,
:payable,
:state_mutability
]
defmacro __using__(opts) do
function = opts[:function]
define_func_call(function)
end
def method_signature(%{name: name, inputs: inputs}) do
param_types =
inputs
|> Enum.map(&input_type/1)
|> Enum.join(",")
"#{name}(#{param_types})"
end
def input_type({_, type}) do
Web3.ABI.type_name(type)
end
def define_func_call(%__MODULE__{} = abi) do
signature = method_signature(abi)
func_selector =
signature
|> ExKeccak.hash_256()
|> Web3.ABI.to_hex()
|> binary_part(0, 10)
abi_name =
signature
|> String.replace(["(", ")", "[", "]", ","], "_")
|> String.to_atom()
params =
abi.inputs
|> Enum.with_index(1)
|> Enum.map(fn {input, i} ->
var_name =
case elem(input, 0) do
:"" -> :"arg#{i}"
name -> name
end
Macro.var(var_name, nil)
end)
return_types =
abi.outputs
|> Enum.map(&elem(&1, 1))
case abi.state_mutability do
:view ->
define_immutable_call(abi, signature, func_selector, abi_name, params, return_types)
:pure ->
define_immutable_call(abi, signature, func_selector, abi_name, params, return_types)
:nonpayable ->
define_transaction_call(abi, signature, func_selector, abi_name, params)
:payable ->
define_transaction_call(abi, signature, func_selector, abi_name, params)
_ ->
nil
end
end
def define_transaction_call(abi, signature, selector, abi_name, params) do
quote generated: true do
def unquote(:"inspect_#{abi_name}")(unquote_splicing(params), opts) do
unquote(__MODULE__).tco(
unquote(params),
unquote(Macro.escape(abi)),
unquote(selector),
Keyword.merge([to: @contract_address], opts)
)
end
@doc unquote(signature)
def unquote(abi_name)(unquote_splicing(params), opts) do
result =
unquote(__MODULE__).tco(
unquote(params),
unquote(Macro.escape(abi)),
unquote(selector),
Keyword.merge([to: @contract_address], opts)
)
signed_txn = Web3.ABI.Signer.sign_transaction(result, @priv_key)
payload = %Dispatcher.Payload{
json_rpc_arguments: [
http: Keyword.get(@config, :http),
http_options: Keyword.get(@config, :http_options, []),
rpc_endpoint: Keyword.get(@config, :rpc_endpoint)
],
middleware: Keyword.get(@config, :middleware, []),
args: [signed_txn],
return_fn: :raw,
method: :eth_sendRawTransaction
}
Dispatcher.dispatch(payload)
end
end
end
def define_immutable_call(abi, signature, selector, abi_name, params, return_types) do
return_type_string =
return_types
|> Enum.map(&Web3.ABI.type_name/1)
|> Enum.join(",")
doc = """
#{signature} returns (#{return_type_string})
"""
quote generated: true do
@doc unquote(doc)
def unquote(abi_name)(unquote_splicing(params), opts \\ []) do
block =
Keyword.get(opts, :block, :latest)
|> unquote(__MODULE__).normalize_block_number()
tco =
unquote(__MODULE__).tco(
unquote(params),
unquote(Macro.escape(abi)),
unquote(selector),
Keyword.merge([to: @contract_address], opts)
)
payload = %Dispatcher.Payload{
json_rpc_arguments: [
http: Keyword.get(@config, :http),
http_options: Keyword.get(@config, :http_options, []),
rpc_endpoint: Keyword.get(@config, :rpc_endpoint)
],
middleware: Keyword.get(@config, :middleware, []),
args: [tco, block],
method: :eth_call,
return_fn: unquote(Macro.escape(return_types))
}
Dispatcher.dispatch(payload)
end
end
end
def tco(inputs, function, selector, opts) do
input_types = Enum.map(function.inputs, &elem(&1, 1))
params_data =
encode_inputs(inputs, input_types)
|> Base.encode16(case: :lower)
data = selector <> params_data
to =
Keyword.get(opts, :to)
|> normalize_address()
Map.new(opts)
|> Map.take([:from, :gas, :gas_price, :gas_limit, :value, :nonce, :chain_id])
|> Map.merge(%{to: to, data: data})
end
def encode_inputs(inputs, input_types) do
encoded_inputs =
for {value, type} <- Enum.zip(inputs, input_types) do
Web3.ABI.TypeEncoder.encode(value, type)
end
Web3.ABI.TypeEncoder.pack(encoded_inputs, input_types)
end
def cast_inputs(inputs, types) do
inputs
|> Enum.zip(types)
|> Enum.map(fn
{input, type} -> cast_input(input, type)
end)
end
defp cast_input(address, :address) do
with {:ok, %{bytes: bytes}} <- Address.cast(address) do
bytes
end
end
defp cast_input(bytes_value, {:bytes, _}) do
with {:ok, %{bytes: bytes}} <- Full.cast(bytes_value) do
bytes
end
end
defp cast_input(bytes_value, :bytes) do
with {:ok, %{bytes: bytes}} <- Data.cast(bytes_value) do
bytes
end
end
defp cast_input(int_value, {int_type, _}) when int_type in [:int, :uint] do
with {:ok, %{value: value}} <- HexDigit.cast(int_value) do
value
end
end
defp cast_input(values, {:array, inner_type}) do
Enum.map(values, &cast_input(&1, inner_type))
end
defp cast_input(values, {:array, inner_type, _n}) do
Enum.map(values, &cast_input(&1, inner_type))
end
defp cast_input(values, {:tuple, types}) do
values
|> Tuple.to_list()
|> Enum.zip(types)
|> Enum.map(fn {value, type} -> cast_input(value, type) end)
|> List.to_tuple()
end
defp cast_input(input, _type), do: input
def normalize_address(address) do
{:ok, addr} = Address.cast(address)
to_string(addr)
end
def normalize_block_number(<<"0x", _::binary()>> = block), do: block
def normalize_block_number(block) when is_integer(block),
do: "0x" <> Integer.to_string(block, 16)
def normalize_block_number(block) when block in [:latest, :earliest, :pending], do: block
end