feat: track state-change block and canceler on IndexingAgreement#7
Open
MoonBoi9001 wants to merge 1 commit intomb9/track-on-chain-rca-offersfrom
Open
Conversation
704daf4 to
b4c94d8
Compare
b4c94d8 to
60ec575
Compare
MoonBoi9001
added a commit
that referenced
this pull request
Apr 23, 2026
Drop the branches: [main] filter so stacked PRs whose base is another branch in the same repo (e.g. PR #7 stacked on this one) still get build-and-test runs. Keep the push trigger narrow to main so CI does not fire on every force-push across feature branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60ec575 to
9bafd57
Compare
Consumers that reconcile agreement state against the on-chain truth (dipper's chain_listener, primarily) need two things the aggregated IndexingAgreement entity does not currently expose: 1. A block-indexed marker of the most recent state change, so a poll can fetch only agreements that moved since the last seen block. Adds `lastStateChangeBlock: BigInt!` to the entity and stamps it with `event.block.number` on every handler that transitions state: AgreementAccepted / Canceled / Updated / RCACollected in recurringCollector.ts, and IndexingAgreementAccepted / Canceled / Updated in subgraphService.ts. Distinct from the existing `lastUpdatedAt` timestamp which is only written on AgreementUpdated. 2. The address that initiated a cancel, preserved as data rather than derived from the enum. Adds `canceledBy: Bytes!` to the entity and a new `handleIndexingAgreementCanceled` on the SubgraphService data source that reads `canceledOnBehalfOf` from the event. That captures operator-initiated cancels correctly -- the RecurringCollector's AgreementCanceled event only carries an enum (ServiceProvider or Payer) and the real signer is lost. Matchstick tests cover the new field stamping and the new cancel handler; existing tests gain `lastStateChangeBlock` assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9bafd57 to
8360209
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The
IndexingAgreemententity currently captures the full business state of an agreement -- payer, indexer, pricing, timestamps of major transitions -- but two gaps make it awkward to consume as a source of truth for state reconciliation.First, there is no block-indexed marker of the most recent state change. The existing
lastUpdatedAttimestamp is only written inhandleAgreementUpdated, so pollinglastUpdatedAt_gtwould miss accepts, cancels, and collections. Consumers that want a delta-style "what moved since block N" have to either iterate every entity on every poll or fall back to reading the event-log entities (which cuts against the aggregated-entity direction of PR #5).Second, when an agreement is canceled the entity records
state = CanceledByServiceProvider | CanceledByPayerbut loses the actual signer address. Most cancels come directly from the payer or the indexer so the enum is sufficient, but in production an operator can sign on behalf of either party and the enum collapses that to just the party category. Dipper's chain_listener compares the canceler address to its own signer address to decide whether a cancel was self-initiated or not -- without the address on the entity, that check has to fall back to the event-log entity.Summary
lastStateChangeBlock: BigInt!toIndexingAgreement. Every handler that transitions state stamps it withevent.block.number:handleAgreementAccepted/Canceled/Updated/RCACollectedon the RecurringCollector data source, andhandleIndexingAgreementAccepted/Canceled/Updatedon the SubgraphService data source.createOrLoadIndexingAgreementinitialises it to zero.canceledBy: Bytes!toIndexingAgreement, initialised toBytes.empty()and written in a newhandleIndexingAgreementCanceledhandler that readscanceledOnBehalfOffrom the SubgraphService event. The RecurringCollector handler continues to set thestateenum; neither handler overwrites the other's field.IndexingAgreementCanceledon the SubgraphService data source insubgraph.template.yaml.handleIndexingAgreementCanceledtest that verifiescanceledBycaptures a distinct operator address (not inferred from payer/indexer) and thatlastStateChangeBlockis stamped. Existing accept and update tests gainlastStateChangeBlockassertions.Both new fields are additive and backwards-compatible: existing consumers that don't read them see no change, and new consumers that filter on
lastStateChangeBlock_gtor readcanceledByget the delta-friendly shape they need.Generated with Claude Code