From de5503a815cd0f7c07a618adae9b4773baed77a9 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 16 Jun 2026 13:27:06 +0200 Subject: [PATCH 1/4] fix(transaction-pay-controller): mark MM Pay transactions as externally signed once a quote is available --- packages/transaction-pay-controller/CHANGELOG.md | 4 ++++ .../src/utils/quotes.test.ts | 11 +++++++++++ .../transaction-pay-controller/src/utils/quotes.ts | 9 +++++++++ 3 files changed, 24 insertions(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 9bc8d7c2286..23994cd6687 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 once a quote is available, so the parent transaction is no longer locally signed by `KeyringController:signTransaction`. Unblocks `MoneyKeyring`, which does not implement `signTransaction`. + ## [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..8fa2539eee7 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -730,6 +730,17 @@ describe('Quotes Utils', () => { ); }); + it('marks the transaction as externally signed 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('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..68a25c51cc9 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -233,6 +233,15 @@ function syncTransaction({ tx.batchTransactions = batchTransactions; tx.batchTransactionsOptions = {}; + // Once a quote has been calculated, MM Pay owns submission of the + // transaction via its strategy publish hook. Mark the parent as + // externally signed so the TransactionController skips the local + // `KeyringController:signTransaction` call. This is required for + // accounts whose keyring cannot sign raw transactions (e.g. + // `MoneyKeyring`, which only exposes EIP-7702 authorization and + // EIP-712 / personal message signing). + tx.isExternalSign = true; + tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, chainId: paymentToken.chainId, From 5aa79e74faa2afe802b40f770b54817fbcdb70aa Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 16 Jun 2026 13:28:16 +0200 Subject: [PATCH 2/4] chore(transaction-pay-controller): link PR in changelog --- packages/transaction-pay-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 23994cd6687..1a369dd9aae 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Mark MM Pay transactions as externally signed once a quote is available, so the parent transaction is no longer locally signed by `KeyringController:signTransaction`. Unblocks `MoneyKeyring`, which does not implement `signTransaction`. +- Mark MM Pay transactions as externally signed once a quote is available, so the parent transaction is no longer locally signed by `KeyringController:signTransaction`. Unblocks `MoneyKeyring`, which does not implement `signTransaction`. ([#9145](https://github.com/MetaMask/core/pull/9145)) ## [23.7.0] From b40c262192e2a50a8764f7abcdaf0227475d120a Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 16 Jun 2026 13:30:43 +0200 Subject: [PATCH 3/4] fix(transaction-pay-controller): gate external sign flag on quote availability --- .../transaction-pay-controller/CHANGELOG.md | 2 +- .../src/utils/quotes.test.ts | 15 ++++++++++++++- .../src/utils/quotes.ts | 19 +++++++++++-------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 1a369dd9aae..d6c41a0d3bd 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Mark MM Pay transactions as externally signed once a quote is available, so the parent transaction is no longer locally signed by `KeyringController:signTransaction`. Unblocks `MoneyKeyring`, which does not implement `signTransaction`. ([#9145](https://github.com/MetaMask/core/pull/9145)) +- Mark MM Pay transactions as externally signed when quotes are available, so the parent transaction is no longer locally signed by `KeyringController:signTransaction`. The flag is cleared when no quotes are returned (e.g. the payment token matches the target token) so the transaction falls back to normal local signing. Unblocks `MoneyKeyring`, which does not implement `signTransaction`. ([#9145](https://github.com/MetaMask/core/pull/9145)) ## [23.7.0] diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 8fa2539eee7..69fe2675497 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -730,7 +730,7 @@ describe('Quotes Utils', () => { ); }); - it('marks the transaction as externally signed so the publish hook owns submission', async () => { + it('marks the transaction as externally signed when quotes are available so the publish hook owns submission', async () => { await run(); const transactionMetaMock = {} as TransactionMeta; @@ -741,6 +741,19 @@ describe('Quotes Utils', () => { ); }); + 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 68a25c51cc9..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,14 +237,13 @@ function syncTransaction({ tx.batchTransactions = batchTransactions; tx.batchTransactionsOptions = {}; - // Once a quote has been calculated, MM Pay owns submission of the - // transaction via its strategy publish hook. Mark the parent as - // externally signed so the TransactionController skips the local - // `KeyringController:signTransaction` call. This is required for - // accounts whose keyring cannot sign raw transactions (e.g. - // `MoneyKeyring`, which only exposes EIP-7702 authorization and - // EIP-712 / personal message signing). - tx.isExternalSign = true; + // 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, From 7aec3478417ccd0fe07ee3fbf2edaf9f202dbcc7 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 16 Jun 2026 13:31:50 +0200 Subject: [PATCH 4/4] docs(transaction-pay-controller): simplify changelog entry --- packages/transaction-pay-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index d6c41a0d3bd..57fb0f513aa 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Mark MM Pay transactions as externally signed when quotes are available, so the parent transaction is no longer locally signed by `KeyringController:signTransaction`. The flag is cleared when no quotes are returned (e.g. the payment token matches the target token) so the transaction falls back to normal local signing. Unblocks `MoneyKeyring`, which does not implement `signTransaction`. ([#9145](https://github.com/MetaMask/core/pull/9145)) +- Mark MM Pay transactions as externally signed when quotes are available ([#9145](https://github.com/MetaMask/core/pull/9145)) ## [23.7.0]