diff --git a/README.md b/README.md index 6817705..73d5cf7 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,9 @@ filter = MyToken.EventFilters.transfer(from_address, nil) # Get matching events {:ok, events} = Ethers.get_logs(filter) + +# Get all events from a contract +{:ok, events} = Ethers.get_logs_for_contract(MyToken.EventFilters, "0x123...") ``` ## Documentation diff --git a/lib/ethers.ex b/lib/ethers.ex index d063a61..50a3c1e 100644 --- a/lib/ethers.ex +++ b/lib/ethers.ex @@ -584,6 +584,64 @@ defmodule Ethers do end end + @doc """ + Fetches event logs for all events in a contract's EventFilters module. + + This function is useful when you want to get all events from a contract without + specifying a single event filter. It will automatically decode each log using + the appropriate event selector from the EventFilters module. + + ## Parameters + - event_filters_module: The EventFilters module (e.g. `MyContract.EventFilters`) + - address: The contract address to filter events from (nil means all contracts) + + ## Overrides and Options + + - `:rpc_client`: The RPC Client to use. It should implement ethereum jsonRPC API. default: Ethereumex.HttpClient + - `:rpc_opts`: Extra options to pass to rpc_client. (Like timeout, Server URL, etc.) + - `:fromBlock` | `:from_block`: Minimum block number of logs to filter. + - `:toBlock` | `:to_block`: Maximum block number of logs to filter. + + ## Examples + + ```elixir + # Get all events from a contract + {:ok, events} = Ethers.get_logs_for_contract(MyContract.EventFilters, "0x1234...") + + # Get all events with block range + {:ok, events} = Ethers.get_logs_for_contract(MyContract.EventFilters, "0x1234...", + fromBlock: 1000, + toBlock: 2000 + ) + ``` + """ + @spec get_logs_for_contract(module(), Types.t_address() | nil, Keyword.t()) :: + {:ok, [Event.t()]} | {:error, atom()} + def get_logs_for_contract(event_filters_module, address, overrides \\ []) do + overrides = Keyword.put(overrides, :address, address) + {opts, overrides} = Keyword.split(overrides, @option_keys) + + {rpc_client, rpc_opts} = get_rpc_client(opts) + + with {:ok, log_params} <- + pre_process(event_filters_module, overrides, :get_logs_for_contract, opts) do + rpc_client.eth_get_logs(log_params, rpc_opts) + |> post_process(event_filters_module, :get_logs_for_contract) + end + end + + @doc """ + Same as `Ethers.get_logs_for_contract/3` but raises on error. + """ + @spec get_logs_for_contract!(module(), Types.t_address() | nil, Keyword.t()) :: + [Event.t()] | no_return + def get_logs_for_contract!(event_filters_module, address, overrides \\ []) do + case get_logs_for_contract(event_filters_module, address, overrides) do + {:ok, events} -> events + {:error, reason} -> raise ExecutionError, reason + end + end + @doc """ Combines multiple requests and make a batch json RPC request. @@ -744,6 +802,18 @@ defmodule Ethers do end end + defp pre_process(_event_filters_module, overrides, :get_logs_for_contract, _opts) do + log_params = + overrides + |> Enum.into(%{}) + |> ensure_hex_value(:fromBlock) + |> ensure_hex_value(:from_block) + |> ensure_hex_value(:toBlock) + |> ensure_hex_value(:to_block) + + {:ok, log_params} + end + defp pre_process(event_filter, overrides, :get_logs, _opts) do log_params = event_filter @@ -806,6 +876,23 @@ defmodule Ethers do {:ok, resp} end + defp post_process({:ok, resp}, event_filters_module, :get_logs_for_contract) + when is_list(resp) do + logs = + Enum.flat_map(resp, fn log -> + case Event.find_and_decode(log, event_filters_module) do + {:ok, decoded_log} -> [decoded_log] + {:error, :not_found} -> [] + end + end) + + {:ok, logs} + end + + defp post_process({:ok, resp}, _event_filters_module, :get_logs_for_contract) do + {:ok, resp} + end + defp post_process({:ok, %{"contractAddress" => contract_address}}, _tx_hash, :deployed_address) when not is_nil(contract_address), do: {:ok, contract_address} diff --git a/test/ethers/counter_contract_test.exs b/test/ethers/counter_contract_test.exs index c44eab7..a278162 100644 --- a/test/ethers/counter_contract_test.exs +++ b/test/ethers/counter_contract_test.exs @@ -323,6 +323,117 @@ defmodule Ethers.CounterContractTest do end end + describe "get_logs_for_contract works with all events" do + setup :deploy_counter_contract + + test "can get all events from the contract", %{address: 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() + + assert {:ok, events} = + Ethers.get_logs_for_contract(CounterContract.EventFilters, address, + from_block: current_block_number - 2, + to_block: current_block_number + ) + + assert length(events) == 2 + + [set_called_event, reset_called_event] = events + + 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 "can get all events with get_logs_for_contract! function", %{address: 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() + + events = + Ethers.get_logs_for_contract!(CounterContract.EventFilters, address, + from_block: current_block_number - 2, + to_block: current_block_number + ) + + assert [ + %Ethers.Event{ + address: ^address, + topics: ["SetCalled(uint256,uint256)", 100], + data: [101] + }, + %Ethers.Event{ + address: ^address, + topics: ["ResetCalled()"], + data: [] + } + ] = events + end + + test "can filter logs with from_block and to_block options", %{address: 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() + + assert [ + %Ethers.Event{ + address: ^address, + topics: ["SetCalled(uint256,uint256)", 100], + data: [101], + data_raw: "0x0000000000000000000000000000000000000000000000000000000000000065", + log_index: 0, + removed: false, + transaction_hash: ^tx_hash_1, + transaction_index: 0 + } + ] = + Ethers.get_logs_for_contract!(CounterContract.EventFilters, address, + from_block: current_block_number - 1, + to_block: current_block_number - 1 + ) + end + + test "returns empty list for non-existent contract address" do + fake_address = "0x1234567890123456789012345678901234567890" + + assert {:ok, []} = + Ethers.get_logs_for_contract(CounterContract.EventFilters, fake_address) + end + end + describe "override block number" do setup :deploy_counter_contract diff --git a/test/ethers_test.exs b/test/ethers_test.exs index 861d49e..28d6894 100644 --- a/test/ethers_test.exs +++ b/test/ethers_test.exs @@ -352,6 +352,23 @@ defmodule EthersTest do end end + describe "get_logs_for_contract/3" do + test "returns error when request fails" do + assert {:error, %{reason: :nxdomain}} = + Ethers.get_logs_for_contract(HelloWorldContract.EventFilters, nil, + rpc_opts: [url: "http://non.exists"] + ) + end + + test "with bang function, raises error when request fails" do + assert_raise Mint.TransportError, "non-existing domain", fn -> + Ethers.get_logs_for_contract!(HelloWorldContract.EventFilters, nil, + rpc_opts: [url: "http://non.exists"] + ) + end + end + end + describe "default address" do test "is included in the function calls when has default address" do assert %Ethers.TxData{ 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); }