diff --git a/backend/src/routes/v1/admin.routes.ts b/backend/src/routes/v1/admin.routes.ts index 253f112..f6427c7 100644 --- a/backend/src/routes/v1/admin.routes.ts +++ b/backend/src/routes/v1/admin.routes.ts @@ -222,7 +222,7 @@ router.post('/indexer/reset', async (req: Request, res: Response) => { * /v1/admin/indexer/replay: * post: * tags: [Admin] - * summary: Replay events from a given ledger (idempotent) + * summary: Replay events from a given ledger (StreamEvent rows deduplicated; stream mutations not idempotent — see indexerService.ts JSDoc) * security: [{ adminAuth: [] }] * parameters: * - in: query diff --git a/backend/src/services/indexerService.ts b/backend/src/services/indexerService.ts index 4dc6a2d..d386be4 100644 --- a/backend/src/services/indexerService.ts +++ b/backend/src/services/indexerService.ts @@ -38,8 +38,13 @@ export async function resetIndexer(toLedger: number): Promise { /** * Replay events from a given ledger by resetting state and triggering a poll. - * Deduplication in the worker (transactionHash + eventType + ledger) ensures - * no duplicate StreamEvent rows are created. + * The @@unique([transactionHash, eventType]) constraint on StreamEvent + * guarantees no duplicate StreamEvent rows are created on replay. + * + * CAVEAT: This dedup does NOT apply to stream state mutations. + * Stream.withdrawnAmount (handleTokensWithdrawn, soroban-event-worker.ts:635) + * is incremented unconditionally on every replay, so replay is NOT fully + * idempotent. See issue #808 for the withdrawnAmount idempotency fix. */ export async function replayFromLedger(fromLedger: number): Promise { await resetIndexer(fromLedger);