feat: add RecurringCollector data source and Offer entity#4
Open
MoonBoi9001 wants to merge 2 commits intofeat/aggregated-indexing-agreementfrom
Open
feat: add RecurringCollector data source and Offer entity#4MoonBoi9001 wants to merge 2 commits intofeat/aggregated-indexing-agreementfrom
MoonBoi9001 wants to merge 2 commits intofeat/aggregated-indexing-agreementfrom
Conversation
Adds a second data source for RecurringCollector alongside the existing SubgraphService data source, with a handleOfferStored mapping that writes both an immutable OfferStored log entity and a mutable Offer entity keyed by agreementId. The Offer entity lets indexer-service verify via subgraph query that a stored RCA offer exists on-chain before accepting a DIPs proposal with an empty signature. This replaces the current signature-based authorization path: indexer-service will compute hashRCA(rca) locally and check it matches the on-chain offerHash exposed here. The RecurringCollector contract does not emit any cleanup event when rcaOffers[agreementId] is consumed on accept, so the Offer entity is write-only by design -- downstream consumers that need "still pending" semantics must cross-reference IndexingAgreementAccepted. Config files now split the contract address into subgraphServiceAddress and recurringCollectorAddress. The local-network deploy pipeline will need to pull the RecurringCollector address from horizon.json alongside the existing SubgraphService substitution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Offer entity was previously mutable with "latest offer hash wins" semantics, which didn't match how the entity is actually used. For a given agreementId, the RCA identifying fields (payer, dataService, serviceProvider, deadline, nonce) are fixed by the id derivation, so any duplicate OfferStored event for the same id carries the same offerHash by construction. There is nothing meaningful to overwrite. Switch the entity to @entity(immutable: true) and early-return from handleOfferStored when an entity with the same id already exists. Writing twice to an immutable entity would halt the subgraph, so the guard is load-bearing against dipper crash-recovery re-submissions and chain reorg re-emissions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
530e44a to
dafb4e1
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
Dipper submits
RecurringCollector.offer()on-chain once indexer-service accepts a DIPs proposal. If dipper crashes or restarts after that transaction lands but before the agreement ID is persisted locally, a naive restart would re-submit the sameoffer()and double-spend gas. Dipper needs an authoritative view of "did my prioroffer()land?" that survives its own crash. Querying the subgraph for anOfferentity keyed byagreementIdis the natural answer — the indexing-payments-subgraph already indexes other agreement lifecycle state that dipper consumes, so reusing it keeps the idempotency check in the same data layer rather than bolting on a JSON-RPC dependency.This PR stacks on top of #5 (Maikol's consolidated
IndexingAgreemententity model). That PR tracks agreement lifecycle fromAgreementAcceptedonwards — the window after the indexer-agent callsacceptIndexingAgreement. Dipper's idempotency gate needs visibility into the strictly earlieroffer()→accept()window, during which noIndexingAgreementexists yet. Folding the offer data intoIndexingAgreementwould require creating the entity pre-accept and reworking Maikol's state machine; keeping a separate immutableOfferentity is the minimal-footprint way to cover the window without pushing schema churn into his PR.Stack
feat/aggregated-indexing-agreement(feat: aggregated IndexingAgreement entity #5 — Maikol's PR).Summary
Additions on top of PR #5:
OfferStoredevent entry (~11 lines) inabis/RecurringCollector.json— the only event theRecurringCollectorcontract emits fromoffer()that PR feat: aggregated IndexingAgreement entity #5 does not already cover.OfferStoredevent-log entity and immutable first-winsOfferentity inschema.graphql.Offeris keyed byagreementId(bytes16) so dipper can look it up directly.handleOfferStoredhandler insrc/recurringCollector.tsalongside Maikol's four handlers. Creates theOfferStoredlog unconditionally, then early-returns on duplicateOffer(the@entity(immutable: true)guard pattern — a second write to an immutable entity would halt the subgraph).RecurringCollectordata source insubgraph.template.yamlwith one additional event handler. Intentionally does NOT apply Maikol'stopic1: ["{{subgraphServiceAddress}}"]filter toOfferStored— that filter assumestopic1 == dataService(true for Maikol's four events), while onOfferStoredthe first indexed arg isagreementId, so the filter would match the wrong field.config/hardhat.jsonto pointsubgraphServiceAddressat0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9(the real local-network address). PR feat: aggregated IndexingAgreement entity #5 left it at the zero address, which would make the SubgraphService data source index nothing on local-network.Consumers
Offer(id: <agreementId>) { offerHash }as the idempotency gate before re-submittingoffer().047e0ebwhen the pipeline was switched to proposal-first, so indexer-rs has no runtime dependency on this subgraph today.Why immutable
For a given
agreementId, the RCA's identifying fields (payer,dataService,serviceProvider,deadline,nonce) are fixed by the on-chain id derivation. Any duplicateOfferStoredevent for the same id — whether from a dipper crash-retry or a chain reorg re-emission — carries the sameofferHashby construction. There is nothing meaningful to overwrite, so theOfferentity is declared immutable and the handler guards against a second write with an early return.Related
Draft until end-to-end verification against local-network is complete with the rebased branch.