Skip to content

Indexer Handlers: InvoiceCreatedEvent, SubscriptionPlanCreatedEvent, SubscribedEvent #45

Description

@codebestia

Background

Three handlers, following the exact pattern established (decode → dedicated service function → thin handler → registerEventHandler). Two of them are straightforward; one is not, and that one needs to be flagged honestly rather than papered over.

InvoiceCreatedEvent has a real correlation problem. Invoices in this backend are created off-chain firstPOST /invoices writes an Invoice row with invoiceId: null before anything touches the chain. Nothing currently in this codebase submits create_invoice/create_invoice_signed on-chain on a merchant's behalf, so it isn't yet established how an off-chain-created invoice gets its on-chain invoiceId filled in, or how this handler is supposed to match an incoming InvoiceCreatedEvent back to the right DB row. Two real possibilities, not resolved here:

  • A future issue has this backend relay create_invoice_signed (Manager-role, using the merchant's Ed25519 key ) at the moment POST /invoices is called, and captures the resulting invoice_id synchronously from the transaction result — in which case this handler is a backup/reconciliation path, not the primary way invoiceId gets set.
  • Invoices can also originate entirely on-chain (a merchant's own SDK integration calling create_invoice directly, bypassing our API) — in which case this handler is the only way our backend learns about that invoice at all, and must create a new Invoice row from scratch.
  • The contract's create_invoice_signed already takes a nonce for replay protection — that nonce is a natural correlation key if a future issue sets it to the off-chain Invoice.id before submitting. That's the clean fix, but it depends on the relay issue above existing first, so it isn't implemented here.

Given that, this handler's proposed behavior is a best-effort heuristic, not a guaranteed-correct match — call this out in code comments, don't present it as solved: on InvoiceCreatedEvent, look for an existing Invoice with invoiceId: null and matching merchantId + amount + token + description; if found, set its invoiceId; if not found (or if more than one candidate matches — ambiguous, don't guess), create a new Invoice row from the event data instead. Log loudly on the ambiguous-match case so it's visible, not silently wrong.

SubscriptionPlanCreatedEvent and SubscribedEvent have no such problem — plans and subscriptions have no off-chain-first creation path anywhere in this backend, so their handlers are plain create-if-not-exists.

One more thing to verify, not assume: the Events Reference table lists SubscriptionPlanCreatedEvent's key fields as plan_id, merchant, token, amount, interval, timestamp — no description, even though SubscriptionPlan.description is a required (non-nullable) field in our schema and in the contract's own struct. Either the event's actual decoded payload includes description despite the summary table omitting it (likely, if #[contractevent] emits the full struct), or it doesn't and this field must be fetched separately via get_subscription_plan(plan_id). Confirm which against a real testnet event — same discipline already established for topic names — before writing the handler as if it's settled.

Proposed Steps

  1. Confirm real topic symbols and decoded field shapes for all three events against testnet — including the description question above for SubscriptionPlanCreatedEvent, and whether SubscriptionPlanCreatedEvent's merchant field is an Address or a merchant_id integer (the contract's SubscriptionPlan struct has both merchant_id: u64 and merchant: Address; the event's listed field is ambiguous as to which it carries).
  2. applyInvoiceCreated(event, txHash) in invoice.services.ts — implements the heuristic-match-or-create logic described above.
  3. applySubscriptionPlanCreated(event, txHash) in a new subscription.services.ts — resolves merchantId (via whichever of address/int step 1 confirms), creates a SubscriptionPlan row keyed on planId (unique).
  4. applySubscribed(event, txHash) in subscription.services.ts — resolves the SubscriptionPlan by planId, creates a Subscription row with merchantId taken from the resolved plan (not from the event — matches the schema's existing comment about the composite FK requiring plan/subscription merchant consistency).
  5. Three thin handler files under src/indexer/handlers/, each registered via registerEventHandler, same as invoicePaid.ts.
  6. Wire recordAuditLog calls per Action Catalog for all three.

Acceptance Criteria

  • All three topic strings and field shapes are confirmed against testnet before being hardcoded, not guessed
  • InvoiceCreatedEvent handler's ambiguous-match case is logged loudly and does not silently pick a wrong invoice
  • InvoiceCreatedEvent handler creates a new Invoice row when no off-chain match exists, rather than dropping the event
  • SubscriptionPlanCreatedEvent and SubscribedEvent handlers create rows idempotently keyed on their on-chain unique ids (planId, subscriptionId) — replaying the same event (pre-IndexerEvent-dedupe, as a sanity check) does not create duplicates
  • All Prisma writes live in service functions, not in the handler files themselves
  • No second idempotency mechanism is added

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions