Skip to content

Commit

Permalink
Implement ERC-165 support (#115)
Browse files Browse the repository at this point in the history
* Implement ERC-165 support

* Add documentation and improve design
  • Loading branch information
alisinabh authored Apr 12, 2024
1 parent 20a008b commit 78c0d09
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 33 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Enhancements

- Add ERC-165 contract and behaviour
- Add `skip_docs` option for contract module doc and typespec generation

## v0.4.3 (2024-04-05)

### Bug fixes
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ Ethers already includes some of the well-known contract interface standards for
Here is a list of them.

- [ERC20](https://hexdocs.pm/ethers/Ethers.Contracts.ERC20.html) - The well know fungible token standard
- [ERC165](https://hexdocs.pm/ethers/Ethers.Contracts.ERC165.html) - Standard Interface detection
- [ERC721](https://hexdocs.pm/ethers/Ethers.Contracts.ERC721.html) - Non-Fungible tokens (NFTs) standard
- [ERC777](https://hexdocs.pm/ethers/Ethers.Contracts.ERC777.html) - Improved fungible token standard
- [ERC1155](https://hexdocs.pm/ethers/Ethers.Contracts.ERC1155.html) - Multi-Token standard (Fungible, Non-Fungible or Semi-Fungible)
Expand Down
90 changes: 59 additions & 31 deletions lib/ethers/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ defmodule Ethers.Contract do
- `abi`: Used to pass in the decoded (or even encoded json binay) ABI of contract.
- `abi_file`: Used to pass in the file path to the json ABI of contract.
- `default_address`: Default contract deployed address to include in the parameters. (Optional)
- `skip_docs`: Determines if Ethers should skip generating docs and typespecs. (Default: false)
- `true`: Skip docs and typespecs for all functions.
- `false`: Generate docs and typespecs for all functions.
- `[{function_name :: atom(), skip_docs :: boolean()}]`: Specify for each function.
"""

require Ethers.ContractHelpers
Expand Down Expand Up @@ -79,6 +83,7 @@ defmodule Ethers.Contract do
{:ok, abi} = read_abi(opts)
contract_binary = maybe_read_contract_binary(opts)
default_address = Keyword.get(opts, :default_address)
skip_docs = Keyword.get(opts, :skip_docs, false)

function_selectors = ABI.parse_specification(abi, include_events?: true)

Expand All @@ -100,22 +105,24 @@ defmodule Ethers.Contract do
}
end)

impl_opts = [skip_docs: skip_docs]

constructor_ast =
function_selectors_with_meta
|> Enum.find(@default_constructor, &(&1.type == :constructor))
|> impl(module)
|> impl(module, impl_opts)

functions_ast =
function_selectors_with_meta
|> Enum.filter(&(&1.type == :function and not is_nil(&1.function)))
|> Enum.map(&impl(&1, module))
|> Enum.map(&impl(&1, module, impl_opts))

events_mod_name = Module.concat(module, EventFilters)

events =
function_selectors_with_meta
|> Enum.filter(&(&1.type == :event))
|> Enum.map(&impl(&1, module))
|> Enum.map(&impl(&1, module, impl_opts))

events_module_ast =
quote context: module do
Expand All @@ -132,7 +139,7 @@ defmodule Ethers.Contract do
error_modules_ast =
function_selectors_with_meta
|> Enum.filter(&(&1.type == :error))
|> Enum.map(&impl(&1, module))
|> Enum.map(&impl(&1, module, impl_opts))

errors_module_impl = errors_impl(function_selectors_with_meta, module)

Expand Down Expand Up @@ -171,24 +178,27 @@ defmodule Ethers.Contract do

## Helpers

defp impl(%{type: :constructor, selectors: [selector]} = abi, mod) do
defp impl(%{type: :constructor, selectors: [selector]} = abi, mod, opts) do
func_args = generate_arguments(mod, abi.arity, 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 for deployment.
if unquote(generate_docs?(:constructor, opts[:skip_docs])) do
@doc """
Prepares contract constructor values for deployment.
To deploy a contracts use `Ethers.deploy/2` and pass the result of this function as
`:encoded_constructor` option.
To deploy a contracts use `Ethers.deploy/2` and pass the result of this function as
`:encoded_constructor` option.
## Parameters
#{unquote(document_types(selector.types, selector.input_names))}
"""
@spec constructor(unquote_splicing(func_input_types)) :: binary()
end

## 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)
Expand All @@ -202,7 +212,7 @@ defmodule Ethers.Contract do
end
end

defp impl(%{type: :function} = abi, mod) do
defp impl(%{type: :function} = abi, mod, opts) do
name =
abi.function
|> Macro.underscore()
Expand All @@ -216,16 +226,19 @@ defmodule Ethers.Contract do
func_input_types = generate_typespecs(abi.selectors)

quote context: mod, location: :keep do
@doc """
Prepares `#{unquote(human_signature(abi.selectors))}` call parameters on the contract.
if unquote(generate_docs?(name, opts[:skip_docs])) do
@doc """
Prepares `#{unquote(human_signature(abi.selectors))}` call parameters on the contract.
#{unquote(document_help_message(abi.selectors))}
#{unquote(document_help_message(abi.selectors))}
#{unquote(document_parameters(abi.selectors))}
#{unquote(document_parameters(abi.selectors))}
#{unquote(document_returns(abi.selectors))}
"""
@spec unquote(name)(unquote_splicing(func_input_types)) :: Ethers.TxData.t()
end

#{unquote(document_returns(abi.selectors))}
"""
@spec unquote(name)(unquote_splicing(func_input_types)) :: Ethers.TxData.t()
def unquote(name)(unquote_splicing(func_args)) do
{selector, raw_args} =
find_selector!(unquote(Macro.escape(abi.selectors)), unquote(func_args))
Expand All @@ -241,7 +254,7 @@ defmodule Ethers.Contract do
end
end

defp impl(%{type: :event} = abi, mod) do
defp impl(%{type: :event} = abi, mod, opts) do
name =
abi.function
|> Macro.underscore()
Expand All @@ -254,17 +267,20 @@ defmodule Ethers.Contract do
func_typespec = generate_event_typespecs(abi.selectors, abi.arity)

quote context: mod, location: :keep do
@doc """
Create event filter for `#{unquote(human_signature(abi.selectors))}`
if unquote(generate_docs?(name, opts[:skip_docs])) do
@doc """
Create event filter for `#{unquote(human_signature(abi.selectors))}`
For each indexed parameter you can either pass in the value you want to
filter or `nil` if you don't want to filter.
For each indexed parameter you can either pass in the value you want to
filter or `nil` if you don't want to filter.
#{unquote(document_parameters(abi.selectors))}
#{unquote(document_parameters(abi.selectors))}
#{unquote(document_returns(abi.selectors))}
"""
@spec unquote(name)(unquote_splicing(func_typespec)) :: Ethers.EventFilter.t()
end

#{unquote(document_returns(abi.selectors))}
"""
@spec unquote(name)(unquote_splicing(func_typespec)) :: Ethers.EventFilter.t()
def unquote(name)(unquote_splicing(func_args)) do
{selector, raw_args} =
find_selector!(unquote(Macro.escape(abi.selectors)), unquote(func_args))
Expand All @@ -275,7 +291,7 @@ defmodule Ethers.Contract do
end
end

defp impl(%{type: :error, selectors: [selector_abi]} = abi, mod) do
defp impl(%{type: :error, selectors: [selector_abi]} = abi, mod, _opts) do
error_module = Module.concat([mod, Errors, abi.function])

aggregated_arg_names = aggregate_input_names(abi.selectors)
Expand Down Expand Up @@ -347,4 +363,16 @@ defmodule Ethers.Contract do
defp error_mappings, do: unquote(error_mappings)
end
end

defp generate_docs?(_name, true = _skip_docs), do: false
defp generate_docs?(_name, false = _skip_docs), do: true
defp generate_docs?(_name, nil = _skip_docs), do: true

defp generate_docs?(name, skip_docs) do
case Keyword.get(skip_docs, name) do
nil -> true
false -> true
true -> false
end
end
end
10 changes: 9 additions & 1 deletion lib/ethers/contracts/erc1155.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ defmodule Ethers.Contracts.ERC1155 do
@moduledoc """
ERC1155 token interface
More info: https://ethereum.org/en/developers/docs/standards/tokens/erc-1155/
More info: https://eips.ethereum.org/EIPS/eip-1155
"""

use Ethers.Contract, abi: :erc1155

@behaviour Ethers.Contracts.ERC165

# ERC-165 Interface ID
@interface_id Ethers.Utils.hex_decode!("0xd9b67a26")

@impl Ethers.Contracts.ERC165
def erc165_interface_id, do: @interface_id
end
79 changes: 79 additions & 0 deletions lib/ethers/contracts/erc165.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
defmodule Ethers.Contracts.ERC165 do
@moduledoc """
ERC-165 Standard Interface Detection
More info: https://eips.ethereum.org/EIPS/eip-165
## Modules as Interface IDs
Contract modules can opt to implement EIP-165 behaviour so that their name can be used
directly with the `supports_interface/1` function in this module. See below example:
```elixir
defmodule MyEIP165CompatibleContract do
use Ethers.Contract, abi: ...
@behaviour Ethers.Contracts.ERC165
@impl true
def erc165_interface_id, do: Ethers.Utils.hex_decode("[interface_id]")
end
```
Now module name can be used instead of interface_id and will have the same result.
```elixir
iex> Ethers.Contracts.ERC165.supports_interface("[interface_id]") ==
Ethers.Contracts.ERC165.supports_interface(MyEIP165CompatibleContract)
true
```
"""
use Ethers.Contract, abi: :erc165, skip_docs: true

@behaviour __MODULE__

@callback erc165_interface_id() :: <<_::32>>

@interface_id Ethers.Utils.hex_decode!("0x01ffc9a7")

defmodule NotERC165CompatibleError do
defexception [:message]
end

@impl __MODULE__
def erc165_interface_id, do: @interface_id

@doc """
Prepares `supportsInterface(bytes4 interfaceId)` call parameters on the contract.
This function also accepts a module that implements the ERC165 behaviour as input. Example:
```elixir
iex> #{Macro.to_string(__MODULE__)}.supports_interface(Ethers.Contracts.ERC721)
#Ethers.TxData<function supportsInterface(...)>
```
This function should only be called for result and never in a transaction on
its own. (Use Ethers.call/2)
State mutability: view
## Function Parameter Types
- interfaceId: `{:bytes, 4}`
## Return Types (when called with `Ethers.call/2`)
- :bool
"""
@spec supports_interface(<<_::32>> | atom()) :: Ethers.TxData.t()
def supports_interface(module_or_interface_id)

def supports_interface(module) when is_atom(module) do
supports_interface(module.erc165_interface_id())
rescue
UndefinedFunctionError ->
reraise NotERC165CompatibleError,
"module #{module} does not implement ERC165 behaviour",
__STACKTRACE__
end
end
10 changes: 9 additions & 1 deletion lib/ethers/contracts/erc721.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ defmodule Ethers.Contracts.ERC721 do
@moduledoc """
ERC721 token interface
More info: https://ethereum.org/en/developers/docs/standards/tokens/erc-721/
More info: https://eips.ethereum.org/EIPS/eip-721
"""

use Ethers.Contract, abi: :erc721

@behaviour Ethers.Contracts.ERC165

# ERC-165 Interface ID
@interface_id Ethers.Utils.hex_decode!("0x80ac58cd")

@impl Ethers.Contracts.ERC165
def erc165_interface_id, do: @interface_id
end
23 changes: 23 additions & 0 deletions priv/abi/erc165.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"inputs":
[
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs":
[
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
}
]

0 comments on commit 78c0d09

Please sign in to comment.