defmodule Ethers.Contract do
@moduledoc """
Dynamically creates modules for ABIs at compile time.
## How to use
You can simply create a new module and call `use Ethers.Contract` in it with the desired parameters.
```elixir
defmodule MyProject.Contract do
use Ethers.Contract, abi_file: "path/to/abi.json"
end
```
After this, the functions in your contracts should be accessible just by calling
```elixir
data = MyProject.Contract.example_function(...)
# Use data to handle eth_call
Ethers.Contract.call(data, to: "0xADDRESS", from: "0xADDRESS")
{:ok, [...]}
```
## Valid `use` options
- `abi`: Used to pass in the encoded/decoded json ABI of contract.
- `abi_file`: Used to pass in the file path to the json ABI of contract.
- `default_address`: Default contract deployed address. Can be overridden with `:to` option in every function.
## Execution Options
These can be specified for all the actions by contracts.
- `action`: Type of action for this function. Here are available values.
- `:call` uses `eth_call` to call the function and get the result. Will not change blockchain state or cost gas.
- `:send` uses `eth_sendTransaction` to call
- `:estimate_gas` uses `eth_estimateGas` to estimate the gas usage of this transaction.
- `:prepare` only prepares the `data` needed to make a transaction. Useful for Multicall.
- `from`: The address of the wallet making this transaction. The private key should be loaded in the rpc server (For example: go-ethereum). Must be in `"0x..."` format.
- `gas`: The gas limit for your transaction.
- `rpc_client`: The RPC module implementing Ethereum JSON RPC functions. Defaults to `Ethereumex.HttpClient`
- `rpc_opts`: Options to pass to the RCP client e.g. `:url`.
- `to`: The address of the recipient contract. It will be defaulted to `default_address` if it was specified in Contract otherwise is required. Must be in `"0x..."` format.
"""
require Ethers.ContractHelpers
import Ethers.ContractHelpers
@type action :: :call | :send | :prepare
@type t_function_output :: %{
data: binary,
to: Ethers.Types.t_address(),
selector: ABI.FunctionSelector.t()
}
@type t_event_output :: %{
topics: [binary],
address: Ethers.Types.t_address(),
selector: ABI.FunctionSelector.t()
}
@default_constructor %{
selector: %ABI.FunctionSelector{
function: nil,
method_id: nil,
type: :constructor,
inputs_indexed: nil,
state_mutability: nil,
input_names: [],
types: [],
returns: []
}
}
defmacro __using__(opts) do
compiler_module = __MODULE__
quote do
@before_compile unquote(compiler_module)
Module.put_attribute(__MODULE__, :_ethers_using_opts, unquote(opts))
end
end
defmacro __before_compile__(env) do
module = env.module
{opts, _} =
module
|> Module.get_attribute(:_ethers_using_opts)
|> Code.eval_quoted([], env)
{:ok, abi} = read_abi(opts)
contract_binary = maybe_read_contract_binary(opts)
default_address = Keyword.get(opts, :default_address)
function_selectors = ABI.parse_specification(abi, include_events?: true)
function_selectors_with_meta =
Enum.map(function_selectors, fn %{function: function} = selector ->
%{
selector: selector,
has_other_arities: Enum.count(function_selectors, &(&1.function == function)) > 1
}
end)
constructor_ast =
function_selectors_with_meta
|> Enum.find(&(&1.selector.type == :constructor))
|> then(&(&1 || @default_constructor))
|> generate_method(module)
functions_ast =
function_selectors_with_meta
|> Enum.filter(&(&1.selector.type == :function and not is_nil(&1.selector.function)))
|> Enum.map(&generate_method(&1, module))
events_mod_name = Module.concat(module, EventFilters)
events =
function_selectors_with_meta
|> Enum.filter(&(&1.selector.type == :event))
|> Enum.map(&generate_event_filter(&1, module))
events_module_ast =
quote context: module do
defmodule unquote(events_mod_name) do
@moduledoc "Events for `#{Macro.to_string(unquote(module))}`"
defdelegate default_address, to: unquote(module)
unquote(events)
end
end
extra_ast =
quote context: module do
def __contract_binary__, do: unquote(contract_binary)
@doc """
Default address of the contract. Returns `nil` if not specified.
To specify a default address see `Ethers.Contract`
"""
@spec default_address() :: Ethers.Types.t_address() | nil
def default_address, do: unquote(default_address)
end
[extra_ast, constructor_ast | functions_ast] ++ [events_module_ast]
end
@doc false
@spec perform_action(action(), map, Keyword.t()) ::
{:ok, [term]}
| {:ok, Ethers.Types.t_hash()}
| {:ok, Ethers.Contract.t_function_output()}
| {:error, term()}
def perform_action(action, params, overrides)
def perform_action(:call, params, overrides),
do: Ethers.RPC.call(params, overrides)
def perform_action(:send, params, overrides),
do: Ethers.RPC.send(params, overrides)
def perform_action(:estimate_gas, params, overrides),
do: Ethers.RPC.estimate_gas(params, overrides)
def perform_action(:prepare, params, overrides),
do: {:ok, Enum.into(overrides, params)}
def perform_action(action, _params, _overrides),
do: raise(ArgumentError, "Invalid action: #{inspect(action)}")
## Helpers
@spec generate_method(map(), atom()) :: any()
defp generate_method(%{selector: %ABI.FunctionSelector{type: :constructor} = selector}, mod) do
func_args = generate_arguments(mod, selector.types, selector.input_names)
func_input_types =
selector.types
|> Enum.map(&Ethers.Types.to_elixir_type/1)
quote context: mod, location: :keep do
@doc """
Prepares contract constructor values.
To deploy a contracts see `Ethers.deploy/3`.
## Parameters
#{unquote(document_types(selector.types, selector.input_names))}
"""
@spec constructor(unquote_splicing(func_input_types)) :: binary()
def constructor(unquote_splicing(func_args)) do
args =
unquote(func_args)
|> Enum.zip(unquote(Macro.escape(selector.types)))
|> Enum.map(fn {arg, type} -> Ethers.Utils.prepare_arg(arg, type) end)
unquote(Macro.escape(selector))
|> ABI.encode(args)
|> Ethers.Utils.hex_encode(false)
end
end
end
defp generate_method(
%{
selector: %ABI.FunctionSelector{type: :function} = selector,
has_other_arities: has_other_arities
} = _function_data,
mod
) do
name =
selector.function
|> Macro.underscore()
|> String.to_atom()
bang_fun_name = String.to_atom("#{name}!")
func_args = generate_arguments(mod, selector.types, selector.input_names)
func_input_types =
selector.types
|> Enum.map(&Ethers.Types.to_elixir_type/1)
func_return_typespec =
selector.returns
|> Enum.map(&Ethers.Types.to_elixir_type/1)
|> then(fn
[] -> []
list -> Enum.reduce(list, &{:|, [], [&1, &2]})
end)
default_action = get_default_action(selector)
overrides = get_overrides(mod, has_other_arities)
quote context: mod, location: :keep do
@doc """
Executes `#{unquote(human_signature(selector))}` on the contract.
Default action for this function is `#{inspect(unquote(default_action))}`.
To override default action see Execution Options in `Ethers.Contract`.
## Parameters
#{unquote(document_types(selector.types, selector.input_names))}
- overrides: Overrides and options for the call. See Execution Options in `Ethers.Contract`.
## Return Types
#{unquote(document_types(selector.returns))}
"""
@spec unquote(name)(unquote_splicing(func_input_types), Keyword.t()) ::
{:ok, [unquote(func_return_typespec)]}
| {:ok, Ethers.Types.t_hash()}
| {:ok, Ethers.Contract.t_function_output()}
| {:error, term()}
def unquote(name)(unquote_splicing(func_args), unquote(overrides)) do
args =
unquote(func_args)
|> Enum.zip(unquote(Macro.escape(selector.types)))
|> Enum.map(fn {arg, type} -> Ethers.Utils.prepare_arg(arg, type) end)
data =
unquote(Macro.escape(selector))
|> ABI.encode(args)
|> Ethers.Utils.hex_encode()
params =
%{
data: data,
selector: unquote(Macro.escape(selector))
}
|> maybe_add_to_address(__MODULE__)
{action, overrides} = Keyword.pop(overrides, :action, unquote(default_action))
Ethers.Contract.perform_action(action, params, overrides)
end
@doc """
Same as `#{unquote(name)}/#{unquote(Enum.count(func_args) + 1)}` but raises `Ethers.ExecutionError` on errors.
"""
@spec unquote(bang_fun_name)(unquote_splicing(func_input_types), Keyword.t()) ::
[unquote(func_return_typespec)]
| Ethers.Types.t_hash()
| Ethers.Contract.t_function_output()
| no_return
def unquote(bang_fun_name)(unquote_splicing(func_args), unquote(overrides)) do
case unquote(name)(unquote_splicing(func_args), overrides) do
{:ok, result} ->
result
{:error, reason} ->
raise Ethers.ExecutionError,
error: reason,
function: unquote(name),
args: unquote(func_args)
end
end
end
end
defp generate_event_filter(
%{
selector: %ABI.FunctionSelector{type: :event} = selector,
has_other_arities: has_other_arities
} = _function_data,
mod
) do
name =
selector.function
|> Macro.underscore()
|> String.to_atom()
func_args = generate_arguments(mod, selector.inputs_indexed, selector.input_names)
{indexed_types, non_indexed_types} =
selector.types
|> Enum.zip(selector.inputs_indexed)
|> Enum.reduce({[], []}, fn
{type, true}, {indexed, non_indexed} ->
{indexed ++ [type], non_indexed}
{type, false}, {indexed, non_indexed} ->
{indexed, non_indexed ++ [type]}
end)
func_input_typespec =
indexed_types
|> Enum.map(&Ethers.Types.to_elixir_type/1)
selector = %{selector | returns: non_indexed_types}
topic_0 =
selector
|> ABI.FunctionSelector.encode()
|> Ethers.keccak_module().hash_256()
|> Ethers.Utils.hex_encode()
overrides = get_overrides(mod, has_other_arities)
quote context: mod, location: :keep do
@doc """
Create event filter for `#{unquote(human_signature(selector))}`
For each indexed parameter you can either pass in the value you want to
filter or `nil` if you don't want to filter.
## Parameters
#{unquote(document_types(indexed_types, selector.input_names))}
- overrides: Overrides and options for the call.
- `address`: The address or list of addresses of the originating contract(s). (**Optional**)
## Event Data Types
#{unquote(document_types(selector.types, selector.input_names))}
"""
@spec unquote(name)(unquote_splicing(func_input_typespec), Keyword.t()) ::
Ethers.Contract.t_event_output()
def unquote(name)(unquote_splicing(func_args), unquote(overrides)) do
address = Keyword.get(overrides, :address, __MODULE__.default_address())
sub_topics =
Enum.zip(unquote(Macro.escape(selector.types)), unquote(func_args))
|> Enum.map(fn
{_, nil} ->
nil
{type, value} ->
value
|> Ethers.Utils.prepare_arg(type)
|> List.wrap()
|> ABI.TypeEncoder.encode([type])
|> Ethers.Utils.hex_encode()
end)
%{
topics: [unquote(topic_0) | sub_topics],
address: address,
selector: unquote(Macro.escape(selector))
}
end
end
end
end