feat: backfill quote status manager entries after app close#9405
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a6c5e4b. Configure here.
|
@metamaskbot publish-preview |
| // Skip items older than the backfill window: the backend would reject | ||
| // a status report for a quote that old, so there is no point creating | ||
| // an entry or making a request. | ||
| if ( | ||
| Date.now() - historyItem.startTime > | ||
| QUOTE_STATUS_BACKFILL_WINDOW_MS | ||
| ) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Does the backend throw a fetch error? If not, I'd leave the filtering to the backend for flexibility (i.e. if the backend age limit changes)
There was a problem hiding this comment.
no it does not, the notion of "backfill" is currently unsupported by the backend, this means we can also backfill swaps that were done a year ago if we change this condition (and other conditions are met). Yet this is not what we want either right now. This PR is introduced to make things more robust (client operates on best-effort basis). Once we get the first production data in two-three weeks we can revise the strategy.

Explanation
The
QuoteStatusManagerkeeps the bridge-api backend in sync with the outcome of every submitted quote by reportingSUBMITTEDand laterFINALISED. It does this through a persisted store of deferred "quote-status entries" keyed by the composite${quoteId}:${srcTxHash}. On startup,QuoteStatusManager.init()walks only the entries that already exist in that store and reconciles each one against the current transaction status.The problem is that a deferred entry is only ever created when
reportSubmittedruns. If the client closes after a swap/bridge source transaction has been broadcast but beforereportSubmittedexecutes (or before its entry was persisted), no entry is written. On the next launchinit()has nothing to reconcile for that quote, so its terminal status is never reported to the backend, a permanently lost update. Smart-transaction (STX) swaps that confirm while the client is closed are a notable trigger, because the history item's status hash was never written.The key realization is that the persisted transaction history already carries everything needed to rebuild a lost entry. On each
BridgeHistoryItemwe persist thequoteId, the source tx hash (status.srcChain.txHash, with the underlying transaction'shashas a fallback), and thetxMetaId. Those are exactly the identity fields of a quote-status entry (quoteId:srcTxHash+txMetaId).quoteId+ source-tx-hash therefore behave like a foreign key from a history row into the quote-status store, which lets us treat history as the source of truth and losslessly reconstruct any missing entry:erDiagram TX_HISTORY_ITEM ||--o| QUOTE_STATUS_ENTRY : "quoteId + srcTxHash (composite FK)" TX_HISTORY_ITEM { string txMetaId PK "key in state.txHistory; also correlates finalisation" string quoteId FK "part of composite FK" string status_srcChain_txHash FK "srcTxHash; part of composite FK (falls back to txMeta.hash)" string reportedSubmittedTxHash "guard: prevents duplicate reportSubmitted" number startTime "compared against entry TTL" } QUOTE_STATUS_ENTRY { string entryKey PK "hash() => `quoteId:srcTxHash` virtual column, does not exist" string quoteId string srcTxHash string txMetaId "used by reportFinalised()" QuoteStatusState status number createdAt number lastAttemptAt }The solution adds
#seedQuoteStatusEntriesFromHistory(), invoked once immediately beforethis.#quoteStatusManager.init()in the constructor. It iterates persistedtxHistoryand, for each item that has both aquoteIdand atxMetaIdand is within the entry TTL, derives thesrcTxHashand replays submission through the existing idempotent#reportSubmittedOncehelper. That rebuilds the missing deferred entry, and the subsequentinit()reconciliation loop finalizes it exactly as it would in the normal (client-stayed-open) flow.Why this is robust:
#reportSubmittedOnceshort-circuits whenhistoryItem.reportedSubmittedTxHash === srcTxHash, andQuoteStatusManager.reportSubmittedadditionally ignores quotes it is already tracking. Re-seeding on every startup can never double-report or resurrect a completed quote.QUOTE_STATUS_UPDATE_ENTRY_TTLare skipped, because the backend would reject them anyway. This bounds the amount of work at startup and avoids pointless network requests.hashviagetTransactionMetaById, so those cases are covered too.isEnabledcheck, and seeding runs beforeinit()so the reconciliation loop always sees the freshly seeded entries.Changes whose purpose may not be obvious to those unfamiliar with the domain: the
startTimeTTL comparison and thestatus.srcChain.txHash ?? txMeta.hashfallback both exist purely to mirror backend acceptance rules and the STX edge case, not to change the reporting semantics.References
https://consensyssoftware.atlassian.net/browse/SWAPS-4704
Checklist
bridge-status-controller.test.ts).#seedQuoteStatusEntriesFromHistory, plus aCHANGELOG.mdentry).Note
Medium Risk
Changes startup reconciliation and backend quote-status reporting, plus shortens how long failed updates are retried; behavior is idempotent and feature-gated but affects swap/bridge observability.
Overview
On startup, missing quote-status store entries are rebuilt from persisted bridge transaction history so quotes submitted before the client closed still get
SUBMITTED/ final status reported to the backend.Before
QuoteStatusManager.init(),#seedQuoteStatusEntriesFromHistory()walks recenttxHistoryitems and replays the existing idempotent#reportSubmittedOncepath usingquoteId,txMetaId, and source hash (status.srcChain.txHashor transaction metahash). Items outside a new 12hQUOTE_STATUS_BACKFILL_WINDOW_MS, or without required fields, are skipped; seeding is a no-op when the quote-status manager is disabled (soreportedSubmittedTxHashis not set incorrectly).TTL tuning: per-entry local retry lifetime
QUOTE_STATUS_UPDATE_ENTRY_TTLis shortened from 12h to 3h, while the separate backfill window stays at 12h so multi-session in-flight bridges can still be resumed.Reviewed by Cursor Bugbot for commit def1682. Bugbot is set up for automated code reviews on this repo. Configure here.