Skip to content

feat: retry failed quote loads and surface quote errors in TransactionPayController - #9837

Closed
dan437 wants to merge 10 commits into
mainfrom
feat/transaction-pay-retry-failed-quote-loads
Closed

feat: retry failed quote loads and surface quote errors in TransactionPayController#9837
dan437 wants to merge 10 commits into
mainfrom
feat/transaction-pay-retry-failed-quote-loads

Conversation

@dan437

@dan437 dan437 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Explanation

When a quote load in TransactionPayController failed unexpectedly, the error was swallowed (.catch(noop)), nothing was written to state, and the refresh loop skipped transactions with no quotes — so they were never retried. One failed fetch left the transaction permanently without quotes: clients showed no fees, and submission was blocked by the publish guard ("MetaMask Pay: Cannot submit without quote").

This change makes failed quote loads recoverable:

  • refreshQuotes now also retries transactions that should have quotes but have none, on the default refresh interval. One transaction's failure no longer stops the others from refreshing.
  • QuoteRefresher keeps the refresh loop alive while such a transaction exists.
  • Unexpected failures are persisted to quoteError (reason no-quotes) instead of being swallowed — but only when quotes are needed and none are usable. Existing executable quotes stay usable until a refresh replaces them, and direct routes stay silent because they need no quotes. Stale no-op quotes from a previous direct route are dropped so the retry loop picks the transaction up.
  • Every failed or skipped attempt stamps quotesLastUpdated, so retries wait a full interval instead of running every tick, and late writes cannot recreate pay state that cleanup already removed.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • 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

Note

Medium Risk
Touches core quote fetch, refresh, and error state for pay submission; behavior is well-tested but affects when users see errors and when submit is allowed.

Overview
Fixes MetaMask Pay transactions that could stay permanently without quotes after a single failed or empty quote fetch, which blocked fees display and submit.

Quote retry and refresh loop: refreshQuotes now retries transactions that should have quotes but have none (failed load, empty result, fiat payment selected, etc.), respecting the normal refresh interval via quotesLastUpdated. QuoteRefresher keeps polling while isQuoteRetryPending is true, not only when executable quotes already exist. Per-transaction failures are isolated so one bad transaction does not stop others from refreshing.

Failure persistence: Unexpected quote pipeline errors call persistQuoteLoadFailure, which stamps quotesLastUpdated, sets quoteError (no-quotes) only when quotes are needed and none are usable, drops stale no-op quotes so retries can run, and skips writes if pay state was removed. Refresh paths that skip or throw still stamp attempt time so retries do not hammer every tick.

Controller: updateQuotes rejections are logged instead of swallowed with noop, without changing the fire-and-forget call pattern.

Reviewed by Cursor Bugbot for commit b94dcdd. Bugbot is set up for automated code reviews on this repo. Configure here.

@dan437
dan437 marked this pull request as ready for review August 14, 2026 09:16
@dan437
dan437 requested review from a team as code owners August 14, 2026 09:16
@dan437
dan437 temporarily deployed to default-branch August 14, 2026 09:16 — with GitHub Actions Inactive
@matthewwalsh0
matthewwalsh0 self-requested a review August 14, 2026 10:14
subscribeTransactionChanges,
} from './utils/transaction.js';

const log = createModuleLogger(projectLogger, 'controller');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The projectLogger is the log function itself for the main controller file.

transaction.quotes?.some(
(quote) => quote.strategy !== TransactionPayStrategy.None,
),
) || isQuoteRetryPending(transaction),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we need to confirm this is some Barbara wants.

My original assumption was the error cases (provider or simulation errors) wouldn't fix themselves so redundant to have the extra traffic and UX skeletons etc.

chainId: quotes[0].request.sourceChainId,
messenger,
})) ?? DEFAULT_REFRESH_INTERVAL;
const refreshInterval = firstQuote

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, could we assign default then override if quotes and strategy interval, rather than ternary and fallbacks?


const firstQuote = quotes?.[0];

if (!firstQuote && !isQuoteRetryPending(transactionData)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we replace isQuoteRetryPending with just checking if quoteError is set as we always set that?

transactionId,
updateTransactionData,
(data) => {
data.quotesLastUpdated = Date.now();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to update this if we haven't derived new quotes?

What does this enable in the UI?

@dan437

dan437 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Closing this for now. Mobile #34471 fixes the user-facing issue, while this retry logic also retries legitimate empty responses.

@dan437 dan437 closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants