Support solidity events and add high-level filter RPC wrappers - #86
Conversation
kubo39
commented
Feb 11, 2026
- Add FilterID type for type-safe filter ID handling
- Add newFilter, getFilterChanges, getFilterLogs, uninstallFilter, newBlockFilter, newPendingTransactionFilter wrappers to RPCConnector
- Fix convTo!LogsResponse: Nullable!(Log[]) doesn't support ~=
- Add unit tests for all filter RPC wrappers
There was a problem hiding this comment.
Pull request overview
This PR adds higher-level, type-oriented wrappers around the Ethereum JSON-RPC filter APIs to RPCConnector, introduces a FilterID type for safer filter identifier handling, and fixes convTo!LogsResponse to work with Nullable!(Log[]).
Changes:
- Add
FilterIDstruct to represent filter IDs returned byeth_newFilter/eth_newBlockFilter/eth_newPendingTransactionFilter. - Add
RPCConnectorwrapper methods for creating filters and fetching/uninstalling them. - Fix
convTo!LogsResponseaccumulation logic and add mock-based unit tests for several of the new wrappers.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| source/deth/util/types.d | Fixes convTo!LogsResponse and adds the FilterID type. |
| source/deth/rpcconnector.d | Adds filter RPC wrapper methods and associated mock-based unit tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -296,9 +296,11 @@ To convTo(To, _From)(const _From f) @safe pure | |||
| { | |||
| log.blockTimestamp = elem[`blockTimestamp`].str[2 .. $].to!ulong(16); | |||
| } | |||
| logs ~= log; | |||
| logArray ~= log; | |||
| } | |||
| }(); | |||
| LogsResponse logs; | |||
| logs.logs = logArray; | |||
| return logs; | |||
There was a problem hiding this comment.
convTo!LogsResponse builds logArray via repeated ~= appends inside a loop, which causes repeated reallocations for large log sets. Since f.array.length is known, consider pre-sizing the array or using an appender/indexed assignment to avoid quadratic growth behavior.
| Log[] getFilterChanges(FilterID filterID) @trusted | ||
| { | ||
| JSONValue rawResponse = eth_getFilterChanges(filterID.id); | ||
| if (rawResponse.isNull) | ||
| return []; | ||
| return rawResponse.convTo!LogsResponse.get; | ||
| } |
There was a problem hiding this comment.
getFilterChanges always attempts to decode the response as log objects (LogsResponse). Per the execution-api spec, eth_getFilterChanges returns different shapes depending on filter type (log filter => log objects, block filter => block hashes, pending tx filter => tx hashes). Since this PR also adds newBlockFilter/newPendingTransactionFilter, calling getFilterChanges with those filter IDs will fail to parse. Consider splitting this into typed wrappers (e.g., one returning Log[] for eth_newFilter IDs and another returning Hash[]/bytes[] for block/pending filters), or returning a SumType/raw JSONValue so callers can handle the variant response safely.
There was a problem hiding this comment.
We'll implement the more high-level approach later, such as:
| API | based on | retuning |
|-----|----------------|------------------|
| `watchLogs(filter)` | `eth_newFilter` | `Log[]` |
| `watchBlocks()` | `eth_newBlockFilter` | `Hash[]` |
| `watchPendingTransactions()` | `eth_newPendingTransactionFilter` | `Hash[]` |
| jtx["address"] = filter.address.get.convTo!string.ox; | ||
| } | ||
| if (!filter.topics.isNull) | ||
| jtx["topics"] = filter.topics.get; |
There was a problem hiding this comment.
topics is being assigned as Hash[] (ubyte[32][]) directly into a JSONValue. The JSON-RPC schema expects topics as 0x-prefixed 32-byte hex strings (or arrays thereof), so serializing raw bytes here will not match what execution clients accept. Convert each topic hash to a hex string before assigning to jtx["topics"].
| jtx["topics"] = filter.topics.get; | |
| { | |
| auto topicStrings = filter.topics.get | |
| .map!(t => "0x" ~ toHexString(t[])) | |
| .array; | |
| jtx["topics"] = topicStrings; | |
| } |
| JSONValue jtx; | ||
| if (!filter.from.isNull) | ||
| { | ||
| const block = filter.from.get; | ||
| mixin BlockNumberToJSON!block; | ||
| jtx["fromBlock"] = _block; | ||
| } | ||
| if (!filter.to.isNull) | ||
| { | ||
| const block = filter.to.get; | ||
| mixin BlockNumberToJSON!block; | ||
| jtx["toBlock"] = _block; | ||
| } |
There was a problem hiding this comment.
newFilter encodes block bounds using fromBlock/toBlock, but the existing getLogs wrapper in this same file builds the JSON with keys from/to (see getLogs around lines ~255-275). Since both use LogFilter, this inconsistency is likely to break one of the calls and is hard to maintain. Consider extracting a shared helper that builds the filter JSON and uses the execution-api field names consistently (typically fromBlock/toBlock).
| /// Wrapper for eth_getFilterLogs. | ||
| /// | ||
| /// https://ethereum.github.io/execution-apis/api/methods/eth_getFilterLogs/ | ||
| Log[] getFilterLogs(FilterID filterID) @trusted | ||
| { | ||
| JSONValue rawResponse = eth_getFilterLogs(filterID.id); | ||
| if (rawResponse.isNull) | ||
| return []; | ||
| return rawResponse.convTo!LogsResponse.get; | ||
| } |
There was a problem hiding this comment.
A wrapper for eth_getFilterLogs was added, but there is no corresponding mock-based unit test verifying that it correctly decodes log objects and calls eth_getFilterLogs (similar to the getFilterChanges tests below). Adding a unit test here would prevent regressions in JSON decoding and method wiring.
- Add FilterID type for type-safe filter ID handling - Add newFilter, getFilterChanges, getFilterLogs, uninstallFilter, newBlockFilter, newPendingTransactionFilter wrappers to RPCConnector - Fix convTo!LogsResponse: Nullable!(Log[]) doesn't support ~= - Add unit tests for all filter RPC wrappers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
64ae646 to
9cd7a20
Compare
- Add LogFilterWatcher, BlockFilterWatcher, PendingTxFilterWatcher for HTTP polling-based event/block/tx monitoring via getChanges() - Add watchLogs/watchBlocks/watchPendingTransactions factory methods to RPCConnector - Add getFilterChangesHashes for block/pending tx filters returning Hash[] - Add compile-time event code generation (allEvents) that produces typed event structs and decode methods from contract ABI - Add dataInputTypes to ContractEvent, fix eventFromJson to parse indexed/non-indexed types with explicit loop (CTFE compatible) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add NumberChanged event to Counter contract and demonstrate event watching with watchLogs/getChanges/decodeNumberChangedEvent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>