Skip to content

Commit 5cbd5d0

Browse files
committed
Add a way to fetch all contract events
1 parent 7065f31 commit 5cbd5d0

File tree

5 files changed

+227
-0
lines changed

5 files changed

+227
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ filter = MyToken.EventFilters.transfer(from_address, nil)
116116

117117
# Get matching events
118118
{:ok, events} = Ethers.get_logs(filter)
119+
120+
# Get all events from a contract
121+
{:ok, events} = Ethers.get_logs_for_contract(MyToken.EventFilters, "0x123...")
119122
```
120123

121124
## Documentation

lib/ethers.ex

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,64 @@ defmodule Ethers do
584584
end
585585
end
586586

587+
@doc """
588+
Fetches event logs for all events in a contract's EventFilters module.
589+
590+
This function is useful when you want to get all events from a contract without
591+
specifying a single event filter. It will automatically decode each log using
592+
the appropriate event selector from the EventFilters module.
593+
594+
## Parameters
595+
- event_filters_module: The EventFilters module (e.g. `MyContract.EventFilters`)
596+
- address: The contract address to filter events from (nil means all contracts)
597+
598+
## Overrides and Options
599+
600+
- `:rpc_client`: The RPC Client to use. It should implement ethereum jsonRPC API. default: Ethereumex.HttpClient
601+
- `:rpc_opts`: Extra options to pass to rpc_client. (Like timeout, Server URL, etc.)
602+
- `:fromBlock` | `:from_block`: Minimum block number of logs to filter.
603+
- `:toBlock` | `:to_block`: Maximum block number of logs to filter.
604+
605+
## Examples
606+
607+
```elixir
608+
# Get all events from a contract
609+
{:ok, events} = Ethers.get_logs_for_contract(MyContract.EventFilters, "0x1234...")
610+
611+
# Get all events with block range
612+
{:ok, events} = Ethers.get_logs_for_contract(MyContract.EventFilters, "0x1234...",
613+
fromBlock: 1000,
614+
toBlock: 2000
615+
)
616+
```
617+
"""
618+
@spec get_logs_for_contract(module(), Types.t_address() | nil, Keyword.t()) ::
619+
{:ok, [Event.t()]} | {:error, atom()}
620+
def get_logs_for_contract(event_filters_module, address, overrides \\ []) do
621+
overrides = Keyword.put(overrides, :address, address)
622+
{opts, overrides} = Keyword.split(overrides, @option_keys)
623+
624+
{rpc_client, rpc_opts} = get_rpc_client(opts)
625+
626+
with {:ok, log_params} <-
627+
pre_process(event_filters_module, overrides, :get_logs_for_contract, opts) do
628+
rpc_client.eth_get_logs(log_params, rpc_opts)
629+
|> post_process(event_filters_module, :get_logs_for_contract)
630+
end
631+
end
632+
633+
@doc """
634+
Same as `Ethers.get_logs_for_contract/3` but raises on error.
635+
"""
636+
@spec get_logs_for_contract!(module(), Types.t_address() | nil, Keyword.t()) ::
637+
[Event.t()] | no_return
638+
def get_logs_for_contract!(event_filters_module, address, overrides \\ []) do
639+
case get_logs_for_contract(event_filters_module, address, overrides) do
640+
{:ok, events} -> events
641+
{:error, reason} -> raise ExecutionError, reason
642+
end
643+
end
644+
587645
@doc """
588646
Combines multiple requests and make a batch json RPC request.
589647
@@ -744,6 +802,21 @@ defmodule Ethers do
744802
end
745803
end
746804

805+
defp pre_process(_event_filters_module, overrides, :get_logs_for_contract, opts) do
806+
{address, _opts} = Keyword.pop(opts, :address)
807+
808+
log_params =
809+
overrides
810+
|> Enum.into(%{})
811+
|> ensure_hex_value(:fromBlock)
812+
|> ensure_hex_value(:from_block)
813+
|> ensure_hex_value(:toBlock)
814+
|> ensure_hex_value(:to_block)
815+
|> Map.put(:address, address)
816+
817+
{:ok, log_params}
818+
end
819+
747820
defp pre_process(event_filter, overrides, :get_logs, _opts) do
748821
log_params =
749822
event_filter
@@ -806,6 +879,23 @@ defmodule Ethers do
806879
{:ok, resp}
807880
end
808881

882+
defp post_process({:ok, resp}, event_filters_module, :get_logs_for_contract)
883+
when is_list(resp) do
884+
logs =
885+
Enum.flat_map(resp, fn log ->
886+
case Event.find_and_decode(log, event_filters_module) do
887+
{:ok, decoded_log} -> [decoded_log]
888+
{:error, :not_found} -> []
889+
end
890+
end)
891+
892+
{:ok, logs}
893+
end
894+
895+
defp post_process({:ok, resp}, _event_filters_module, :get_logs_for_contract) do
896+
{:ok, resp}
897+
end
898+
809899
defp post_process({:ok, %{"contractAddress" => contract_address}}, _tx_hash, :deployed_address)
810900
when not is_nil(contract_address),
811901
do: {:ok, contract_address}

test/ethers/counter_contract_test.exs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,117 @@ defmodule Ethers.CounterContractTest do
323323
end
324324
end
325325

326+
describe "get_logs_for_contract works with all events" do
327+
setup :deploy_counter_contract
328+
329+
test "can get all events from the contract", %{address: address} do
330+
{:ok, tx_hash_1} =
331+
CounterContract.set(101) |> Ethers.send_transaction(from: @from, to: address)
332+
333+
wait_for_transaction!(tx_hash_1)
334+
335+
{:ok, tx_hash_2} =
336+
CounterContract.reset() |> Ethers.send_transaction(from: @from, to: address)
337+
338+
wait_for_transaction!(tx_hash_2)
339+
340+
{:ok, current_block_number} = Ethers.current_block_number()
341+
342+
assert {:ok, events} =
343+
Ethers.get_logs_for_contract(CounterContract.EventFilters, address,
344+
from_block: current_block_number - 2,
345+
to_block: current_block_number
346+
)
347+
348+
assert length(events) == 2
349+
350+
[set_called_event, reset_called_event] = events
351+
352+
assert %Event{
353+
address: ^address,
354+
topics: ["SetCalled(uint256,uint256)", 100],
355+
data: [101]
356+
} = set_called_event
357+
358+
assert %Event{
359+
address: ^address,
360+
topics: ["ResetCalled()"],
361+
data: []
362+
} = reset_called_event
363+
end
364+
365+
test "can get all events with get_logs_for_contract! function", %{address: address} do
366+
{:ok, tx_hash_1} =
367+
CounterContract.set(101) |> Ethers.send_transaction(from: @from, to: address)
368+
369+
wait_for_transaction!(tx_hash_1)
370+
371+
{:ok, tx_hash_2} =
372+
CounterContract.reset() |> Ethers.send_transaction(from: @from, to: address)
373+
374+
wait_for_transaction!(tx_hash_2)
375+
376+
{:ok, current_block_number} = Ethers.current_block_number()
377+
378+
events =
379+
Ethers.get_logs_for_contract!(CounterContract.EventFilters, address,
380+
from_block: current_block_number - 2,
381+
to_block: current_block_number
382+
)
383+
384+
assert [
385+
%Ethers.Event{
386+
address: ^address,
387+
topics: ["SetCalled(uint256,uint256)", 100],
388+
data: [101]
389+
},
390+
%Ethers.Event{
391+
address: ^address,
392+
topics: ["ResetCalled()"],
393+
data: []
394+
}
395+
] = events
396+
end
397+
398+
test "can filter logs with from_block and to_block options", %{address: address} do
399+
{:ok, tx_hash_1} =
400+
CounterContract.set(101) |> Ethers.send_transaction(from: @from, to: address)
401+
402+
wait_for_transaction!(tx_hash_1)
403+
404+
{:ok, tx_hash_2} =
405+
CounterContract.reset() |> Ethers.send_transaction(from: @from, to: address)
406+
407+
wait_for_transaction!(tx_hash_2)
408+
409+
{:ok, current_block_number} = Ethers.current_block_number()
410+
411+
assert [
412+
%Ethers.Event{
413+
address: ^address,
414+
topics: ["SetCalled(uint256,uint256)", 100],
415+
data: [101],
416+
data_raw: "0x0000000000000000000000000000000000000000000000000000000000000065",
417+
log_index: 0,
418+
removed: false,
419+
transaction_hash: ^tx_hash_1,
420+
transaction_index: 0
421+
}
422+
] =
423+
Ethers.get_logs_for_contract!(CounterContract.EventFilters, address,
424+
from_block: current_block_number - 1,
425+
to_block: current_block_number - 1
426+
)
427+
end
428+
429+
test "returns empty list for non-existent contract address" do
430+
fake_address = "0x1234567890123456789012345678901234567890"
431+
432+
assert {:ok, []} =
433+
Ethers.get_logs_for_contract(CounterContract.EventFilters, fake_address)
434+
end
435+
end
436+
326437
describe "override block number" do
327438
setup :deploy_counter_contract
328439

test/ethers_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,23 @@ defmodule EthersTest do
352352
end
353353
end
354354

355+
describe "get_logs_for_contract/3" do
356+
test "returns error when request fails" do
357+
assert {:error, %{reason: :nxdomain}} =
358+
Ethers.get_logs_for_contract(HelloWorldContract.EventFilters, nil,
359+
rpc_opts: [url: "http://non.exists"]
360+
)
361+
end
362+
363+
test "with bang function, raises error when request fails" do
364+
assert_raise Mint.TransportError, "non-existing domain", fn ->
365+
Ethers.get_logs_for_contract!(HelloWorldContract.EventFilters, nil,
366+
rpc_opts: [url: "http://non.exists"]
367+
)
368+
end
369+
end
370+
end
371+
355372
describe "default address" do
356373
test "is included in the function calls when has default address" do
357374
assert %Ethers.TxData{

test/support/contracts/counter.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,11 @@ contract Counter {
2121
storeAmount = newAmount;
2222
}
2323

24+
function reset() public {
25+
delete storeAmount;
26+
emit ResetCalled();
27+
}
28+
29+
event ResetCalled();
2430
event SetCalled(uint256 indexed oldAmount, uint256 newAmount);
2531
}

0 commit comments

Comments
 (0)