Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 87 additions & 0 deletions lib/ethers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
111 changes: 111 additions & 0 deletions test/ethers/counter_contract_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions test/ethers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
6 changes: 6 additions & 0 deletions test/support/contracts/counter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ contract Counter {
storeAmount = newAmount;
}

function reset() public {
delete storeAmount;
emit ResetCalled();
}

event ResetCalled();
event SetCalled(uint256 indexed oldAmount, uint256 newAmount);
}