diff --git a/CHANGELOG.md b/CHANGELOG.md index f22c3f2..c8b4e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ### Enhancements +- Add combined event filters: fetch multiple events (OR semantics) in a single `eth_getLogs` + request with `Ethers.EventFilter.combine/1`, or all events of a contract by passing its + EventFilters module (e.g. `Ethers.get_logs(MyContract.EventFilters, address: "0x...")`) — + the resulting `Ethers.CombinedEventFilter` works with `Ethers.get_logs/2` and + `Ethers.batch/2` and decodes each fetched log with its matching event selector - Add [EIP-191](https://eips.ethereum.org/EIPS/eip-191) personal message support: hash, recover and verify messages with `Ethers.PersonalMessage`, and sign them via `Ethers.personal_sign/2` with the `Ethers.Signer.Local` and `Ethers.Signer.JsonRPC` signers through the new optional diff --git a/README.md b/README.md index 0853f1a..e1194a0 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,16 @@ filter = MyToken.EventFilters.transfer(from_address, nil) # Get matching events {:ok, events} = Ethers.get_logs(filter) + +# Combine multiple events in a single request (topics are OR-ed) +filter = + Ethers.EventFilter.combine([ + MyToken.EventFilters.transfer(nil, nil), + MyToken.EventFilters.approval(nil, nil) + ]) + +# Or fetch all events of a contract by passing its EventFilters module +{:ok, events} = Ethers.get_logs(MyToken.EventFilters, address: "0x...") ``` ## Documentation diff --git a/lib/ethers.ex b/lib/ethers.ex index ad3071a..2617e2f 100644 --- a/lib/ethers.ex +++ b/lib/ethers.ex @@ -36,7 +36,9 @@ defmodule Ethers do to the action data. This is only accepted for these actions and will through error on others. - `:call`: data should be a Ethers.TxData struct and overrides are accepted. - `:estimate_gas`: data should be a Ethers.TxData struct or a map and overrides are accepted. - - `:get_logs`: data should be a Ethers.EventFilter struct and overrides are accepted. + - `:get_logs`: data should be a Ethers.EventFilter struct, an Ethers.CombinedEventFilter + struct or an EventFilters module (to match all events of a contract) and overrides + are accepted. - `:send_transaction`: data should be a Ethers.TxData struct and overrides are accepted. @@ -60,6 +62,7 @@ defmodule Ethers do ``` """ + alias Ethers.CombinedEventFilter alias Ethers.Event alias Ethers.EventFilter alias Ethers.ExecutionError @@ -68,6 +71,8 @@ defmodule Ethers do alias Ethers.Types alias Ethers.Utils + import Ethers.Types, only: [is_module: 1] + @option_keys [:rpc_client, :rpc_opts, :signer, :signer_opts] @hex_decode_post_process [ :chain_id, @@ -687,6 +692,14 @@ defmodule Ethers do @doc """ Fetches the event logs with the given filter. + The filter can be one of: + + - A single event filter. (e.g. `MyContract.EventFilters.transfer(nil, nil)`) + - A combined event filter matching multiple events in one request, created with + `Ethers.EventFilter.combine/1`. + - An EventFilters module of a contract (e.g. `MyContract.EventFilters`) to match all + events of that contract. + ## Overrides and Options - `:address`: Indicates event emitter contract address. (nil means all contracts) @@ -695,8 +708,14 @@ defmodule Ethers do - `:fromBlock` | `:from_block`: Minimum block number of logs to filter. - `:toBlock` | `:to_block`: Maximum block number of logs to filter. """ - @spec get_logs(map(), Keyword.t()) :: {:ok, [Event.t()]} | {:error, atom()} - def get_logs(event_filter, overrides \\ []) do +@spec get_logs(map() | module(), Keyword.t()) :: {:ok, [Event.t()]} | {:error, term()} + def get_logs(event_filter, overrides \\ []) + + def get_logs(events_module, overrides) when is_module(events_module) do + get_logs(CombinedEventFilter.all!(events_module), overrides) + end + + def get_logs(event_filter, overrides) do {opts, overrides} = Keyword.split(overrides, @option_keys) {rpc_client, rpc_opts} = get_rpc_client(opts) @@ -710,7 +729,7 @@ defmodule Ethers do @doc """ Same as `Ethers.get_logs/2` but raises on error. """ - @spec get_logs!(map(), Keyword.t()) :: [Event.t()] | no_return + @spec get_logs!(map() | module(), Keyword.t()) :: [Event.t()] | no_return def get_logs!(params, overrides \\ []) do case get_logs(params, overrides) do {:ok, logs} -> logs @@ -886,6 +905,8 @@ defmodule Ethers do |> ensure_hex_value(:from_block) |> ensure_hex_value(:toBlock) |> ensure_hex_value(:to_block) + |> rename_key(:from_block, :fromBlock) + |> rename_key(:to_block, :toBlock) {:ok, log_params} end @@ -930,6 +951,11 @@ defmodule Ethers do Utils.hex_to_integer(gas_hex) end + defp post_process({:ok, resp}, %CombinedEventFilter{} = combined_filter, :get_logs) + when is_list(resp) do + {:ok, CombinedEventFilter.decode_logs(resp, combined_filter)} + end + defp post_process({:ok, resp}, event_filter, :get_logs) when is_list(resp) do logs = Enum.map(resp, &Event.decode(&1, event_filter.selector)) @@ -993,11 +1019,29 @@ defmodule Ethers do end end + defp rename_key(params, key, new_key) do + case Map.pop(params, key) do + {nil, params} -> params + {value, params} -> Map.put(params, new_key, value) + end + end + defp prepare_requests(requests) do Enum.map(requests, fn - {action, data} -> {action, data, []} - {action, data, overrides} -> {action, data, overrides} - action when is_atom(action) -> {action, [], []} + {:get_logs, events_module} when is_module(events_module) -> + {:get_logs, CombinedEventFilter.all!(events_module), []} + + {:get_logs, events_module, overrides} when is_module(events_module) -> + {:get_logs, CombinedEventFilter.all!(events_module), overrides} + + {action, data} -> + {action, data, []} + + {action, data, overrides} -> + {action, data, overrides} + + action when is_atom(action) -> + {action, [], []} end) end diff --git a/lib/ethers/combined_event_filter.ex b/lib/ethers/combined_event_filter.ex new file mode 100644 index 0000000..463a4ba --- /dev/null +++ b/lib/ethers/combined_event_filter.ex @@ -0,0 +1,240 @@ +defmodule Ethers.CombinedEventFilter do + @moduledoc """ + A filter matching any of multiple events (OR semantics) in a single `eth_getLogs` request. + + Combined event filters are created with `Ethers.EventFilter.combine/1` and can be used + anywhere a regular event filter is accepted (e.g. `Ethers.get_logs/2` and + `Ethers.batch/2`). To match all events of a contract, pass its EventFilters module + directly to `Ethers.get_logs/2` (or `Ethers.EventFilter.combine/1`) which internally + translates it to a combined filter of all the contract events. + + The topics of the combined filters are sent to the RPC endpoint as an OR-ed list of + `topic_0` values so filtering happens server side. Fetched logs are decoded using the + event selector matching their first topic. + + ## Limitations + + - Only filters with wildcard indexed arguments (all `nil`) can be combined. Combining + filters with indexed-argument values would create cross-product OR semantics on the + topics and match unintended logs. + - All combined filters must belong to the same address. Filters with conflicting + default addresses cannot be combined since `eth_getLogs` accepts a single address. + + ## Examples + + ```elixir + filter = + Ethers.EventFilter.combine([ + Ethers.Contracts.ERC20.EventFilters.transfer(nil, nil), + Ethers.Contracts.ERC20.EventFilters.approval(nil, nil) + ]) + + Ethers.get_logs(filter, address: "0x...") + {:ok, [%Ethers.Event{...}, ...]} + + # Or fetch all events of a contract by passing its EventFilters module + Ethers.get_logs(Ethers.Contracts.ERC20.EventFilters, address: "0x...") + {:ok, [%Ethers.Event{...}, ...]} + ``` + """ + + alias Ethers.ContractHelpers + alias Ethers.Event + alias Ethers.EventFilter + + import Ethers.Types, only: [is_module: 1] + + @typedoc """ + Holds the combined topics (a single OR-ed list of `topic_0` values), the event + selectors indexed by their `topic_0` and the default address. + """ + @type t :: %__MODULE__{ + topics: [[binary()]], + selectors: %{binary() => ABI.FunctionSelector.t()}, + default_address: nil | Ethers.Types.t_address() + } + + @enforce_keys [:topics, :selectors] + defstruct [:topics, :selectors, :default_address] + + @doc false + @spec new([EventFilter.t() | module()] | module()) :: t() + def new(events_module) when is_module(events_module), do: new([events_module]) + + def new(filters_or_modules) when is_list(filters_or_modules) do + case Enum.flat_map(filters_or_modules, &expand_filters/1) do + [] -> + raise ArgumentError, "cannot combine an empty list of event filters" + + event_filters -> + Enum.each(event_filters, &validate_filter!/1) + + {topic_0s, selectors} = Enum.reduce(event_filters, {[], %{}}, &collect_topic_0/2) + + %__MODULE__{ + topics: [Enum.reverse(topic_0s)], + selectors: selectors, + default_address: combined_default_address!(event_filters) + } + end + end + + @doc false + @spec from_events_module(module()) :: t() + def from_events_module(events_module), do: new([events_module]) + + @doc false + @spec all!(module()) :: t() + def all!(events_module) when is_module(events_module) do + if Code.ensure_loaded?(events_module) and function_exported?(events_module, :__all__, 0) do + events_module.__all__() + else + raise ArgumentError, + "#{inspect(events_module)} is not an EventFilters module of an Ethers contract" + end + end + + @doc false + @spec to_map(t(), Keyword.t()) :: map() + def to_map(%__MODULE__{} = combined_filter, overrides) do + %{topics: combined_filter.topics} + |> maybe_add_address(combined_filter.default_address) + |> then(&Enum.into(overrides, &1)) + end + + @doc """ + Decodes fetched logs using the matching event selectors of the combined filter. + + Logs not matching any of the combined events are discarded. (Cannot happen with logs + fetched using the combined filter itself, since the RPC endpoint only returns logs + matching the requested topics) + """ + @spec decode_logs([map()], t()) :: [Event.t()] + def decode_logs(logs, %__MODULE__{selectors: selectors}) when is_list(logs) do + Enum.flat_map(logs, fn log -> + [topic_0 | _] = Map.fetch!(log, "topics") + + case Map.fetch(selectors, String.downcase(topic_0)) do + {:ok, selector} -> [Event.decode(log, selector)] + :error -> [] + end + end) + end + + defp expand_filters(%EventFilter{} = filter), do: [filter] + + defp expand_filters(events_module) when is_module(events_module) do + unless Code.ensure_loaded?(events_module) and + function_exported?(events_module, :__events__, 0) do + raise ArgumentError, + "#{inspect(events_module)} is not an EventFilters module of an Ethers contract" + end + + case events_module.__events__() do + [] -> + raise ArgumentError, "#{inspect(events_module)} does not have any events" + + selectors -> + Enum.map(selectors, &wildcard_filter(&1, events_module.__default_address__())) + end + end + + defp expand_filters(other) do + raise ArgumentError, + "expected an Ethers.EventFilter struct or an EventFilters module, got: #{inspect(other)}" + end + + defp wildcard_filter(selector, default_address) do + wildcard_args = Enum.map(ContractHelpers.event_indexed_types(selector), fn _ -> nil end) + + selector + |> ContractHelpers.encode_event_topics(wildcard_args) + |> EventFilter.new(selector, default_address) + end + + defp collect_topic_0(%EventFilter{topics: [topic_0 | _]} = filter, {topic_0s, selectors}) do + topic_0 = String.downcase(topic_0) + + if Map.has_key?(selectors, topic_0) do + {topic_0s, selectors} + else + {[topic_0 | topic_0s], Map.put(selectors, topic_0, filter.selector)} + end + end + + defp validate_filter!(%EventFilter{topics: [_topic_0 | sub_topics]} = filter) do + if Enum.all?(sub_topics, &is_nil/1) do + :ok + else + raise ArgumentError, + "cannot combine event filter with indexed-argument values: #{inspect(filter)}" <> + " (use nil as a wildcard for all indexed arguments)" + end + end + + defp combined_default_address!(event_filters) do + event_filters + |> Enum.map(& &1.default_address) + |> Enum.reject(&is_nil/1) + |> Enum.uniq_by(&String.downcase/1) + |> case do + [] -> + nil + + [address] -> + address + + addresses -> + raise ArgumentError, + "cannot combine event filters with conflicting default addresses: " <> + "#{inspect(addresses)} (eth_getLogs accepts a single address," <> + " combine filters of the same contract)" + end + end + + defp maybe_add_address(map, nil), do: map + defp maybe_add_address(map, address), do: Map.put(map, :address, address) + + defimpl Inspect do + import Inspect.Algebra + + def inspect( + %{topics: [topic_0s], selectors: selectors, default_address: default_address}, + opts + ) do + default_address = + case default_address do + nil -> + [] + + _ -> + [ + line(), + color("default_address: ", :default, opts), + color(inspect(default_address), :string, opts) + ] + end + + events = + topic_0s + |> Enum.map(&Map.fetch!(selectors, &1)) + |> Enum.map(fn selector -> + concat([ + line(), + color("event", :atom, opts), + " ", + color(ABI.FunctionSelector.encode(selector), :call, opts) + ]) + end) + + inner = concat(events ++ default_address) + + concat([ + color("#Ethers.CombinedEventFilter<", :map, opts), + nest(inner, 2), + break(""), + color(">", :map, opts) + ]) + end + end +end diff --git a/lib/ethers/contract.ex b/lib/ethers/contract.ex index 800c5d5..385c883 100644 --- a/lib/ethers/contract.ex +++ b/lib/ethers/contract.ex @@ -138,6 +138,12 @@ defmodule Ethers.Contract do defdelegate __default_address__, to: unquote(module) unquote(events_impl) + @doc false + @spec __all__() :: Ethers.CombinedEventFilter.t() + def __all__ do + Ethers.CombinedEventFilter.from_events_module(__MODULE__) + end + def __events__, do: unquote(Macro.escape(event_selectors)) end end diff --git a/lib/ethers/event_filter.ex b/lib/ethers/event_filter.ex index 6865bd2..fa1d85c 100644 --- a/lib/ethers/event_filter.ex +++ b/lib/ethers/event_filter.ex @@ -27,14 +27,58 @@ defmodule Ethers.EventFilter do } end + @doc """ + Combines multiple event filters into a single `Ethers.CombinedEventFilter` matching + any of the given events (OR semantics) in one `eth_getLogs` request. + + The combined filter can be used anywhere a regular event filter is accepted + (e.g. `Ethers.get_logs/2` and `Ethers.batch/2`). Filtering happens server side using + an OR-ed list of `topic_0` values and each fetched log is decoded using its matching + event selector. + + Accepts event filters as well as EventFilters modules of contracts, which translate + to all events of that contract. (e.g. `Ethers.Contracts.ERC20.EventFilters`) + + ## Rules + + Raises `ArgumentError` if: + + - The list of filters is empty. + - Any filter has indexed-argument values. Only wildcard filters (all indexed + arguments set to `nil`) can be combined, since `eth_getLogs` topics are positional + and OR-ing them across events would match unintended logs. + - Filters have conflicting default addresses. `eth_getLogs` accepts a single address, + so all combined filters must belong to the same contract. + + ## Examples + + filter = + Ethers.EventFilter.combine([ + Ethers.Contracts.ERC20.EventFilters.transfer(nil, nil), + Ethers.Contracts.ERC20.EventFilters.approval(nil, nil) + ]) + + Ethers.get_logs(filter, address: "0x...") + #=> {:ok, [%Ethers.Event{...}, ...]} + + # All events of a contract + Ethers.EventFilter.combine([Ethers.Contracts.ERC20.EventFilters]) + """ + @spec combine([t() | module()] | module()) :: Ethers.CombinedEventFilter.t() + defdelegate combine(event_filters), to: Ethers.CombinedEventFilter, as: :new + @doc false - @spec to_map(t() | map(), Keyword.t()) :: map() + @spec to_map(t() | Ethers.CombinedEventFilter.t() | map(), Keyword.t()) :: map() def to_map(%__MODULE__{} = tx_data, overrides) do tx_data |> event_filter_map() |> to_map(overrides) end + def to_map(%Ethers.CombinedEventFilter{} = combined_filter, overrides) do + Ethers.CombinedEventFilter.to_map(combined_filter, overrides) + end + def to_map(event_filter, overrides) do Enum.into(overrides, event_filter) end diff --git a/lib/ethers/types.ex b/lib/ethers/types.ex index 47a634f..c551101 100644 --- a/lib/ethers/types.ex +++ b/lib/ethers/types.ex @@ -54,6 +54,11 @@ defmodule Ethers.Types do defguardp valid_bitsize(bitsize) when bitsize >= 8 and bitsize <= 256 and rem(bitsize, 8) == 0 defguardp valid_bytesize(bytesize) when bytesize >= 1 and bytesize <= 32 + @doc """ + Guard to check if the given term can be a module name. (an atom excluding `nil` and booleans) + """ + defguard is_module(term) when is_atom(term) and term not in [nil, true, false] + @doc """ Converts EVM data types to typespecs for documentation """ diff --git a/test/ethers/counter_contract_test.exs b/test/ethers/counter_contract_test.exs index c44eab7..90023b0 100644 --- a/test/ethers/counter_contract_test.exs +++ b/test/ethers/counter_contract_test.exs @@ -323,6 +323,167 @@ defmodule Ethers.CounterContractTest do end end + describe "combined event filters" do + setup :deploy_counter_contract + + test "can fetch multiple events in a single request", %{address: address} do + %{block_range: block_range} = emit_set_and_reset(address) + + filter = + Ethers.EventFilter.combine([ + CounterContract.EventFilters.set_called(nil), + CounterContract.EventFilters.reset_called() + ]) + + assert {:ok, [set_called_event, reset_called_event]} = + Ethers.get_logs(filter, [address: address] ++ block_range) + + assert %Event{ + address: ^address, + topics: ["SetCalled(uint256,uint256)", 100], + data: [101] + } = set_called_event + + assert %Event{ + address: ^address, + topics: ["ResetCalled()"], + data: [] + } = reset_called_event + end + + test "passing an EventFilters module matches all events of the contract", %{address: address} do + %{block_range: block_range} = emit_set_and_reset(address) + + assert [ + %Event{topics: ["SetCalled(uint256,uint256)", 100], data: [101]}, + %Event{topics: ["ResetCalled()"], data: []} + ] = + Ethers.get_logs!( + CounterContract.EventFilters, + [address: address] ++ block_range + ) + end + + test "combine accepts EventFilters modules", %{address: address} do + %{block_range: block_range} = emit_set_and_reset(address) + + filter = Ethers.EventFilter.combine([CounterContract.EventFilters]) + + assert filter == Ethers.EventFilter.combine(CounterContract.EventFilters) + assert filter == CounterContract.EventFilters.__all__() + + assert {:ok, [%Event{}, %Event{}]} = + Ethers.get_logs(filter, [address: address] ++ block_range) + end + + test "only fetches logs of the combined events", %{address: address} do + %{block_range: block_range} = emit_set_and_reset(address) + + filter = Ethers.EventFilter.combine([CounterContract.EventFilters.set_called(nil)]) + + assert {:ok, [%Event{topics: ["SetCalled(uint256,uint256)", 100]}]} = + Ethers.get_logs(filter, [address: address] ++ block_range) + end + + test "works with batch requests", %{address: address} do + %{block_range: block_range} = emit_set_and_reset(address) + + filter = + Ethers.EventFilter.combine([ + CounterContract.EventFilters.set_called(nil), + CounterContract.EventFilters.reset_called() + ]) + + assert {:ok, [ok: [set_called_event, reset_called_event], ok: [_, _]]} = + Ethers.batch([ + {:get_logs, filter, [address: address] ++ block_range}, + {:get_logs, CounterContract.EventFilters, [address: address] ++ block_range} + ]) + + assert %Event{topics: ["SetCalled(uint256,uint256)", 100]} = set_called_event + assert %Event{topics: ["ResetCalled()"]} = reset_called_event + end + + test "uses the default address of the combined filters", %{address: address} do + %{block_range: block_range} = emit_set_and_reset(address) + + filter = + Ethers.EventFilter.combine([ + %{CounterContract.EventFilters.set_called(nil) | default_address: address}, + %{CounterContract.EventFilters.reset_called() | default_address: address} + ]) + + assert filter.default_address == address + + assert {:ok, [%Event{address: ^address}, %Event{address: ^address}]} = + Ethers.get_logs(filter, block_range) + end + + test "deduplicates repeated event filters" do + filter = + Ethers.EventFilter.combine([ + CounterContract.EventFilters.set_called(nil), + CounterContract.EventFilters.set_called(nil) + ]) + + assert [[_topic_0]] = filter.topics + end + + test "combining filters with indexed-argument values raises" do + assert_raise ArgumentError, ~r/indexed-argument values/, fn -> + Ethers.EventFilter.combine([CounterContract.EventFilters.set_called(100)]) + end + end + + test "combining an empty list raises" do + assert_raise ArgumentError, "cannot combine an empty list of event filters", fn -> + Ethers.EventFilter.combine([]) + end + end + + test "combining filters with conflicting default addresses raises" do + assert_raise ArgumentError, ~r/conflicting default addresses/, fn -> + Ethers.EventFilter.combine([ + %{ + CounterContract.EventFilters.set_called(nil) + | default_address: "0x1000bf6a479f320ead074411a4b0e7944ea8c9c1" + }, + %{ + CounterContract.EventFilters.reset_called() + | default_address: "0x2000bf6a479f320ead074411a4b0e7944ea8c9c2" + } + ]) + end + end + + test "combining non event filters raises" do + assert_raise ArgumentError, ~r/is not an EventFilters module/, fn -> + Ethers.EventFilter.combine([:not_a_module]) + end + + assert_raise ArgumentError, ~r/expected an Ethers.EventFilter struct/, fn -> + Ethers.EventFilter.combine([%{some: :map}]) + end + end + + test "get_logs with a non EventFilters module raises" do + assert_raise ArgumentError, ~r/is not an EventFilters module/, fn -> + Ethers.get_logs(:not_a_module) + end + end + + test "renders the correct values when inspected" do + filter = + Ethers.EventFilter.combine([ + CounterContract.EventFilters.set_called(nil), + CounterContract.EventFilters.reset_called() + ]) + + assert "#Ethers.CombinedEventFilter<\n event SetCalled(uint256,uint256)\n event ResetCalled()>" == + inspect(filter) + end + end + describe "override block number" do setup :deploy_counter_contract @@ -351,6 +512,22 @@ defmodule Ethers.CounterContractTest do end end + defp emit_set_and_reset(address) do + {:ok, tx_hash_1} = + CounterContract.set(101) |> Ethers.send_transaction(from: @from, to: address) + + wait_for_transaction!(tx_hash_1) + + {:ok, tx_hash_2} = + CounterContract.reset() |> Ethers.send_transaction(from: @from, to: address) + + wait_for_transaction!(tx_hash_2) + + {:ok, current_block_number} = Ethers.current_block_number() + + %{block_range: [from_block: current_block_number - 2, to_block: current_block_number]} + end + defp deploy_counter_contract(_ctx) do encoded_constructor = CounterContract.constructor(100) diff --git a/test/ethers_test.exs b/test/ethers_test.exs index f87b383..adc7c24 100644 --- a/test/ethers_test.exs +++ b/test/ethers_test.exs @@ -355,6 +355,17 @@ defmodule EthersTest do ) end end + + test "with combined event filter, returns error when request fails" do + filter = Ethers.EventFilter.combine([HelloWorldContract.EventFilters.hello_set()]) + + assert {:error, %{reason: :nxdomain}} = + Ethers.get_logs(filter, rpc_opts: [url: "http://non.exists"]) + + assert_raise Finch.TransportError, "non-existing domain", fn -> + Ethers.get_logs!(filter, rpc_opts: [url: "http://non.exists"]) + end + end end describe "default address" do @@ -432,6 +443,29 @@ defmodule EthersTest do |> Ethers.EventFilter.to_map([]) end + test "is included in combined event filters when has default address" do + filter = HelloWorldWithDefaultAddressContract.EventFilters.__all__() + + assert %Ethers.CombinedEventFilter{ + default_address: "0x1000bf6a479f320ead074411a4b0e7944ea8c9c1" + } = filter + + assert %{ + topics: [["0xbe6cf5e99b344c66895d6304d442b2f51b6359ee51ac581db2255de9373e24b8"]], + address: "0x1000bf6a479f320ead074411a4b0e7944ea8c9c1" + } == Ethers.EventFilter.to_map(filter, []) + end + + test "is not included in combined event filters when does not have default address" do + filter = Ethers.EventFilter.combine(HelloWorldContract.EventFilters) + + assert %Ethers.CombinedEventFilter{default_address: nil} = filter + + assert %{ + topics: [["0xbe6cf5e99b344c66895d6304d442b2f51b6359ee51ac581db2255de9373e24b8"]] + } == Ethers.EventFilter.to_map(filter, []) + end + test "is not included in event filters when does not have default address" do assert %Ethers.EventFilter{ topics: [ diff --git a/test/support/contracts/counter.sol b/test/support/contracts/counter.sol index 9150e8b..825f8bc 100644 --- a/test/support/contracts/counter.sol +++ b/test/support/contracts/counter.sol @@ -21,5 +21,11 @@ contract Counter { storeAmount = newAmount; } + function reset() public { + delete storeAmount; + emit ResetCalled(); + } + + event ResetCalled(); event SetCalled(uint256 indexed oldAmount, uint256 newAmount); }