@metamask-previews/bridge-status-controller 73.1.0-preview-e12e4a38f → 74.0.0-preview-4f9d41d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/dist/bridge-status-controller.cjs +167 -21
  3. package/dist/bridge-status-controller.cjs.map +1 -1
  4. package/dist/bridge-status-controller.d.cts +6 -1
  5. package/dist/bridge-status-controller.d.cts.map +1 -1
  6. package/dist/bridge-status-controller.d.mts +6 -1
  7. package/dist/bridge-status-controller.d.mts.map +1 -1
  8. package/dist/bridge-status-controller.mjs +167 -21
  9. package/dist/bridge-status-controller.mjs.map +1 -1
  10. package/dist/constants.cjs +1 -0
  11. package/dist/constants.cjs.map +1 -1
  12. package/dist/constants.d.cts.map +1 -1
  13. package/dist/constants.d.mts.map +1 -1
  14. package/dist/constants.mjs +1 -0
  15. package/dist/constants.mjs.map +1 -1
  16. package/dist/index.cjs +12 -6
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +3 -1
  19. package/dist/index.d.cts.map +1 -1
  20. package/dist/index.d.mts +3 -1
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +3 -0
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/quote-status-manager/constants.cjs +181 -0
  25. package/dist/quote-status-manager/constants.cjs.map +1 -0
  26. package/dist/quote-status-manager/constants.d.cts +137 -0
  27. package/dist/quote-status-manager/constants.d.cts.map +1 -0
  28. package/dist/quote-status-manager/constants.d.mts +137 -0
  29. package/dist/quote-status-manager/constants.d.mts.map +1 -0
  30. package/dist/quote-status-manager/constants.mjs +178 -0
  31. package/dist/quote-status-manager/constants.mjs.map +1 -0
  32. package/dist/quote-status-manager/errors.cjs +51 -0
  33. package/dist/quote-status-manager/errors.cjs.map +1 -0
  34. package/dist/quote-status-manager/errors.d.cts +45 -0
  35. package/dist/quote-status-manager/errors.d.cts.map +1 -0
  36. package/dist/quote-status-manager/errors.d.mts +45 -0
  37. package/dist/quote-status-manager/errors.d.mts.map +1 -0
  38. package/dist/quote-status-manager/errors.mjs +46 -0
  39. package/dist/quote-status-manager/errors.mjs.map +1 -0
  40. package/dist/quote-status-manager/quote-status-api-service.cjs +241 -0
  41. package/dist/quote-status-manager/quote-status-api-service.cjs.map +1 -0
  42. package/dist/quote-status-manager/quote-status-api-service.d.cts +93 -0
  43. package/dist/quote-status-manager/quote-status-api-service.d.cts.map +1 -0
  44. package/dist/quote-status-manager/quote-status-api-service.d.mts +93 -0
  45. package/dist/quote-status-manager/quote-status-api-service.d.mts.map +1 -0
  46. package/dist/quote-status-manager/quote-status-api-service.mjs +237 -0
  47. package/dist/quote-status-manager/quote-status-api-service.mjs.map +1 -0
  48. package/dist/quote-status-manager/quote-status-entry-store.cjs +271 -0
  49. package/dist/quote-status-manager/quote-status-entry-store.cjs.map +1 -0
  50. package/dist/quote-status-manager/quote-status-entry-store.d.cts +144 -0
  51. package/dist/quote-status-manager/quote-status-entry-store.d.cts.map +1 -0
  52. package/dist/quote-status-manager/quote-status-entry-store.d.mts +144 -0
  53. package/dist/quote-status-manager/quote-status-entry-store.d.mts.map +1 -0
  54. package/dist/quote-status-manager/quote-status-entry-store.mjs +267 -0
  55. package/dist/quote-status-manager/quote-status-entry-store.mjs.map +1 -0
  56. package/dist/quote-status-manager/quote-status-get-with-retry-outcome.cjs +29 -0
  57. package/dist/quote-status-manager/quote-status-get-with-retry-outcome.cjs.map +1 -0
  58. package/dist/quote-status-manager/quote-status-get-with-retry-outcome.d.cts +32 -0
  59. package/dist/quote-status-manager/quote-status-get-with-retry-outcome.d.cts.map +1 -0
  60. package/dist/quote-status-manager/quote-status-get-with-retry-outcome.d.mts +32 -0
  61. package/dist/quote-status-manager/quote-status-get-with-retry-outcome.d.mts.map +1 -0
  62. package/dist/quote-status-manager/quote-status-get-with-retry-outcome.mjs +25 -0
  63. package/dist/quote-status-manager/quote-status-get-with-retry-outcome.mjs.map +1 -0
  64. package/dist/quote-status-manager/quote-status-state-fsm.cjs +107 -0
  65. package/dist/quote-status-manager/quote-status-state-fsm.cjs.map +1 -0
  66. package/dist/quote-status-manager/quote-status-state-fsm.d.cts +57 -0
  67. package/dist/quote-status-manager/quote-status-state-fsm.d.cts.map +1 -0
  68. package/dist/quote-status-manager/quote-status-state-fsm.d.mts +57 -0
  69. package/dist/quote-status-manager/quote-status-state-fsm.d.mts.map +1 -0
  70. package/dist/quote-status-manager/quote-status-state-fsm.mjs +103 -0
  71. package/dist/quote-status-manager/quote-status-state-fsm.mjs.map +1 -0
  72. package/dist/quote-status-manager/quote-status-update-with-retry-outcome.cjs +27 -0
  73. package/dist/quote-status-manager/quote-status-update-with-retry-outcome.cjs.map +1 -0
  74. package/dist/quote-status-manager/quote-status-update-with-retry-outcome.d.cts +30 -0
  75. package/dist/quote-status-manager/quote-status-update-with-retry-outcome.d.cts.map +1 -0
  76. package/dist/quote-status-manager/quote-status-update-with-retry-outcome.d.mts +30 -0
  77. package/dist/quote-status-manager/quote-status-update-with-retry-outcome.d.mts.map +1 -0
  78. package/dist/quote-status-manager/quote-status-update-with-retry-outcome.mjs +23 -0
  79. package/dist/quote-status-manager/quote-status-update-with-retry-outcome.mjs.map +1 -0
  80. package/dist/quote-status-manager/quotes-status-manager.cjs +477 -0
  81. package/dist/quote-status-manager/quotes-status-manager.cjs.map +1 -0
  82. package/dist/quote-status-manager/quotes-status-manager.d.cts +124 -0
  83. package/dist/quote-status-manager/quotes-status-manager.d.cts.map +1 -0
  84. package/dist/quote-status-manager/quotes-status-manager.d.mts +124 -0
  85. package/dist/quote-status-manager/quotes-status-manager.d.mts.map +1 -0
  86. package/dist/quote-status-manager/quotes-status-manager.mjs +473 -0
  87. package/dist/quote-status-manager/quotes-status-manager.mjs.map +1 -0
  88. package/dist/quote-status-manager/types.cjs +8 -0
  89. package/dist/quote-status-manager/types.cjs.map +1 -0
  90. package/dist/quote-status-manager/types.d.cts +145 -0
  91. package/dist/quote-status-manager/types.d.cts.map +1 -0
  92. package/dist/quote-status-manager/types.d.mts +145 -0
  93. package/dist/quote-status-manager/types.d.mts.map +1 -0
  94. package/dist/quote-status-manager/types.mjs +6 -0
  95. package/dist/quote-status-manager/types.mjs.map +1 -0
  96. package/dist/quote-status-manager/utils.cjs +11 -0
  97. package/dist/quote-status-manager/utils.cjs.map +1 -0
  98. package/dist/quote-status-manager/utils.d.cts +7 -0
  99. package/dist/quote-status-manager/utils.d.cts.map +1 -0
  100. package/dist/quote-status-manager/utils.d.mts +7 -0
  101. package/dist/quote-status-manager/utils.d.mts.map +1 -0
  102. package/dist/quote-status-manager/utils.mjs +7 -0
  103. package/dist/quote-status-manager/utils.mjs.map +1 -0
  104. package/dist/quote-status-manager/validators.cjs +44 -0
  105. package/dist/quote-status-manager/validators.cjs.map +1 -0
  106. package/dist/quote-status-manager/validators.d.cts +172 -0
  107. package/dist/quote-status-manager/validators.d.cts.map +1 -0
  108. package/dist/quote-status-manager/validators.d.mts +172 -0
  109. package/dist/quote-status-manager/validators.d.mts.map +1 -0
  110. package/dist/quote-status-manager/validators.mjs +39 -0
  111. package/dist/quote-status-manager/validators.mjs.map +1 -0
  112. package/dist/types.cjs +2 -0
  113. package/dist/types.cjs.map +1 -1
  114. package/dist/types.d.cts +15 -3
  115. package/dist/types.d.cts.map +1 -1
  116. package/dist/types.d.mts +15 -3
  117. package/dist/types.d.mts.map +1 -1
  118. package/dist/types.mjs +2 -0
  119. package/dist/types.mjs.map +1 -1
  120. package/dist/utils/history.cjs +1 -0
  121. package/dist/utils/history.cjs.map +1 -1
  122. package/dist/utils/history.d.cts.map +1 -1
  123. package/dist/utils/history.d.mts.map +1 -1
  124. package/dist/utils/history.mjs +1 -0
  125. package/dist/utils/history.mjs.map +1 -1
  126. package/package.json +1 -1
@@ -0,0 +1,477 @@
1
+ "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _QuoteStatusManager_instances, _QuoteStatusManager_messenger, _QuoteStatusManager_quoteStatusApiService, _QuoteStatusManager_quoteStatusEntryStore, _QuoteStatusManager_isEnabled, _QuoteStatusManager_onError, _QuoteStatusManager_updateIntervalMs, _QuoteStatusManager_retryIntervalId, _QuoteStatusManager_processInitial, _QuoteStatusManager_ensureRetryTimerRunning, _QuoteStatusManager_stopRetryTimer, _QuoteStatusManager_processRetries, _QuoteStatusManager_hasPendingUpdates, _QuoteStatusManager_stopRetryTimerIfIdle, _QuoteStatusManager_processEntry, _QuoteStatusManager_markCompleted, _QuoteStatusManager_markExpired, _QuoteStatusManager_handleNonRetryableUpdateStatusError;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.QuoteStatusManager = void 0;
16
+ const transaction_controller_1 = require("@metamask/transaction-controller");
17
+ const types_1 = require("../types.cjs");
18
+ const transaction_1 = require("../utils/transaction.cjs");
19
+ const constants_1 = require("./constants.cjs");
20
+ const errors_1 = require("./errors.cjs");
21
+ const quote_status_api_service_1 = require("./quote-status-api-service.cjs");
22
+ const quote_status_entry_store_1 = require("./quote-status-entry-store.cjs");
23
+ const quote_status_state_fsm_1 = require("./quote-status-state-fsm.cjs");
24
+ const quote_status_update_with_retry_outcome_1 = require("./quote-status-update-with-retry-outcome.cjs");
25
+ /**
26
+ * Tracks bridge/swap quotes through their lifecycle and keeps the backend in
27
+ * sync with the latest known status of each quote.
28
+ *
29
+ * Quotes are reported via {@link reportSubmitted} and {@link reportFinalised},
30
+ * stored as runtime entries, and pushed to the backend. Updates that fail in a
31
+ * retryable way are retried on a periodic timer until each entry reaches a
32
+ * terminal state, at which point it is evicted and the timer stops once the
33
+ * store is empty.
34
+ */
35
+ class QuoteStatusManager {
36
+ /**
37
+ * Creates a new manager and immediately processes any persisted entries.
38
+ *
39
+ * @param options - Constructor options.
40
+ * @param options.messenger - Messenger used to communicate with the backend
41
+ * API service.
42
+ * @param options.clientId - Identifier of the client making the requests.
43
+ * @param options.clientProduct - Name of the client product making the
44
+ * requests.
45
+ * @param options.clientVersion - Optional version of the client product.
46
+ * @param options.apiBaseUrl - Base URL of the quote status backend API.
47
+ * @param options.onPersistUpdates - Callback invoked to persist entry updates.
48
+ * @param options.onError - Optional callback invoked when a non-recoverable
49
+ * error occurs.
50
+ * @param options.isEnabled - Optional predicate gating whether the manager
51
+ * performs any work.
52
+ * @param options.entryTtlMs - Time-to-live, in milliseconds, after which a
53
+ * tracked entry is evicted.
54
+ * @param options.updateIntervalMs - How often the manager re-processes entries that
55
+ * have not yet reached a terminal state
56
+ * @param options.initialData - Persisted entries to rehydrate on startup.
57
+ */
58
+ constructor({ messenger, clientId, clientProduct, clientVersion, apiBaseUrl, onError, isEnabled, onPersistUpdates, entryTtlMs, updateIntervalMs, initialData, }) {
59
+ _QuoteStatusManager_instances.add(this);
60
+ _QuoteStatusManager_messenger.set(this, void 0);
61
+ _QuoteStatusManager_quoteStatusApiService.set(this, void 0);
62
+ _QuoteStatusManager_quoteStatusEntryStore.set(this, void 0);
63
+ _QuoteStatusManager_isEnabled.set(this, void 0);
64
+ _QuoteStatusManager_onError.set(this, void 0);
65
+ _QuoteStatusManager_updateIntervalMs.set(this, void 0);
66
+ /**
67
+ * Handle for the periodic retry timer. While running, every
68
+ * {@link QUOTE_STATUS_UPDATE_RETRY_INTERVAL_MS} all entries that have not yet
69
+ * reached a terminal state (Completed/Expired) are re-processed. It is started
70
+ * lazily when there is work to do and stopped once the store is empty.
71
+ */
72
+ _QuoteStatusManager_retryIntervalId.set(this, null);
73
+ __classPrivateFieldSet(this, _QuoteStatusManager_isEnabled, isEnabled, "f");
74
+ __classPrivateFieldSet(this, _QuoteStatusManager_onError, onError, "f");
75
+ __classPrivateFieldSet(this, _QuoteStatusManager_updateIntervalMs, updateIntervalMs, "f");
76
+ __classPrivateFieldSet(this, _QuoteStatusManager_messenger, messenger, "f");
77
+ __classPrivateFieldSet(this, _QuoteStatusManager_quoteStatusApiService, new quote_status_api_service_1.QuoteStatusApiService({
78
+ messenger,
79
+ clientId,
80
+ clientProduct,
81
+ clientVersion,
82
+ apiBaseUrl,
83
+ onError,
84
+ }), "f");
85
+ __classPrivateFieldSet(this, _QuoteStatusManager_quoteStatusEntryStore, new quote_status_entry_store_1.QuoteStatusEntryStore({
86
+ onPersistUpdates,
87
+ entryTtlMs,
88
+ initial: initialData,
89
+ }), "f");
90
+ }
91
+ /**
92
+ * Whether quote-status tracking and backend sync are currently active.
93
+ *
94
+ * Reflects the latest value returned by the optional `isEnabled` predicate
95
+ * supplied at construction time. When no predicate was provided, this is
96
+ * always `false` and all public methods that gate on enablement no-op.
97
+ *
98
+ * @returns `true` when the `isEnabled` predicate returns a truthy value.
99
+ */
100
+ get enabled() {
101
+ return Boolean(__classPrivateFieldGet(this, _QuoteStatusManager_isEnabled, "f")?.call(this));
102
+ }
103
+ /**
104
+ * Reports that a previously submitted quote has finalized on-chain.
105
+ *
106
+ * Looks up the tracked entry by its transaction metadata id, transitions it to
107
+ * the appropriate terminal state, and processes the update. No-ops when the
108
+ * manager is disabled, and surfaces an error when the entry is missing or
109
+ * cannot transition to the finalized state.
110
+ *
111
+ * @param txMetaId - Transaction metadata id of the finalized quote.
112
+ * @param success - Whether the transaction finalized successfully.
113
+ */
114
+ reportFinalised(txMetaId, success) {
115
+ if (!__classPrivateFieldGet(this, _QuoteStatusManager_isEnabled, "f")?.call(this)) {
116
+ return;
117
+ }
118
+ const entry = __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").getByTxMetaId(txMetaId);
119
+ if (!entry) {
120
+ __classPrivateFieldGet(this, _QuoteStatusManager_onError, "f")?.call(this, new errors_1.QuoteStatusUpdateError('reporting finalization status but entry was not found', { quoteId: '' }));
121
+ return;
122
+ }
123
+ const nextState = success
124
+ ? constants_1.QuoteStatusState.FinalizedSuccess
125
+ : constants_1.QuoteStatusState.FinalizedFailed;
126
+ if (!entry.status.canTransitionTo(nextState)) {
127
+ // This is expected, there are race conditions where
128
+ // reportFinalized can be called twice. If the second
129
+ // call fails due to the first completed sucesfully
130
+ // backend will report that we cannot transition outside
131
+ // a final state, which is correct and we can safely abort
132
+ // the flow.
133
+ return;
134
+ }
135
+ entry.status.transitionTo(nextState);
136
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_ensureRetryTimerRunning).call(this);
137
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processEntry).call(this, entry);
138
+ }
139
+ /**
140
+ * Reports that a quote has been submitted on-chain and begins tracking it.
141
+ *
142
+ * Creates a new entry in the `Submitted` state, starts the retry timer, and
143
+ * processes the initial status update. No-ops when the manager is disabled,
144
+ * and surfaces an error if the entry cannot be retrieved after being stored.
145
+ *
146
+ * @param quoteId - Identifier of the submitted quote.
147
+ * @param srcTxHash - Hash of the source-chain transaction for the quote.
148
+ * @param txMetaId - Optional transaction metadata id used to correlate
149
+ * finalization reports.
150
+ */
151
+ reportSubmitted(quoteId, srcTxHash, txMetaId) {
152
+ if (!__classPrivateFieldGet(this, _QuoteStatusManager_isEnabled, "f")?.call(this)) {
153
+ return;
154
+ }
155
+ // Once a quote has advanced past `Submitted` (finalized or terminal), it is
156
+ // done: reporting `SUBMITTED` again would be rejected by the backend as an
157
+ // invalid transition. Retained terminal entries let us recognize and drop
158
+ // these late/duplicate submissions instead of looping on a 400.
159
+ const isQuoteAlreadyTracked = __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f")
160
+ .getByQuoteId(quoteId)
161
+ .some((entry) => entry.status.state !== constants_1.QuoteStatusState.Submitted);
162
+ if (isQuoteAlreadyTracked) {
163
+ return;
164
+ }
165
+ const entryKey = quote_status_entry_store_1.QuoteStatusEntryStore.hash({
166
+ quoteId,
167
+ srcTxHash,
168
+ });
169
+ const entry = __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").put(entryKey, {
170
+ quoteId,
171
+ srcTxHash,
172
+ txMetaId,
173
+ status: new quote_status_state_fsm_1.QuoteStatusStateFsm(constants_1.QuoteStatusState.Submitted),
174
+ });
175
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_ensureRetryTimerRunning).call(this);
176
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processEntry).call(this, entry);
177
+ }
178
+ /**
179
+ * Tears down the manager by stopping the retry timer and clearing all tracked
180
+ * entries.
181
+ */
182
+ destroy() {
183
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_stopRetryTimer).call(this);
184
+ __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").clear();
185
+ }
186
+ /**
187
+ * Reconciles quote-status entries whose finalization was missed while the
188
+ * client was closed. Called once during initialization.
189
+ *
190
+ * Swap/bridge source transactions report their final status through the
191
+ * `TransactionController:transactionStatusUpdated` subscription
192
+ * ({@link #onTransactionConfirmed}/{@link #onTransactionFailed}). When a source
193
+ * transaction reaches a terminal state while the client is not running, that
194
+ * event is never re-emitted on the next startup, so the persisted entry would
195
+ * remain `Submitted` until it expires via TTL. This replays the missed terminal
196
+ * event for both swaps and bridges: a confirmed source transaction is reported
197
+ * as a finalized success and a failed/dropped one as a finalized failure.
198
+ *
199
+ * `rejected` is ignored because the transaction was never broadcast.
200
+ */
201
+ init() {
202
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processInitial).call(this);
203
+ for (const entry of __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").values()) {
204
+ // Only entries still awaiting finalization need catching up. Entries
205
+ // already in a finalized state are re-sent by `processInitial()`.
206
+ if (entry.status.state !== constants_1.QuoteStatusState.Submitted ||
207
+ !entry.txMetaId) {
208
+ continue;
209
+ }
210
+ const txMeta = (0, transaction_1.getTransactionMetaById)(__classPrivateFieldGet(this, _QuoteStatusManager_messenger, "f"), entry.txMetaId);
211
+ if (!txMeta) {
212
+ continue;
213
+ }
214
+ // Reconcile swaps and bridges alike: a terminal source transaction
215
+ // finalizes the quote status. `hasNestedSwapTransactions` also covers
216
+ // batch/7702 swaps whose type may still read as `batch` rather than `swap`.
217
+ const isCrossChainTrade = (txMeta.type !== undefined && (0, transaction_1.isCrossChainTx)(txMeta.type)) ||
218
+ (0, transaction_1.hasNestedSwapTransactions)(txMeta);
219
+ if (!isCrossChainTrade) {
220
+ continue;
221
+ }
222
+ if (txMeta.status === transaction_controller_1.TransactionStatus.confirmed) {
223
+ this.reportFinalised(entry.txMetaId, true);
224
+ }
225
+ else if (txMeta.status === transaction_controller_1.TransactionStatus.failed ||
226
+ txMeta.status === transaction_controller_1.TransactionStatus.dropped) {
227
+ this.reportFinalised(entry.txMetaId, false);
228
+ }
229
+ }
230
+ }
231
+ /**
232
+ * Fetches the current quote status from the backend with automatic retries.
233
+ *
234
+ * Unlike {@link reportSubmitted} and {@link reportFinalised}, this is a
235
+ * read-only query and does not mutate tracked entries. Returns `null` when
236
+ * the manager is disabled ({@link enabled} is `false`).
237
+ *
238
+ * @param quoteId - Identifier of the quote whose status should be fetched.
239
+ * @param options - Retry configuration.
240
+ * @param options.maxRetries - Maximum number of retries after the initial attempt.
241
+ * @param options.delayMsBetweenRetries - Delay in milliseconds between attempts.
242
+ */
243
+ getStatus(quoteId, options = {
244
+ maxRetries: 0,
245
+ delayMsBetweenRetries: 1000,
246
+ }) {
247
+ if (!__classPrivateFieldGet(this, _QuoteStatusManager_isEnabled, "f")?.call(this)) {
248
+ return;
249
+ }
250
+ __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusApiService, "f")
251
+ .getQuoteStatusWithRetry({
252
+ quoteId,
253
+ }, {
254
+ maxRetries: options.maxRetries ?? 0,
255
+ delayMsBetweenRetries: options.delayMsBetweenRetries ?? 1000,
256
+ })
257
+ .catch(() => undefined);
258
+ }
259
+ }
260
+ exports.QuoteStatusManager = QuoteStatusManager;
261
+ _QuoteStatusManager_messenger = new WeakMap(), _QuoteStatusManager_quoteStatusApiService = new WeakMap(), _QuoteStatusManager_quoteStatusEntryStore = new WeakMap(), _QuoteStatusManager_isEnabled = new WeakMap(), _QuoteStatusManager_onError = new WeakMap(), _QuoteStatusManager_updateIntervalMs = new WeakMap(), _QuoteStatusManager_retryIntervalId = new WeakMap(), _QuoteStatusManager_instances = new WeakSet(), _QuoteStatusManager_processInitial = function _QuoteStatusManager_processInitial() {
262
+ for (const entry of __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").values()) {
263
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processEntry).call(this, entry);
264
+ }
265
+ // Terminal entries (Completed/Expired) are retained, so the store is never
266
+ // empty. Only start the retry timer when there is an entry whose status
267
+ // still needs to be reported; otherwise it would tick forever doing nothing.
268
+ if (__classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_hasPendingUpdates).call(this)) {
269
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_ensureRetryTimerRunning).call(this);
270
+ }
271
+ }, _QuoteStatusManager_ensureRetryTimerRunning = function _QuoteStatusManager_ensureRetryTimerRunning() {
272
+ if (__classPrivateFieldGet(this, _QuoteStatusManager_retryIntervalId, "f") !== null) {
273
+ return;
274
+ }
275
+ __classPrivateFieldSet(this, _QuoteStatusManager_retryIntervalId, setInterval(() => __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processRetries).call(this), __classPrivateFieldGet(this, _QuoteStatusManager_updateIntervalMs, "f")), "f");
276
+ }, _QuoteStatusManager_stopRetryTimer = function _QuoteStatusManager_stopRetryTimer() {
277
+ if (__classPrivateFieldGet(this, _QuoteStatusManager_retryIntervalId, "f") !== null) {
278
+ clearInterval(__classPrivateFieldGet(this, _QuoteStatusManager_retryIntervalId, "f"));
279
+ __classPrivateFieldSet(this, _QuoteStatusManager_retryIntervalId, null, "f");
280
+ }
281
+ }, _QuoteStatusManager_processRetries = function _QuoteStatusManager_processRetries() {
282
+ if (!__classPrivateFieldGet(this, _QuoteStatusManager_isEnabled, "f")?.call(this)) {
283
+ return;
284
+ }
285
+ // Snapshot first: `#processEntry` can mutate the store (e.g. transitioning an
286
+ // accepted entry to a terminal state), so iterating a live iterator would be
287
+ // unsafe.
288
+ const entries = [...__classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").values()];
289
+ if (!__classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_hasPendingUpdates).call(this)) {
290
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_stopRetryTimer).call(this);
291
+ return;
292
+ }
293
+ for (const entry of entries) {
294
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processEntry).call(this, entry);
295
+ }
296
+ }, _QuoteStatusManager_hasPendingUpdates = function _QuoteStatusManager_hasPendingUpdates() {
297
+ for (const entry of __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").values()) {
298
+ const { state } = entry.status;
299
+ const isTerminal = state === constants_1.QuoteStatusState.Completed ||
300
+ state === constants_1.QuoteStatusState.Expired;
301
+ if (!isTerminal && entry.acknowledgedState !== state) {
302
+ return true;
303
+ }
304
+ }
305
+ return false;
306
+ }, _QuoteStatusManager_stopRetryTimerIfIdle = function _QuoteStatusManager_stopRetryTimerIfIdle() {
307
+ if (!__classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_hasPendingUpdates).call(this)) {
308
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_stopRetryTimer).call(this);
309
+ }
310
+ }, _QuoteStatusManager_processEntry = function _QuoteStatusManager_processEntry(entry) {
311
+ // The backend already accepted the entry's current status, so there is
312
+ // nothing new to report. The entry is kept tracked (e.g. a `Submitted`
313
+ // quote awaiting finalization) until its status advances past the
314
+ // acknowledged one, at which point it is reprocessed.
315
+ if (entry.acknowledgedState === entry.status.state) {
316
+ return;
317
+ }
318
+ const sentStatus = entry.status.state;
319
+ const sentStatusBackend = constants_1.QuoteStatusStateToBackendStatus[sentStatus];
320
+ // Terminal states (`Completed`/`Expired`) have no backend status to report.
321
+ // The entry is retained so future interactions with the quote are rejected.
322
+ if (sentStatusBackend === null) {
323
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_stopRetryTimerIfIdle).call(this);
324
+ return;
325
+ }
326
+ // Re-checked before each retry attempt so an in-flight retry stops early when
327
+ // the entry has since advanced, been acknowledged, become terminal, or
328
+ // expired (a later `reportFinalised` re-triggers processing). This avoids
329
+ // firing a request the backend would reject (e.g. `SUBMITTED` after
330
+ // finalization).
331
+ const retry = () => {
332
+ const live = __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").get(quote_status_entry_store_1.QuoteStatusEntryStore.hash(entry));
333
+ return Boolean(live &&
334
+ live.status.state === sentStatus &&
335
+ live.acknowledgedState !== sentStatus);
336
+ };
337
+ __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusApiService, "f")
338
+ .updateQuoteStatusWithRetry({
339
+ quoteId: entry.quoteId,
340
+ srcTxHash: entry.srcTxHash,
341
+ newStatus: sentStatusBackend,
342
+ }, {
343
+ maxRetries: 5,
344
+ delayMsBetweenRetries: 3000,
345
+ retry,
346
+ })
347
+ .then((outcome) => {
348
+ switch (outcome.type) {
349
+ case constants_1.QuoteStatusFetchWithRetryOutcomeType.Accepted: {
350
+ const current = __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").get(quote_status_entry_store_1.QuoteStatusEntryStore.hash(entry));
351
+ // The entry can only be absent if the store was cleared (e.g. via
352
+ // `destroy`) while the request was in flight; nothing left to do.
353
+ if (!current) {
354
+ return undefined;
355
+ }
356
+ if (current.status.state !== sentStatus) {
357
+ // The status advanced mid-flight; report the newer status.
358
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processEntry).call(this, current);
359
+ return undefined;
360
+ }
361
+ if (sentStatus === constants_1.QuoteStatusState.FinalizedSuccess ||
362
+ sentStatus === constants_1.QuoteStatusState.FinalizedFailed) {
363
+ // A finalized status was accepted; advance to the terminal
364
+ // `Completed` state and keep the entry so any later duplicate
365
+ // `reportSubmitted` for this quote is rejected instead of looping.
366
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_markCompleted).call(this, current);
367
+ // Call getStatus to complete the flow on the backend.
368
+ // We discard any value for now, this is used purely
369
+ // to notify backend that the flow is complete.
370
+ this.getStatus(entry.quoteId, { maxRetries: 1 });
371
+ return undefined;
372
+ }
373
+ // A non-final status (e.g. `Submitted`) was accepted. The quote is
374
+ // not done yet: it still needs to be finalized via a later
375
+ // `reportFinalised`, which looks the entry up by `txMetaId`. Keep
376
+ // the entry tracked and record the acknowledgement so the retry
377
+ // loop stops re-sending the already-accepted status.
378
+ current.acknowledgedState = sentStatus;
379
+ __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").update(current);
380
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_stopRetryTimerIfIdle).call(this);
381
+ return undefined;
382
+ }
383
+ case constants_1.QuoteStatusFetchWithRetryOutcomeType.NonRetryable:
384
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_handleNonRetryableUpdateStatusError).call(this, entry, outcome);
385
+ return undefined;
386
+ case constants_1.QuoteStatusFetchWithRetryOutcomeType.Interrupted:
387
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processEntry).call(this, entry);
388
+ return undefined;
389
+ case constants_1.QuoteStatusFetchWithRetryOutcomeType.RetryableExhausted:
390
+ entry.lastAttemptAt = Date.now();
391
+ __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").update(entry);
392
+ return undefined;
393
+ default:
394
+ return undefined;
395
+ }
396
+ })
397
+ .catch(() => {
398
+ entry.lastAttemptAt = Date.now();
399
+ __classPrivateFieldGet(this, _QuoteStatusManager_quoteStatusEntryStore, "f").update(entry);
400
+ });
401
+ }, _QuoteStatusManager_markCompleted = function _QuoteStatusManager_markCompleted(entry, finalizedState) {
402
+ if (finalizedState && entry.status.canTransitionTo(finalizedState)) {
403
+ entry.status.transitionTo(finalizedState);
404
+ }
405
+ if (entry.status.canTransitionTo(constants_1.QuoteStatusState.Completed)) {
406
+ entry.status.transitionTo(constants_1.QuoteStatusState.Completed);
407
+ }
408
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_stopRetryTimerIfIdle).call(this);
409
+ }, _QuoteStatusManager_markExpired = function _QuoteStatusManager_markExpired(entry) {
410
+ if (entry.status.canTransitionTo(constants_1.QuoteStatusState.Expired)) {
411
+ entry.status.transitionTo(constants_1.QuoteStatusState.Expired);
412
+ }
413
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_stopRetryTimerIfIdle).call(this);
414
+ }, _QuoteStatusManager_handleNonRetryableUpdateStatusError = function _QuoteStatusManager_handleNonRetryableUpdateStatusError(entry, outcome) {
415
+ const { response } = outcome;
416
+ const backendFinalizedToState = {
417
+ [constants_1.QuoteStatusBackendStatus.FinalizedSuccess]: constants_1.QuoteStatusState.FinalizedSuccess,
418
+ [constants_1.QuoteStatusBackendStatus.FinalizedFailed]: constants_1.QuoteStatusState.FinalizedFailed,
419
+ };
420
+ // The transition we requested is invalid because the backend is already in a
421
+ // terminal state (e.g. we re-sent `SUBMITTED` after finalization). There is
422
+ // nothing left to report, so advance the entry to `Completed` and keep it.
423
+ if (response?.type ===
424
+ constants_1.QuoteStatusUpdateBackendErrorType.InvalidStatusTransaction &&
425
+ (response.currentStatus === constants_1.QuoteStatusBackendStatus.FinalizedSuccess ||
426
+ response.currentStatus === constants_1.QuoteStatusBackendStatus.FinalizedFailed)) {
427
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_markCompleted).call(this, entry, backendFinalizedToState[response.currentStatus]);
428
+ return;
429
+ }
430
+ // For mismatch errors the backend reports the status it currently has, which
431
+ // lets us reconcile our local state. The discriminant check also narrows
432
+ // `response` to the variant carrying `currentStatus`.
433
+ if (response?.type ===
434
+ constants_1.QuoteStatusUpdateBackendErrorType.InvalidStatusTransaction ||
435
+ response?.type ===
436
+ constants_1.QuoteStatusUpdateBackendErrorType.QuoteStatusOnChainMismatch) {
437
+ const nextState = backendFinalizedToState[response.currentStatus];
438
+ if (nextState) {
439
+ // We are behind the backend's terminal status; advance our local state
440
+ // and reprocess so the correct finalization status is reported.
441
+ if (entry.status.canTransitionTo(nextState)) {
442
+ entry.status.transitionTo(nextState);
443
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_processEntry).call(this, entry);
444
+ return;
445
+ }
446
+ // The backend reports a finalized status but we cannot transition there.
447
+ // If we already hold that same finalized state, this can be a stale
448
+ // `SUBMITTED` response racing with an in-flight finalized update. Do not
449
+ // complete in this case: keep the entry pending so the finalized update
450
+ // can continue retrying on transient backend errors such as
451
+ // `CONCURRENT_UPDATE`.
452
+ if (entry.status.state === nextState) {
453
+ return;
454
+ }
455
+ // Any other non-transitionable finalized mismatch indicates we've reached
456
+ // (or passed) a terminal state but disagree with the backend's final
457
+ // status. Converge to `Completed` (kept) rather than expiring.
458
+ if (entry.status.state !== constants_1.QuoteStatusState.Completed &&
459
+ entry.status.state !== constants_1.QuoteStatusState.Expired) {
460
+ // Genuine discrepancy between our finalized status and the backend's
461
+ // (e.g. we observed the opposite outcome); surface it for visibility,
462
+ // but still complete the entry instead of looping or abandoning it.
463
+ __classPrivateFieldGet(this, _QuoteStatusManager_onError, "f")?.call(this, new errors_1.QuoteStatusUpdateError(`reporting finalization status but entry cannot transition from "${entry.status.state}" to "${nextState}"`, { quoteId: entry.quoteId }));
464
+ }
465
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_markCompleted).call(this, entry);
466
+ return;
467
+ }
468
+ }
469
+ // Any non-retryable error we could not reconcile means we cannot make
470
+ // progress on this entry, so abandon it rather than leaving it stuck.
471
+ __classPrivateFieldGet(this, _QuoteStatusManager_instances, "m", _QuoteStatusManager_markExpired).call(this, entry);
472
+ __classPrivateFieldGet(this, _QuoteStatusManager_onError, "f")?.call(this, new errors_1.QuoteStatusUpdateError(`abandoning entry due to non-retryable error`, {
473
+ quoteId: entry.quoteId,
474
+ errorType: response?.type,
475
+ }));
476
+ };
477
+ //# sourceMappingURL=quotes-status-manager.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quotes-status-manager.cjs","sourceRoot":"","sources":["../../src/quote-status-manager/quotes-status-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { TransactionStatus } from '@metamask/transaction-controller';\n\nimport { BridgeClientId, BridgeStatusControllerMessenger } from '../types';\nimport {\n getTransactionMetaById,\n hasNestedSwapTransactions,\n isCrossChainTx,\n} from '../utils/transaction';\nimport {\n QuoteStatusState,\n QuoteStatusStateToBackendStatus,\n QuoteStatusUpdateBackendErrorType,\n QuoteStatusBackendStatus,\n QuoteStatusFetchWithRetryOutcomeType,\n} from './constants';\nimport { QuoteStatusUpdateError } from './errors';\nimport { QuoteStatusApiService } from './quote-status-api-service';\nimport { QuoteStatusEntryStore } from './quote-status-entry-store';\nimport { QuoteStatusStateFsm } from './quote-status-state-fsm';\nimport { QuoteStatusUpdateWithRetryOutcome } from './quote-status-update-with-retry-outcome';\nimport { QuoteStatusPersistEntry, QuoteStatusRuntimeEntry } from './types';\n\n/**\n * Tracks bridge/swap quotes through their lifecycle and keeps the backend in\n * sync with the latest known status of each quote.\n *\n * Quotes are reported via {@link reportSubmitted} and {@link reportFinalised},\n * stored as runtime entries, and pushed to the backend. Updates that fail in a\n * retryable way are retried on a periodic timer until each entry reaches a\n * terminal state, at which point it is evicted and the timer stops once the\n * store is empty.\n */\nexport class QuoteStatusManager {\n readonly #messenger: BridgeStatusControllerMessenger;\n\n readonly #quoteStatusApiService: QuoteStatusApiService;\n\n readonly #quoteStatusEntryStore: QuoteStatusEntryStore;\n\n readonly #isEnabled: (() => boolean) | undefined;\n\n readonly #onError: ((error: QuoteStatusUpdateError) => void) | undefined;\n\n readonly #updateIntervalMs: number;\n\n /**\n * Handle for the periodic retry timer. While running, every\n * {@link QUOTE_STATUS_UPDATE_RETRY_INTERVAL_MS} all entries that have not yet\n * reached a terminal state (Completed/Expired) are re-processed. It is started\n * lazily when there is work to do and stopped once the store is empty.\n */\n #retryIntervalId: ReturnType<typeof setInterval> | null = null;\n\n /**\n * Creates a new manager and immediately processes any persisted entries.\n *\n * @param options - Constructor options.\n * @param options.messenger - Messenger used to communicate with the backend\n * API service.\n * @param options.clientId - Identifier of the client making the requests.\n * @param options.clientProduct - Name of the client product making the\n * requests.\n * @param options.clientVersion - Optional version of the client product.\n * @param options.apiBaseUrl - Base URL of the quote status backend API.\n * @param options.onPersistUpdates - Callback invoked to persist entry updates.\n * @param options.onError - Optional callback invoked when a non-recoverable\n * error occurs.\n * @param options.isEnabled - Optional predicate gating whether the manager\n * performs any work.\n * @param options.entryTtlMs - Time-to-live, in milliseconds, after which a\n * tracked entry is evicted.\n * @param options.updateIntervalMs - How often the manager re-processes entries that\n * have not yet reached a terminal state\n * @param options.initialData - Persisted entries to rehydrate on startup.\n */\n constructor({\n messenger,\n clientId,\n clientProduct,\n clientVersion,\n apiBaseUrl,\n onError,\n isEnabled,\n onPersistUpdates,\n entryTtlMs,\n updateIntervalMs,\n initialData,\n }: {\n messenger: BridgeStatusControllerMessenger;\n clientId: BridgeClientId;\n clientProduct: string;\n clientVersion?: string;\n apiBaseUrl: string;\n onPersistUpdates: (\n updates: Record<string, QuoteStatusPersistEntry>,\n ) => void;\n onError?: (error: QuoteStatusUpdateError) => void;\n isEnabled?: () => boolean;\n entryTtlMs: number;\n updateIntervalMs: number;\n initialData: Record<string, QuoteStatusPersistEntry>;\n }) {\n this.#isEnabled = isEnabled;\n this.#onError = onError;\n this.#updateIntervalMs = updateIntervalMs;\n this.#messenger = messenger;\n\n this.#quoteStatusApiService = new QuoteStatusApiService({\n messenger,\n clientId,\n clientProduct,\n clientVersion,\n apiBaseUrl,\n onError,\n });\n\n this.#quoteStatusEntryStore = new QuoteStatusEntryStore({\n onPersistUpdates,\n entryTtlMs,\n initial: initialData,\n });\n }\n\n /**\n * Whether quote-status tracking and backend sync are currently active.\n *\n * Reflects the latest value returned by the optional `isEnabled` predicate\n * supplied at construction time. When no predicate was provided, this is\n * always `false` and all public methods that gate on enablement no-op.\n *\n * @returns `true` when the `isEnabled` predicate returns a truthy value.\n */\n get enabled(): boolean {\n return Boolean(this.#isEnabled?.());\n }\n\n /**\n * Reports that a previously submitted quote has finalized on-chain.\n *\n * Looks up the tracked entry by its transaction metadata id, transitions it to\n * the appropriate terminal state, and processes the update. No-ops when the\n * manager is disabled, and surfaces an error when the entry is missing or\n * cannot transition to the finalized state.\n *\n * @param txMetaId - Transaction metadata id of the finalized quote.\n * @param success - Whether the transaction finalized successfully.\n */\n reportFinalised(txMetaId: string, success: boolean): void {\n if (!this.#isEnabled?.()) {\n return;\n }\n\n const entry = this.#quoteStatusEntryStore.getByTxMetaId(txMetaId);\n\n if (!entry) {\n this.#onError?.(\n new QuoteStatusUpdateError(\n 'reporting finalization status but entry was not found',\n { quoteId: '' },\n ),\n );\n return;\n }\n\n const nextState = success\n ? QuoteStatusState.FinalizedSuccess\n : QuoteStatusState.FinalizedFailed;\n\n if (!entry.status.canTransitionTo(nextState)) {\n // This is expected, there are race conditions where\n // reportFinalized can be called twice. If the second\n // call fails due to the first completed sucesfully\n // backend will report that we cannot transition outside\n // a final state, which is correct and we can safely abort\n // the flow.\n return;\n }\n\n entry.status.transitionTo(nextState);\n this.#ensureRetryTimerRunning();\n this.#processEntry(entry);\n }\n\n /**\n * Reports that a quote has been submitted on-chain and begins tracking it.\n *\n * Creates a new entry in the `Submitted` state, starts the retry timer, and\n * processes the initial status update. No-ops when the manager is disabled,\n * and surfaces an error if the entry cannot be retrieved after being stored.\n *\n * @param quoteId - Identifier of the submitted quote.\n * @param srcTxHash - Hash of the source-chain transaction for the quote.\n * @param txMetaId - Optional transaction metadata id used to correlate\n * finalization reports.\n */\n reportSubmitted(quoteId: string, srcTxHash: string, txMetaId?: string): void {\n if (!this.#isEnabled?.()) {\n return;\n }\n\n // Once a quote has advanced past `Submitted` (finalized or terminal), it is\n // done: reporting `SUBMITTED` again would be rejected by the backend as an\n // invalid transition. Retained terminal entries let us recognize and drop\n // these late/duplicate submissions instead of looping on a 400.\n const isQuoteAlreadyTracked = this.#quoteStatusEntryStore\n .getByQuoteId(quoteId)\n .some((entry) => entry.status.state !== QuoteStatusState.Submitted);\n if (isQuoteAlreadyTracked) {\n return;\n }\n\n const entryKey = QuoteStatusEntryStore.hash({\n quoteId,\n srcTxHash,\n });\n\n const entry = this.#quoteStatusEntryStore.put(entryKey, {\n quoteId,\n srcTxHash,\n txMetaId,\n status: new QuoteStatusStateFsm(QuoteStatusState.Submitted),\n });\n\n this.#ensureRetryTimerRunning();\n this.#processEntry(entry);\n }\n\n /**\n * Tears down the manager by stopping the retry timer and clearing all tracked\n * entries.\n */\n destroy(): void {\n this.#stopRetryTimer();\n this.#quoteStatusEntryStore.clear();\n }\n\n /**\n * Reconciles quote-status entries whose finalization was missed while the\n * client was closed. Called once during initialization.\n *\n * Swap/bridge source transactions report their final status through the\n * `TransactionController:transactionStatusUpdated` subscription\n * ({@link #onTransactionConfirmed}/{@link #onTransactionFailed}). When a source\n * transaction reaches a terminal state while the client is not running, that\n * event is never re-emitted on the next startup, so the persisted entry would\n * remain `Submitted` until it expires via TTL. This replays the missed terminal\n * event for both swaps and bridges: a confirmed source transaction is reported\n * as a finalized success and a failed/dropped one as a finalized failure.\n *\n * `rejected` is ignored because the transaction was never broadcast.\n */\n init(): void {\n this.#processInitial();\n\n for (const entry of this.#quoteStatusEntryStore.values()) {\n // Only entries still awaiting finalization need catching up. Entries\n // already in a finalized state are re-sent by `processInitial()`.\n if (\n entry.status.state !== QuoteStatusState.Submitted ||\n !entry.txMetaId\n ) {\n continue;\n }\n\n const txMeta = getTransactionMetaById(this.#messenger, entry.txMetaId);\n if (!txMeta) {\n continue;\n }\n\n // Reconcile swaps and bridges alike: a terminal source transaction\n // finalizes the quote status. `hasNestedSwapTransactions` also covers\n // batch/7702 swaps whose type may still read as `batch` rather than `swap`.\n const isCrossChainTrade =\n (txMeta.type !== undefined && isCrossChainTx(txMeta.type)) ||\n hasNestedSwapTransactions(txMeta);\n if (!isCrossChainTrade) {\n continue;\n }\n\n if (txMeta.status === TransactionStatus.confirmed) {\n this.reportFinalised(entry.txMetaId, true);\n } else if (\n txMeta.status === TransactionStatus.failed ||\n txMeta.status === TransactionStatus.dropped\n ) {\n this.reportFinalised(entry.txMetaId, false);\n }\n }\n }\n\n /**\n * Fetches the current quote status from the backend with automatic retries.\n *\n * Unlike {@link reportSubmitted} and {@link reportFinalised}, this is a\n * read-only query and does not mutate tracked entries. Returns `null` when\n * the manager is disabled ({@link enabled} is `false`).\n *\n * @param quoteId - Identifier of the quote whose status should be fetched.\n * @param options - Retry configuration.\n * @param options.maxRetries - Maximum number of retries after the initial attempt.\n * @param options.delayMsBetweenRetries - Delay in milliseconds between attempts.\n */\n getStatus(\n quoteId: string,\n options: {\n maxRetries?: number;\n delayMsBetweenRetries?: number;\n } = {\n maxRetries: 0,\n delayMsBetweenRetries: 1000,\n },\n ): void {\n if (!this.#isEnabled?.()) {\n return;\n }\n\n this.#quoteStatusApiService\n .getQuoteStatusWithRetry(\n {\n quoteId,\n },\n {\n maxRetries: options.maxRetries ?? 0,\n delayMsBetweenRetries: options.delayMsBetweenRetries ?? 1000,\n },\n // Errors already reported by #onError handlers of `getQuoteStatusWithRetry()`\n )\n .catch(() => undefined);\n }\n\n /**\n * Processes every entry rehydrated from persisted data on startup and starts\n * the retry timer if any entries still require further updates.\n */\n #processInitial(): void {\n for (const entry of this.#quoteStatusEntryStore.values()) {\n this.#processEntry(entry);\n }\n\n // Terminal entries (Completed/Expired) are retained, so the store is never\n // empty. Only start the retry timer when there is an entry whose status\n // still needs to be reported; otherwise it would tick forever doing nothing.\n if (this.#hasPendingUpdates()) {\n this.#ensureRetryTimerRunning();\n }\n }\n\n /**\n * Starts the periodic retry timer if it is not already running.\n *\n * The timer re-processes every non-terminal entry on each tick. It is\n * idempotent so callers can invoke it freely whenever new work is enqueued.\n */\n #ensureRetryTimerRunning(): void {\n if (this.#retryIntervalId !== null) {\n return;\n }\n\n this.#retryIntervalId = setInterval(\n () => this.#processRetries(),\n this.#updateIntervalMs,\n );\n }\n\n /**\n * Stops the periodic retry timer if it is running.\n */\n #stopRetryTimer(): void {\n if (this.#retryIntervalId !== null) {\n clearInterval(this.#retryIntervalId);\n this.#retryIntervalId = null;\n }\n }\n\n /**\n * Retry tick: re-processes every entry. Reading `values()` first transitions\n * TTL-expired entries to `Expired` (keeping them), and `#processEntry` no-ops\n * for terminal/acknowledged entries. When no entry has a status left to report\n * the timer stops until new work is enqueued.\n */\n #processRetries(): void {\n if (!this.#isEnabled?.()) {\n return;\n }\n\n // Snapshot first: `#processEntry` can mutate the store (e.g. transitioning an\n // accepted entry to a terminal state), so iterating a live iterator would be\n // unsafe.\n const entries = [...this.#quoteStatusEntryStore.values()];\n\n if (!this.#hasPendingUpdates()) {\n this.#stopRetryTimer();\n return;\n }\n\n for (const entry of entries) {\n this.#processEntry(entry);\n }\n }\n\n /**\n * Returns whether any tracked entry still has a status that needs to be\n * reported to the backend.\n *\n * Terminal entries (`Completed`/`Expired`) and entries whose current status\n * has already been acknowledged are excluded, so this is the signal used to\n * decide whether the retry timer has any work left to do.\n *\n * @returns `true` when at least one entry has an unreported status.\n */\n #hasPendingUpdates(): boolean {\n for (const entry of this.#quoteStatusEntryStore.values()) {\n const { state } = entry.status;\n const isTerminal =\n state === QuoteStatusState.Completed ||\n state === QuoteStatusState.Expired;\n if (!isTerminal && entry.acknowledgedState !== state) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Stops the retry timer when there are no entries left whose status needs\n * reporting. Since terminal entries are retained, the store is never empty, so\n * the timer's idle condition is \"no pending updates\" rather than \"no entries\".\n */\n #stopRetryTimerIfIdle(): void {\n if (!this.#hasPendingUpdates()) {\n this.#stopRetryTimer();\n }\n }\n\n /**\n * Pushes a single entry's current status to the backend and reconciles the\n * local state with the request outcome.\n *\n * Terminal entries are kept but no longer reported. Accepted finalized updates\n * advance the entry to `Completed` (kept so duplicate reports are rejected);\n * accepted non-final updates (e.g. `Submitted`) are kept tracked so a later\n * {@link reportFinalised} can find them, while being flagged so the retry loop\n * stops re-sending the acknowledged status. Updates whose status advanced\n * mid-flight are reprocessed, retryable failures are left for the next retry\n * tick, and non-retryable failures are delegated to\n * {@link #handleNonRetryableUpdateStatusError}.\n *\n * @param entry - The runtime entry to process.\n */\n #processEntry(entry: QuoteStatusRuntimeEntry): void {\n // The backend already accepted the entry's current status, so there is\n // nothing new to report. The entry is kept tracked (e.g. a `Submitted`\n // quote awaiting finalization) until its status advances past the\n // acknowledged one, at which point it is reprocessed.\n if (entry.acknowledgedState === entry.status.state) {\n return;\n }\n\n const sentStatus = entry.status.state;\n const sentStatusBackend = QuoteStatusStateToBackendStatus[sentStatus];\n\n // Terminal states (`Completed`/`Expired`) have no backend status to report.\n // The entry is retained so future interactions with the quote are rejected.\n if (sentStatusBackend === null) {\n this.#stopRetryTimerIfIdle();\n return;\n }\n\n // Re-checked before each retry attempt so an in-flight retry stops early when\n // the entry has since advanced, been acknowledged, become terminal, or\n // expired (a later `reportFinalised` re-triggers processing). This avoids\n // firing a request the backend would reject (e.g. `SUBMITTED` after\n // finalization).\n const retry = (): boolean => {\n const live = this.#quoteStatusEntryStore.get(\n QuoteStatusEntryStore.hash(entry),\n );\n return Boolean(\n live &&\n live.status.state === sentStatus &&\n live.acknowledgedState !== sentStatus,\n );\n };\n\n this.#quoteStatusApiService\n .updateQuoteStatusWithRetry(\n {\n quoteId: entry.quoteId,\n srcTxHash: entry.srcTxHash,\n newStatus: sentStatusBackend,\n },\n {\n maxRetries: 5,\n delayMsBetweenRetries: 3000,\n retry,\n },\n )\n .then((outcome) => {\n switch (outcome.type) {\n case QuoteStatusFetchWithRetryOutcomeType.Accepted: {\n const current = this.#quoteStatusEntryStore.get(\n QuoteStatusEntryStore.hash(entry),\n );\n // The entry can only be absent if the store was cleared (e.g. via\n // `destroy`) while the request was in flight; nothing left to do.\n if (!current) {\n return undefined;\n }\n if (current.status.state !== sentStatus) {\n // The status advanced mid-flight; report the newer status.\n this.#processEntry(current);\n return undefined;\n }\n if (\n sentStatus === QuoteStatusState.FinalizedSuccess ||\n sentStatus === QuoteStatusState.FinalizedFailed\n ) {\n // A finalized status was accepted; advance to the terminal\n // `Completed` state and keep the entry so any later duplicate\n // `reportSubmitted` for this quote is rejected instead of looping.\n this.#markCompleted(current);\n // Call getStatus to complete the flow on the backend.\n // We discard any value for now, this is used purely\n // to notify backend that the flow is complete.\n this.getStatus(entry.quoteId, { maxRetries: 1 });\n return undefined;\n }\n // A non-final status (e.g. `Submitted`) was accepted. The quote is\n // not done yet: it still needs to be finalized via a later\n // `reportFinalised`, which looks the entry up by `txMetaId`. Keep\n // the entry tracked and record the acknowledgement so the retry\n // loop stops re-sending the already-accepted status.\n current.acknowledgedState = sentStatus;\n this.#quoteStatusEntryStore.update(current);\n this.#stopRetryTimerIfIdle();\n return undefined;\n }\n case QuoteStatusFetchWithRetryOutcomeType.NonRetryable:\n this.#handleNonRetryableUpdateStatusError(entry, outcome);\n return undefined;\n case QuoteStatusFetchWithRetryOutcomeType.Interrupted:\n this.#processEntry(entry);\n return undefined;\n case QuoteStatusFetchWithRetryOutcomeType.RetryableExhausted:\n entry.lastAttemptAt = Date.now();\n this.#quoteStatusEntryStore.update(entry);\n return undefined;\n default:\n return undefined;\n }\n })\n .catch(() => {\n entry.lastAttemptAt = Date.now();\n this.#quoteStatusEntryStore.update(entry);\n });\n }\n\n /**\n * Advances an entry to the terminal `Completed` state and stops the retry\n * timer if no work remains. The entry is kept in the store so later duplicate\n * reports for the same quote are recognized and rejected.\n *\n * @param entry - The runtime entry to complete.\n * @param finalizedState - Optional finalized state the backend reports as\n * current. When provided and reachable, the entry is first advanced through it\n * so the FSM's forward-only path (`Submitted -> FinalizedX -> Completed`) is\n * respected before reaching `Completed`.\n */\n #markCompleted(\n entry: QuoteStatusRuntimeEntry,\n finalizedState?: QuoteStatusState,\n ): void {\n if (finalizedState && entry.status.canTransitionTo(finalizedState)) {\n entry.status.transitionTo(finalizedState);\n }\n\n if (entry.status.canTransitionTo(QuoteStatusState.Completed)) {\n entry.status.transitionTo(QuoteStatusState.Completed);\n }\n\n this.#stopRetryTimerIfIdle();\n }\n\n /**\n * Advances an entry to the terminal `Expired` state (abandoning it) and stops\n * the retry timer if no work remains. The entry is kept in the store so later\n * interactions with the same quote are rejected.\n *\n * @param entry - The runtime entry to abandon.\n */\n #markExpired(entry: QuoteStatusRuntimeEntry): void {\n if (entry.status.canTransitionTo(QuoteStatusState.Expired)) {\n entry.status.transitionTo(QuoteStatusState.Expired);\n }\n\n this.#stopRetryTimerIfIdle();\n }\n\n /**\n * Reconciles local state in response to a non-retryable backend error.\n *\n * When the backend reports a terminal status, the entry is either advanced to\n * match and reprocessed (if it is behind) or converged to `Completed` (if it\n * is already finalized), surfacing an error only for a genuine status\n * discrepancy. Other non-retryable errors that cannot be reconciled mark the\n * entry `Expired` (abandoned). Entries are always kept in the store so future\n * interactions with the quote are rejected rather than looping.\n *\n * @param entry - The runtime entry whose update failed.\n * @param outcome - The non-retryable outcome returned by the API service,\n * including the backend response used for reconciliation.\n */\n #handleNonRetryableUpdateStatusError(\n entry: QuoteStatusRuntimeEntry,\n outcome: QuoteStatusUpdateWithRetryOutcome,\n ): void {\n const { response } = outcome;\n\n const backendFinalizedToState: Partial<\n Record<QuoteStatusBackendStatus, QuoteStatusState>\n > = {\n [QuoteStatusBackendStatus.FinalizedSuccess]:\n QuoteStatusState.FinalizedSuccess,\n [QuoteStatusBackendStatus.FinalizedFailed]:\n QuoteStatusState.FinalizedFailed,\n };\n\n // The transition we requested is invalid because the backend is already in a\n // terminal state (e.g. we re-sent `SUBMITTED` after finalization). There is\n // nothing left to report, so advance the entry to `Completed` and keep it.\n if (\n response?.type ===\n QuoteStatusUpdateBackendErrorType.InvalidStatusTransaction &&\n (response.currentStatus === QuoteStatusBackendStatus.FinalizedSuccess ||\n response.currentStatus === QuoteStatusBackendStatus.FinalizedFailed)\n ) {\n this.#markCompleted(\n entry,\n backendFinalizedToState[response.currentStatus],\n );\n return;\n }\n\n // For mismatch errors the backend reports the status it currently has, which\n // lets us reconcile our local state. The discriminant check also narrows\n // `response` to the variant carrying `currentStatus`.\n if (\n response?.type ===\n QuoteStatusUpdateBackendErrorType.InvalidStatusTransaction ||\n response?.type ===\n QuoteStatusUpdateBackendErrorType.QuoteStatusOnChainMismatch\n ) {\n const nextState = backendFinalizedToState[response.currentStatus];\n\n if (nextState) {\n // We are behind the backend's terminal status; advance our local state\n // and reprocess so the correct finalization status is reported.\n if (entry.status.canTransitionTo(nextState)) {\n entry.status.transitionTo(nextState);\n this.#processEntry(entry);\n return;\n }\n\n // The backend reports a finalized status but we cannot transition there.\n // If we already hold that same finalized state, this can be a stale\n // `SUBMITTED` response racing with an in-flight finalized update. Do not\n // complete in this case: keep the entry pending so the finalized update\n // can continue retrying on transient backend errors such as\n // `CONCURRENT_UPDATE`.\n if (entry.status.state === nextState) {\n return;\n }\n\n // Any other non-transitionable finalized mismatch indicates we've reached\n // (or passed) a terminal state but disagree with the backend's final\n // status. Converge to `Completed` (kept) rather than expiring.\n if (\n entry.status.state !== QuoteStatusState.Completed &&\n entry.status.state !== QuoteStatusState.Expired\n ) {\n // Genuine discrepancy between our finalized status and the backend's\n // (e.g. we observed the opposite outcome); surface it for visibility,\n // but still complete the entry instead of looping or abandoning it.\n this.#onError?.(\n new QuoteStatusUpdateError(\n `reporting finalization status but entry cannot transition from \"${entry.status.state}\" to \"${nextState}\"`,\n { quoteId: entry.quoteId },\n ),\n );\n }\n this.#markCompleted(entry);\n return;\n }\n }\n\n // Any non-retryable error we could not reconcile means we cannot make\n // progress on this entry, so abandon it rather than leaving it stuck.\n this.#markExpired(entry);\n this.#onError?.(\n new QuoteStatusUpdateError(\n `abandoning entry due to non-retryable error`,\n {\n quoteId: entry.quoteId,\n errorType: response?.type,\n },\n ),\n );\n }\n}\n"]}