Commit 75ac83b
authored
feat: backfill quote status manager entries after app close (#9405)
## Explanation
The `QuoteStatusManager` keeps the bridge-api backend in sync with the
outcome of every submitted quote by reporting `SUBMITTED` and later
`FINALISED`. 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
`reportSubmitted` runs. If the client closes *after* a swap/bridge
source transaction has been broadcast but *before* `reportSubmitted`
executes (or before its entry was persisted), no entry is written. On
the next launch `init()` 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
`BridgeHistoryItem` we persist the `quoteId`, the source tx hash
(`status.srcChain.txHash`, with the underlying transaction's `hash` as a
fallback), and the `txMetaId`. 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:
```mermaid
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 **before** `this.#quoteStatusManager.init()` in the
constructor. It iterates persisted `txHistory` and, for each item that
has both a `quoteId` and a `txMetaId` and is within the entry TTL,
derives the `srcTxHash` and replays submission through the existing
idempotent `#reportSubmittedOnce` helper. That rebuilds the missing
deferred entry, and the subsequent `init()` reconciliation loop
finalizes it exactly as it would in the normal (client-stayed-open)
flow.
Why this is robust:
- **Idempotent by construction.** `#reportSubmittedOnce` short-circuits
when `historyItem.reportedSubmittedTxHash === srcTxHash`, and
`QuoteStatusManager.reportSubmitted` additionally ignores quotes it is
already tracking. Re-seeding on every startup can never double-report or
resurrect a completed quote.
- **TTL-bounded.** Items older than `QUOTE_STATUS_UPDATE_ENTRY_TTL` are
skipped, because the backend would reject them anyway. This bounds the
amount of work at startup and avoids pointless network requests.
- **No new source of truth.** Entries are derived from data we already
persist on the history item, so the backfill introduces no new state to
keep consistent, history remains authoritative and the store is a
rebuildable projection of it.
- **STX-safe fallback.** When the history status hash was never written
(STX swaps confirmed while closed), it falls back to the transaction
meta's `hash` via `getTransactionMetaById`, so those cases are covered
too.
- **Disabled-safe & correctly ordered.** The whole path is gated behind
the manager's `isEnabled` check, and seeding runs before `init()` so the
reconciliation loop always sees the freshly seeded entries.
Changes whose purpose may not be obvious to those unfamiliar with the
domain: the `startTime` TTL comparison and the `status.srcChain.txHash
?? txMeta.hash` fallback 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
- [x] I've updated the test suite for new or updated code as appropriate
(272 lines of new coverage in `bridge-status-controller.test.ts`).
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate (JSDoc on
`#seedQuoteStatusEntriesFromHistory`, plus a `CHANGELOG.md` entry).
- [x] I've communicated my changes to consumers by updating changelogs
for packages I've changed.
- [ ] I've introduced breaking changes in this PR and have prepared
draft pull requests for clients and consumer packages to resolve them.
_(N/A — additive, non-breaking startup behavior.)_
<!-- CURSOR_SUMMARY -->
---
> [!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 recent `txHistory` items
and replays the existing idempotent `#reportSubmittedOnce` path using
`quoteId`, `txMetaId`, and source hash (`status.srcChain.txHash` or
transaction meta `hash`). Items outside a new **12h**
`QUOTE_STATUS_BACKFILL_WINDOW_MS`, or without required fields, are
skipped; seeding is a no-op when the quote-status manager is disabled
(so `reportedSubmittedTxHash` is not set incorrectly).
>
> **TTL tuning:** per-entry local retry lifetime
`QUOTE_STATUS_UPDATE_ENTRY_TTL` is shortened from **12h to 3h**, while
the separate backfill window stays at 12h so multi-session in-flight
bridges can still be resumed.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
def1682. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent 9b2a8b1 commit 75ac83b
4 files changed
Lines changed: 361 additions & 1 deletion
File tree
- packages/bridge-status-controller
- src
- quote-status-manager
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
Lines changed: 280 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| |||
6806 | 6807 | | |
6807 | 6808 | | |
6808 | 6809 | | |
| 6810 | + | |
| 6811 | + | |
| 6812 | + | |
| 6813 | + | |
| 6814 | + | |
| 6815 | + | |
| 6816 | + | |
| 6817 | + | |
| 6818 | + | |
| 6819 | + | |
| 6820 | + | |
| 6821 | + | |
| 6822 | + | |
| 6823 | + | |
| 6824 | + | |
| 6825 | + | |
| 6826 | + | |
| 6827 | + | |
| 6828 | + | |
| 6829 | + | |
| 6830 | + | |
| 6831 | + | |
| 6832 | + | |
| 6833 | + | |
| 6834 | + | |
| 6835 | + | |
| 6836 | + | |
| 6837 | + | |
| 6838 | + | |
| 6839 | + | |
| 6840 | + | |
| 6841 | + | |
| 6842 | + | |
| 6843 | + | |
| 6844 | + | |
| 6845 | + | |
| 6846 | + | |
| 6847 | + | |
| 6848 | + | |
| 6849 | + | |
| 6850 | + | |
| 6851 | + | |
| 6852 | + | |
| 6853 | + | |
| 6854 | + | |
| 6855 | + | |
| 6856 | + | |
| 6857 | + | |
| 6858 | + | |
| 6859 | + | |
| 6860 | + | |
| 6861 | + | |
| 6862 | + | |
| 6863 | + | |
| 6864 | + | |
| 6865 | + | |
| 6866 | + | |
| 6867 | + | |
| 6868 | + | |
| 6869 | + | |
| 6870 | + | |
| 6871 | + | |
| 6872 | + | |
| 6873 | + | |
| 6874 | + | |
| 6875 | + | |
| 6876 | + | |
| 6877 | + | |
| 6878 | + | |
| 6879 | + | |
| 6880 | + | |
| 6881 | + | |
| 6882 | + | |
| 6883 | + | |
| 6884 | + | |
| 6885 | + | |
| 6886 | + | |
| 6887 | + | |
| 6888 | + | |
| 6889 | + | |
| 6890 | + | |
| 6891 | + | |
| 6892 | + | |
| 6893 | + | |
| 6894 | + | |
| 6895 | + | |
| 6896 | + | |
| 6897 | + | |
| 6898 | + | |
| 6899 | + | |
| 6900 | + | |
| 6901 | + | |
| 6902 | + | |
| 6903 | + | |
| 6904 | + | |
| 6905 | + | |
| 6906 | + | |
| 6907 | + | |
| 6908 | + | |
| 6909 | + | |
| 6910 | + | |
| 6911 | + | |
| 6912 | + | |
| 6913 | + | |
| 6914 | + | |
| 6915 | + | |
| 6916 | + | |
| 6917 | + | |
| 6918 | + | |
| 6919 | + | |
| 6920 | + | |
| 6921 | + | |
| 6922 | + | |
| 6923 | + | |
| 6924 | + | |
| 6925 | + | |
| 6926 | + | |
| 6927 | + | |
| 6928 | + | |
| 6929 | + | |
| 6930 | + | |
| 6931 | + | |
| 6932 | + | |
| 6933 | + | |
| 6934 | + | |
| 6935 | + | |
| 6936 | + | |
| 6937 | + | |
| 6938 | + | |
| 6939 | + | |
| 6940 | + | |
| 6941 | + | |
| 6942 | + | |
| 6943 | + | |
| 6944 | + | |
| 6945 | + | |
| 6946 | + | |
| 6947 | + | |
| 6948 | + | |
| 6949 | + | |
| 6950 | + | |
| 6951 | + | |
| 6952 | + | |
| 6953 | + | |
| 6954 | + | |
| 6955 | + | |
| 6956 | + | |
| 6957 | + | |
| 6958 | + | |
| 6959 | + | |
| 6960 | + | |
| 6961 | + | |
| 6962 | + | |
| 6963 | + | |
| 6964 | + | |
| 6965 | + | |
| 6966 | + | |
| 6967 | + | |
| 6968 | + | |
| 6969 | + | |
| 6970 | + | |
| 6971 | + | |
| 6972 | + | |
| 6973 | + | |
| 6974 | + | |
| 6975 | + | |
| 6976 | + | |
| 6977 | + | |
| 6978 | + | |
| 6979 | + | |
| 6980 | + | |
| 6981 | + | |
| 6982 | + | |
| 6983 | + | |
| 6984 | + | |
| 6985 | + | |
| 6986 | + | |
| 6987 | + | |
| 6988 | + | |
| 6989 | + | |
| 6990 | + | |
| 6991 | + | |
| 6992 | + | |
| 6993 | + | |
| 6994 | + | |
| 6995 | + | |
| 6996 | + | |
| 6997 | + | |
| 6998 | + | |
| 6999 | + | |
| 7000 | + | |
| 7001 | + | |
| 7002 | + | |
| 7003 | + | |
| 7004 | + | |
| 7005 | + | |
| 7006 | + | |
| 7007 | + | |
| 7008 | + | |
| 7009 | + | |
| 7010 | + | |
| 7011 | + | |
| 7012 | + | |
| 7013 | + | |
| 7014 | + | |
| 7015 | + | |
| 7016 | + | |
| 7017 | + | |
| 7018 | + | |
| 7019 | + | |
| 7020 | + | |
| 7021 | + | |
| 7022 | + | |
| 7023 | + | |
| 7024 | + | |
| 7025 | + | |
| 7026 | + | |
| 7027 | + | |
| 7028 | + | |
| 7029 | + | |
| 7030 | + | |
| 7031 | + | |
| 7032 | + | |
| 7033 | + | |
| 7034 | + | |
| 7035 | + | |
| 7036 | + | |
| 7037 | + | |
| 7038 | + | |
| 7039 | + | |
| 7040 | + | |
| 7041 | + | |
| 7042 | + | |
| 7043 | + | |
| 7044 | + | |
| 7045 | + | |
| 7046 | + | |
| 7047 | + | |
| 7048 | + | |
| 7049 | + | |
| 7050 | + | |
| 7051 | + | |
| 7052 | + | |
| 7053 | + | |
| 7054 | + | |
| 7055 | + | |
| 7056 | + | |
| 7057 | + | |
| 7058 | + | |
| 7059 | + | |
| 7060 | + | |
| 7061 | + | |
| 7062 | + | |
| 7063 | + | |
| 7064 | + | |
| 7065 | + | |
| 7066 | + | |
| 7067 | + | |
| 7068 | + | |
| 7069 | + | |
| 7070 | + | |
| 7071 | + | |
| 7072 | + | |
| 7073 | + | |
| 7074 | + | |
| 7075 | + | |
| 7076 | + | |
| 7077 | + | |
| 7078 | + | |
| 7079 | + | |
| 7080 | + | |
| 7081 | + | |
| 7082 | + | |
| 7083 | + | |
| 7084 | + | |
| 7085 | + | |
| 7086 | + | |
| 7087 | + | |
| 7088 | + | |
6809 | 7089 | | |
6810 | 7090 | | |
6811 | 7091 | | |
| |||
Lines changed: 63 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| |||
316 | 317 | | |
317 | 318 | | |
318 | 319 | | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
319 | 326 | | |
320 | 327 | | |
321 | 328 | | |
| |||
482 | 489 | | |
483 | 490 | | |
484 | 491 | | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
485 | 495 | | |
486 | 496 | | |
487 | 497 | | |
| |||
614 | 624 | | |
615 | 625 | | |
616 | 626 | | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
617 | 680 | | |
618 | 681 | | |
619 | 682 | | |
| |||
0 commit comments