diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 9bc8d7c2286..57fb0f513aa 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/keyring-controller` from `^27.0.0` to `^27.1.0` ([#9129](https://github.com/MetaMask/core/pull/9129)) +### Fixed + +- Mark MM Pay transactions as externally signed when quotes are available ([#9145](https://github.com/MetaMask/core/pull/9145)) + ## [23.7.0] ### Added diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 937b03235e1..69fe2675497 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -730,6 +730,30 @@ describe('Quotes Utils', () => { ); }); + it('marks the transaction as externally signed when quotes are available so the publish hook owns submission', async () => { + await run(); + + const transactionMetaMock = {} as TransactionMeta; + updateTransactionMock.mock.calls[0][1](transactionMetaMock); + + expect(transactionMetaMock).toMatchObject( + expect.objectContaining({ isExternalSign: true }), + ); + }); + + it('clears the externally signed flag when no quotes are returned so the transaction falls back to local signing', async () => { + getQuotesMock.mockResolvedValue([]); + + await run(); + + const transactionMetaMock = {} as TransactionMeta; + updateTransactionMock.mock.calls[0][1](transactionMetaMock); + + expect(transactionMetaMock).toMatchObject( + expect.objectContaining({ isExternalSign: false }), + ); + }); + it('updates metrics in metadata', async () => { await run(); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 319bbbc71ca..098911a2de1 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -163,6 +163,7 @@ export async function updateQuotes( syncTransaction({ batchTransactions, + hasQuotes: quotes.length > 0, isPostQuote, messenger: messenger as never, paymentToken, @@ -198,6 +199,7 @@ export async function updateQuotes( * * @param request - Request object. * @param request.batchTransactions - Batch transactions to sync. + * @param request.hasQuotes - Whether MM Pay produced any quotes for this transaction. * @param request.isPostQuote - Whether this is a post-quote flow. * @param request.messenger - Messenger instance. * @param request.paymentToken - Payment token (source for standard flows, destination for post-quote). @@ -206,6 +208,7 @@ export async function updateQuotes( */ function syncTransaction({ batchTransactions, + hasQuotes, isPostQuote, messenger, paymentToken, @@ -213,6 +216,7 @@ function syncTransaction({ transactionId, }: { batchTransactions: BatchTransaction[]; + hasQuotes: boolean; isPostQuote?: boolean; messenger: TransactionPayControllerMessenger; paymentToken: TransactionPaymentToken | undefined; @@ -233,6 +237,14 @@ function syncTransaction({ tx.batchTransactions = batchTransactions; tx.batchTransactionsOptions = {}; + // When MM Pay has produced quotes, it owns submission of this transaction + // via its strategy publish hook, so the parent must be marked externally + // signed to skip the local `KeyringController:signTransaction` call. + // When there are no quotes (e.g. user selected the target token as the + // payment token in a Predict flow), the transaction falls back to normal + // local signing, so the flag is cleared to allow that. + tx.isExternalSign = hasQuotes; + tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, chainId: paymentToken.chainId,