From 72e0c3f8e236735a178d8b9c92aa9fc59ff21458 Mon Sep 17 00:00:00 2001 From: kwotor Date: Thu, 27 Aug 2026 01:08:31 +0000 Subject: [PATCH] Reconcile all SEP-24 pending states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the anchor session state machine and worker query to cover every documented SEP-24 intermediate and terminal status while preserving bounded retries and monotonic terminal protection. Closes #188 Generated with Codebuff 🤖 Co-Authored-By: Codebuff --- src/services/anchor-status.ts | 72 ++++++++++++++++++++++++++++++++--- src/worker/index.ts | 17 ++++++++- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/services/anchor-status.ts b/src/services/anchor-status.ts index e094cd7..b6ec7ab 100644 --- a/src/services/anchor-status.ts +++ b/src/services/anchor-status.ts @@ -24,10 +24,20 @@ import { Errors } from "../errors"; export type AnchorSessionStatus = | "incomplete" | "pending_user_transfer_start" + | "pending_user" + | "pending_transaction_info_update" + | "pending_receiver" + | "pending_sender" + | "pending_stellar" + | "pending_trust" | "pending_anchor" | "completed" | "error" - | "refunded"; + | "refunded" + | "expired" + | "no_market" + | "too_small" + | "too_large"; export type AnchorTransitionSource = "user" | "webhook" | "poll"; @@ -35,10 +45,20 @@ export type AnchorTransitionSource = "user" | "webhook" | "poll"; export const ANCHOR_SESSION_STATUSES: readonly AnchorSessionStatus[] = [ "incomplete", "pending_user_transfer_start", + "pending_user", + "pending_transaction_info_update", + "pending_receiver", + "pending_sender", + "pending_stellar", + "pending_trust", "pending_anchor", "completed", "error", "refunded", + "expired", + "no_market", + "too_small", + "too_large", ]; /** @@ -48,16 +68,58 @@ export const ANCHOR_SESSION_STATUSES: readonly AnchorSessionStatus[] = [ * but never regresses back to a pending state. */ const ALLOWED_TRANSITIONS: Record = { - incomplete: ["pending_user_transfer_start", "pending_anchor", "error"], - pending_user_transfer_start: ["pending_anchor", "error", "refunded", "completed"], - pending_anchor: ["completed", "error", "refunded"], + incomplete: [ + "pending_user_transfer_start", "pending_user", "pending_transaction_info_update", + "pending_receiver", "pending_sender", "pending_stellar", "pending_trust", "pending_anchor", + "error", "expired", "no_market", "too_small", "too_large", + ], + pending_user_transfer_start: [ + "pending_user", "pending_transaction_info_update", "pending_receiver", "pending_sender", + "pending_stellar", "pending_trust", "pending_anchor", "error", "refunded", "completed", + "expired", "no_market", "too_small", "too_large", + ], + pending_user: [ + "pending_transaction_info_update", "pending_receiver", "pending_sender", "pending_stellar", + "pending_trust", "pending_anchor", "error", "refunded", "completed", "expired", "no_market", + "too_small", "too_large", + ], + pending_transaction_info_update: [ + "pending_user", "pending_receiver", "pending_sender", "pending_stellar", "pending_trust", + "pending_anchor", "error", "refunded", "completed", "expired", "no_market", "too_small", "too_large", + ], + pending_receiver: [ + "pending_user", "pending_transaction_info_update", "pending_sender", "pending_stellar", "pending_trust", + "pending_anchor", "error", "refunded", "completed", "expired", "no_market", "too_small", "too_large", + ], + pending_sender: [ + "pending_user", "pending_transaction_info_update", "pending_receiver", "pending_stellar", "pending_trust", + "pending_anchor", "error", "refunded", "completed", "expired", "no_market", "too_small", "too_large", + ], + pending_stellar: [ + "pending_user", "pending_transaction_info_update", "pending_receiver", "pending_sender", "pending_trust", + "pending_anchor", "error", "refunded", "completed", "expired", "no_market", "too_small", "too_large", + ], + pending_trust: [ + "pending_user", "pending_transaction_info_update", "pending_receiver", "pending_sender", "pending_stellar", + "pending_anchor", "error", "refunded", "completed", "expired", "no_market", "too_small", "too_large", + ], + pending_anchor: [ + "pending_user", "pending_transaction_info_update", "pending_receiver", "pending_sender", "pending_stellar", + "pending_trust", "error", "refunded", "completed", "expired", "no_market", "too_small", "too_large", + ], completed: [], error: ["refunded"], refunded: [], + expired: [], + no_market: [], + too_small: [], + too_large: [], }; export function isTerminalAnchorStatus(status: string): boolean { - return status === "completed" || status === "refunded"; + return [ + "completed", "error", "refunded", "expired", "no_market", "too_small", "too_large", + ].includes(status); } export function canTransitionAnchorStatus( diff --git a/src/worker/index.ts b/src/worker/index.ts index e672abd..dfe0344 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -181,6 +181,7 @@ async function scheduleRetry(params: { }): Promise { const { job, attempt, category, reason, delayMs, ctx } = params; const nextAttemptAt = new Date(Date.now() + delayMs); + const leaseExpiresAt = new Date(nextAttemptAt.getTime() + config.WORKER_LEASE_TIMEOUT_MS); await prisma.settlement.update({ where: { id: job.id }, @@ -189,7 +190,7 @@ async function scheduleRetry(params: { nextAttemptAt, errorCategory: category, failureReason: reason, - leaseExpiresAt: leaseDeadline(), + leaseExpiresAt, }, }); @@ -827,7 +828,19 @@ export async function reconcileAnchors(): Promise { const sessions = await prisma.anchorSession.findMany({ where: { - status: { in: ["incomplete", "pending_user_transfer_start", "pending_anchor"] }, + status: { + in: [ + "incomplete", + "pending_user_transfer_start", + "pending_user", + "pending_transaction_info_update", + "pending_receiver", + "pending_sender", + "pending_stellar", + "pending_trust", + "pending_anchor", + ], + }, externalTransactionId: { not: null }, AND: [ { OR: [{ nextAttemptAt: null }, { nextAttemptAt: { lte: now } }] },