Skip to content

Commit b5ee972

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

File tree

5 files changed

+224
-0
lines changed

5 files changed

+224
-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: 87 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,18 @@ defmodule Ethers do
744802
end
745803
end
746804

805+
defp pre_process(_event_filters_module, overrides, :get_logs_for_contract, _opts) do
806+
log_params =
807+
overrides
808+
|> Enum.into(%{})
809+
|> ensure_hex_value(:fromBlock)
810+
|> ensure_hex_value(:from_block)
811+
|> ensure_hex_value(:toBlock)
812+
|> ensure_hex_value(:to_block)
813+
814+
{:ok, log_params}
815+
end
816+
747817
defp pre_process(event_filter, overrides, :get_logs, _opts) do
748818
log_params =
749819
event_filter
@@ -806,6 +876,23 @@ defmodule Ethers do
806876
{:ok, resp}
807877
end
808878

879+
defp post_process({:ok, resp}, event_filters_module, :get_logs_for_contract)
880+
when is_list(resp) do
881+
logs =
882+
Enum.flat_map(resp, fn log ->
883+
case Event.find_and_decode(log, event_filters_module) do
884+
{:ok, decoded_log} -> [decoded_log]
885+
{:error, :not_found} -> []
886+
end
887+
end)
888+
889+
{:ok, logs}
890+
end
891+
892+
defp post_process({:ok, resp}, _event_filters_module, :get_logs_for_contract) do
893+
{:ok, resp}
894+
end
895+
809896
defp post_process({:ok, %{"contractAddress" => contract_address}}, _tx_hash, :deployed_address)
810897
when not is_nil(contract_address),
811898
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)