KeeperHub / KeeperHub/keeperhub
feat(web3): filter Query Contract Events by indexed event arguments
- Dominant language
- TypeScript
- Stars
- 24
- Forks
- 93
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 253
Description
### Before filing
- [x] I searched open and closed issues for this proposal.
- [x] I checked the docs and the current behaviour on `staging`.
- [x] This is one change, not several. (Several means several issues.)
### Reason: what you cannot do today
`query-events` cannot narrow a query by an indexed event argument. The filter is built with no arguments:
- `plugins/web3/steps/query-events-core.ts:52`: `contract.filters[eventName]?.()`
- Both the fixed batch (`:69`) and the tip batch (`:100`) pass that filter to `queryFilter`.
So a workflow that asks "Transfers of this token **to** address X over the last day" gets every Transfer on the token in that range, and has to filter afterwards.
This blocks a whole class of windowed checks that the existing steps otherwise already support. `query-events` followed by `math/aggregate` (operation `sum`, field path `args.value`) already computes "total transferred on this contract over N blocks". What cannot be computed is the same total scoped to one address, for example:
- net flow into a pool or treasury (sum of `Transfer(to = X)` minus sum of `Transfer(from = X)`);
- a wallet's outgoing transfers of one token;
- `Approval` events for one owner on one known token.
What told me to expect otherwise: `query-transactions`, the sibling history step, already takes optional argument filters (`functionArgs`, type `abi-function-args`, `plugins/web3/index.ts:1319-1326`, "Leave empty to match all calls"). The event selector also already displays which inputs are `indexed` (`components/workflow/config/abi-event-select-field.tsx:49-51`), but nothing lets the user bind them.
### Reason: what the workaround costs
- **Every event in the range is fetched,** then filtered in a `run-code` step. For a busy token, this is most of the token's activity for the window. [Fill in: event count for USDC `Transfer` over 6500 blocks with no filter vs. filtered to one address, from a local run.]
- **Batches fail on busy contracts.** Many hosted RPC providers cap the number of logs returned per `eth_getLogs` call. An unfiltered 2000-block batch on a busy contract can hit that cap, while the same batch filtered by `topic1` or `topic2` usually would not.
- **The workflow cannot be built without custom code.** A windowed, address-scoped metric needs `run-code` just to drop rows the RPC could have dropped.
### Scope: what this touches, and what it does not
Touches:
- `query-events` config: one new optional field.
- `query-events` step input and `query-events-core.ts` filter construction.
- A config renderer for the new field type (indexed inputs of the selected event only).
- Step docs, MCP action schema (if not derived automatically from the config), and unit tests.
Does not touch:
- the output shape (`events`, `fromBlock`, `toBlock`, `eventCount`, `error`);
- block-range resolution, batching, retry and tip-batch behaviour;
- `query-transactions` (already has argument filters);
- Solana `query-solana-program-events`;
- the event trigger;
- the address-less, cross-token scan and the indexer question in #2331;
- time-based lookback (a separate idea; not proposed here).
### Plan: what you propose
1. **Config.** Add an optional field `eventArgs` to `query-events`, placed after `eventName`, following the `functionArgs` precedent in `query-transactions`. It renders one input per **indexed** parameter of the selected event. Non-indexed parameters are not shown, since they cannot be filtered at the RPC. An empty input means "any". Help tip: "Optional: filter by indexed argument values. Leave empty to match all events."
2. **Filter construction.** In `resolveEventFilter`, build the argument list aligned to the event's inputs, with `null` for empty inputs and for non-indexed inputs, and pass it to `contract.filters[eventName](...args)`. The filter stays on the typed `Contract`, so decoding and the `EventLog` path at `query-events.ts:144` are unchanged.
3. **Validation, before any RPC call.** Values are checked against the parameter type (address, `uintN`/`intN`, `bool`, `bytesN`), and invalid values return a clear error naming the parameter. Template values (`{{...}}`) are resolved before validation, as in other steps.
4. **Dynamic indexed types** (`string`, `bytes`) are hashed by ethers into the topic. The docs will state that the match is on the hash of the value.
5. **Tests.**
- A unit test asserts the produced topics: for example, `Transfer(null, X)` gives an empty `topic1` and a padded `topic2`.
- An empty-filter test proves the current behaviour is unchanged.
- Validation error tests.
6. **Docs.** Update the Query Contract Events reference with the new field, the "empty means any" rule, and one example (Transfers into an address, summed with `math/aggregate`).
### Plan: alternatives you considered
- **Filter after fetch with a new "filter array" data step.** This leaves the RPC cost and provider log caps unsolved, which is the main problem.
- **Allow several values per topic (OR lists).** ethers supports this, but it widens the UI and validation. It can follow later without changing this field's single-value shape.
- **Relax the required contract address.** Ruled out in #2331: an address-less scan cannot use `Contract.queryFilter` and needs a separate path.
### Scope: compatibility
- [ ] Changes an existing response shape, status code, CLI flag, or default.
- [ ] Adds, removes, or upgrades a dependency.
- [ ] Changes database schema or requires a migration.
- [ ] Touches authentication, permissions, validation, or spend limits.
- [ ] Changes pricing, plan limits, or anything a user is charged.
Contributor guide
Research direction
Start with plugins/web3/steps/query-events-core.ts and the query-events input/config, then compare optional argument handling in plugins/web3/index.ts:1319-1326 and the indexed-input renderer at components/workflow/config/abi-event-select-field.tsx:49-51. Run the existing query-events unit tests and add coverage for topic construction, empty filters, and validation. Done means indexed arguments filter both batches, documentation and schemas describe the field, and existing output and behavior remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend-api-design, blockchain
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100