Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ERC-165 support #115

Merged
merged 2 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
]
Loading