Skip to content

feat: add RecurringCollector data source and Offer entity#4

Open
MoonBoi9001 wants to merge 2 commits intofeat/aggregated-indexing-agreementfrom
mb9/add-recurring-collector-offer-entity
Open

feat: add RecurringCollector data source and Offer entity#4
MoonBoi9001 wants to merge 2 commits intofeat/aggregated-indexing-agreementfrom
mb9/add-recurring-collector-offer-entity

Conversation

@MoonBoi9001
Copy link
Copy Markdown
Member

@MoonBoi9001 MoonBoi9001 commented Apr 15, 2026

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 same offer() and double-spend gas. Dipper needs an authoritative view of "did my prior offer() land?" that survives its own crash. Querying the subgraph for an Offer entity keyed by agreementId is 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 IndexingAgreement entity model). That PR tracks agreement lifecycle from AgreementAccepted onwards — the window after the indexer-agent calls acceptIndexingAgreement. Dipper's idempotency gate needs visibility into the strictly earlier offer()accept() window, during which no IndexingAgreement exists yet. Folding the offer data into IndexingAgreement would require creating the entity pre-accept and reworking Maikol's state machine; keeping a separate immutable Offer entity is the minimal-footprint way to cover the window without pushing schema churn into his PR.

Stack

Summary

Additions on top of PR #5:

  • New OfferStored event entry (~11 lines) in abis/RecurringCollector.json — the only event the RecurringCollector contract emits from offer() that PR feat: aggregated IndexingAgreement entity #5 does not already cover.
  • New immutable OfferStored event-log entity and immutable first-wins Offer entity in schema.graphql. Offer is keyed by agreementId (bytes16) so dipper can look it up directly.
  • New handleOfferStored handler in src/recurringCollector.ts alongside Maikol's four handlers. Creates the OfferStored log unconditionally, then early-returns on duplicate Offer (the @entity(immutable: true) guard pattern — a second write to an immutable entity would halt the subgraph).
  • Extends the existing RecurringCollector data source in subgraph.template.yaml with one additional event handler. Intentionally does NOT apply Maikol's topic1: ["{{subgraphServiceAddress}}"] filter to OfferStored — that filter assumes topic1 == dataService (true for Maikol's four events), while on OfferStored the first indexed arg is agreementId, so the filter would match the wrong field.
  • Corrects config/hardhat.json to point subgraphServiceAddress at 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 (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

  • dipper (edgeandnode/dipper#607) — queries Offer(id: <agreementId>) { offerHash } as the idempotency gate before re-submitting offer().
  • Indexer-rs briefly queried this entity during an intermediate iteration of the offer-based migration. That dependency was removed in feat(dips): switch to offer-based RCA authorization indexer-rs#1009 commit 047e0eb when 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 duplicate OfferStored event for the same id — whether from a dipper crash-retry or a chain reorg re-emission — carries the same offerHash by construction. There is nothing meaningful to overwrite, so the Offer entity 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.

@MoonBoi9001 MoonBoi9001 changed the base branch from main to feat/aggregated-indexing-agreement April 21, 2026 23:55
MoonBoi9001 and others added 2 commits April 22, 2026 08:10
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>
@MoonBoi9001 MoonBoi9001 force-pushed the mb9/add-recurring-collector-offer-entity branch from 530e44a to dafb4e1 Compare April 22, 2026 00:12
@MoonBoi9001 MoonBoi9001 marked this pull request as ready for review April 22, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant