feat: implement quote status update logic#8462
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 2505ac0. Configure here.
| // Report finalized success for swap/bridge transactions | ||
| if (txMeta.type && isCrossChainTx(txMeta.type)) { | ||
| this.#quoteStatusUpdateManager.reportFinalised(txMeta.id, true); | ||
| } |
There was a problem hiding this comment.
Premature finalized-success reported for bridge transactions
High Severity
In #onTransactionConfirmed, reportFinalised(txMeta.id, true) is called for all cross-chain tx types (both swaps and bridges) when the source chain transaction confirms. For same-chain swaps this is correct — confirmation means the swap is done. For bridge transactions, however, source chain confirmation only means the first leg succeeded; the destination chain transfer is still pending. The #fetchBridgeTxStatus polling handler later calls reportFinalised again with the actual bridge outcome. This means a bridge that ultimately fails on the destination chain will first send FINALIZED_SUCCESS (premature) and then FINALIZED_FAILURE, providing contradictory lifecycle data to the Bridge API.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2505ac0. Configure here.
There was a problem hiding this comment.
This is expected, we care only about reporting the success/failure of the the source tx.
| this.state.txHistory[txMetaId] ?? | ||
| (actionId ? this.state.txHistory[actionId] : undefined); | ||
| if (historyItem?.quoteId) { | ||
| this.#quoteStatusUpdateManager.reportSubmitted( |
There was a problem hiding this comment.
Just wondering why this is called twice, once here at transactionSubmitted and once at transactionStatusUpdated (submitted status) above? Aren't those 2 the same and would there be duplicated reports?
There was a problem hiding this comment.
@infiniteflower they are indeed similar. There was a change recently introduced on the transaction controller where they added a universal event TransactionController:transactionStatusUpdated. I originally made the implementation using the separate submitted event listener and later they introduced the updated event. I kept both for completeness, note there are not issues with duplicate calls, I already tested that.
There was a problem hiding this comment.
@infiniteflower I gave it another look and we indeed require both. TransactionController:transactionStatusSubmitted is called immediately once we broadcast the tx while the update event is triggered on later time. If we omit the submitted event listener, there is an edge case where the quote status manager won't be able to create a record if user closes the app immediately after submitting a swap (or loose internet connection). We'll never get back the tx status in that case.
There was a problem hiding this comment.
the #onTransactionStatusChange handler in TransactionController (line 3337) publishes transactionStatusUpdated right after submitted, so I think this does duplicate the event handling
There was a problem hiding this comment.
We use the generic transaction statusUpdated event instead of specific ones like transactionSubmitted because the initial hash generated during submission can be replaced before it's confirmed on chain
| - Add required `quoteId: string` to `QuoteResponseSchema`; quote responses without a `quoteId` will now fail validation ([#8462](https://github.com/MetaMask/core/pull/8462)) | ||
|
|
There was a problem hiding this comment.
Please mark this as a breaking change. Client e2e tests will need to be updated
|
|
||
| // Finalize the quote status update as a failure since we can no longer | ||
| // determine the outcome via polling. | ||
| this.#quoteStatusUpdateManager.reportFinalised(bridgeTxMetaId, false); |
There was a problem hiding this comment.
The controller restarts polling for these transactions next time the wallet restarts so it's too soon to report finalization here. This will also result in duplicate finalization status updates
There was a problem hiding this comment.
The #handleOldHistoryItem handler deletes the history item to permanently stop polling so the report should happen there
| isCrossChainTx(txMeta.type) && | ||
| !isApprovalTxMeta | ||
| ) { | ||
| if (historyItem?.quoteId && txMeta.hash) { |
There was a problem hiding this comment.
There are multiple places in the code where we assume quoteId is defined, but it won't be for older txHistory items. Can you make it optional in the BridgeHistoryItem type to ensure this undefined check is always done?
|
|
||
| // Report finalized success for swap/bridge transactions | ||
| if (txMeta.type && isCrossChainTx(txMeta.type)) { | ||
| this.#quoteStatusUpdateManager.reportFinalised(txMeta.id, true); |
There was a problem hiding this comment.
for bridge transactions, do we only care about the src transaction? or should finalized only be reported when both src and dest are confirmed?
| return (await res.json()) as QuoteStatusUpdateResponse; | ||
| }; |
There was a problem hiding this comment.
Please add a validator and infer the type for QuoteStatusUpdateResponse rather than casting the response. Validating ensures that the data matches the shape we expect. In the worst case the app or background worker may crash when we attempt to use unvalidated objects
| addTransactionBatchFn, | ||
| config, | ||
| traceFn, | ||
| onQuoteStatusUpdateError, |
There was a problem hiding this comment.
Is this logic going to be different across clients?
| config, | ||
| traceFn, | ||
| onQuoteStatusUpdateError, | ||
| isQuoteStatusUpdateEnabled, |
There was a problem hiding this comment.
If this is only based on a client feature flag, I'd read the flag directly in the controller/update manager
There was a problem hiding this comment.
That was my initial thought as well, but I wanted to keep the controller as stateless as possible, which is why I prefer to pass callbacks and let clients handle initialization. While directly reading the flag might seem appropriate for now, it introduces tight coupling between our service and an external dependency (the LD flags), which complicates testing.
My main concern, however, is that this won’t be the final requirement for the update manager. Since Ethan is planning to add more logic after v1, and we don’t know the exact changes yet, I wanted to keep the interface generic. That way, when we eventually update the implementation logic, the public API can remain stable, making future documentation simpler and more consistent.
|
|
||
| // Report finalized failure for swap/bridge transactions | ||
| if (txMeta.type && isCrossChainTx(txMeta.type)) { | ||
| this.#quoteStatusUpdateManager.reportFinalised(txMeta.id, false); |
There was a problem hiding this comment.
Have you tested this with a HW wallet rejection? If you submit the trade, but reject the tx on a HW wallet, does the submitted event fire? If it doesn't then this should happen after the rejected check below
There was a problem hiding this comment.
Good catch, will have to do it.
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| ...getClientHeaders({ |
| if ( | ||
| type === QuoteStatusUpdateErrorType.InvalidStatusTransaction || | ||
| type === QuoteStatusUpdateErrorType.QuoteStatusOnChainMismatch | ||
| ) { | ||
| const finalizationStatus = response.currentStatus; | ||
| entry.pendingStatuses = [finalizationStatus]; | ||
| this.#persistToState(); | ||
| this.#inFlight.delete(key); | ||
| await this.#attemptFinalizationWithRetries(key, entry); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Why post the status update again? If this happens, doesn't it mean the backend already has the fimnalizationStatus. So all we need to do is remove this from the deferred statuses
| for (const key of this.#deferredRetryQueue.keys()) { | ||
| setTimeout(() => this.#processSingleEntry(key), delay); | ||
| delay += 125; |
There was a problem hiding this comment.
ensureRetryTimerRunning also causes these entries to be processed. Why do both need to be started around the same time?
|
From @micaelae :
|


Explanation
Context
The Bridge API requires clients to report the lifecycle of a swap quote — when a transaction is submitted and when it finalizes (success or failure). Previously,
BridgeStatusControllerhad no mechanism to send these reports, so the Bridge API had no visibility into quote outcomes.Additionally, batch EVM transactions (Smart Transactions / 7702-delegated flows) do not have a
txHashavailable atsubmitTxtime, meaning status reporting for those transactions was impossible without subscribing to a later event.Solution
This PR introduces
QuoteStatusUpdateManagerinsidebridge-status-controller— a resilient, persisted retry queue that reports quote lifecycle events (SUBMITTED,FINALIZED_SUCCESS,FINALIZED_FAILURE) to the Bridge API.Key behaviours:
deferredStatusUpdatescontroller state so it survives service-worker restarts.SVM_TRADE_DESERIALIZE_FAILEDare dropped immediately rather than retried endlessly.isQuoteStatusUpdateEnabledconstructor option (backed by thebridgeQuoteStatusUpdateEnabledremote feature flag in the extension) gates all reporting.onQuoteStatusUpdateErrorconstructor option lets callers (e.g. the extension) forward eviction errors to Sentry.To support STX / 7702-delegated batch transactions,
BridgeStatusControllernow subscribes toTransactionController:transactionSubmittedand triggersSUBMITTEDstatus reporting when a hash becomes available after the fact.bridge-controller'sQuoteResponseSchemais updated to treatquoteIdas a required field, since it is the stable identifier used as the key for the retry queue.Non-obvious changes
BridgeStatusControllerMessenger.AllowedEventsnow requiresTransactionControllerTransactionSubmittedEvent— consumers must add this event to their messengerAllowedEventslist.submitIntentwas fixed along the way:originalTransactionIdis now passed at the top level to#addTxToHistory, so the history item is correctly linked to the syntheticTransactionControllerentry rather than theorderUid.References
isQuoteStatusUpdateEnabledis gated byremoteFeatureFlags.bridgeQuoteStatusUpdateEnabledChecklist
Note
Medium Risk
Medium risk: introduces new persisted retry queue and new event subscriptions to report quote lifecycle status to the Bridge API, and makes
quoteIdrequired in multiple schemas/types, which can break consumers if any integration omits it.Overview
Adds required
quoteIdplumbing across bridge quoting and history:QuoteResponseSchemanow requiresquoteId, test fixtures/mocks/snapshots are updated, andBridgeHistoryItemnow includes a requiredquoteId(breaking).Introduces resilient quote-status reporting in
BridgeStatusControllervia a newQuoteStatusUpdateManager, persisting adeferredStatusUpdatesretry queue and reportingSUBMITTED/finalized success/failure (feature-flagged + error callback). Status reporting is triggered on submission/confirmation/failure/polling-finalization, including a new subscription toTransactionController:transactionSubmittedto handle batch/STX flows where the tx hash is only available after submission.Fixes batch EVM submission cleanup by deleting the pre-submission (actionId-keyed) history item if batch submission fails, and rekeys batch history to the final
txMeta.idwhile capturingbatchId/approvalTxIdonce available.Reviewed by Cursor Bugbot for commit c2e1b23. Bugbot is set up for automated code reviews on this repo. Configure here.