From d089f24347fa19bc0d511807fd92ce2f2cc41d41 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 14 May 2026 11:02:06 +0100 Subject: [PATCH 1/7] fix: Rename gasless feature flag (#8801) ## What changed - Renamed the Relay execute feature flag in `transaction-pay-controller` from `gaslessEnabled` to `isGaslessEnabled`. - Updated feature flag tests to use the new key for both enabled and disabled cases. - Added an Unreleased changelog entry referencing this PR. ## Why This separates the new gasless toggle from the prior release flag so rollout can use one flag up to a given release and another flag starting with this release. ## Validation - `yarn workspace @metamask/transaction-pay-controller run jest --no-coverage src/utils/feature-flags.test.ts` - `yarn workspace @metamask/transaction-pay-controller run test` - `yarn workspace @metamask/transaction-pay-controller run changelog:validate` - `yarn build` - `yarn lint` Note: `yarn validate:changelog` is not defined at the repo root in this checkout, so I used the package changelog validation script. --- > [!NOTE] > **Low Risk** > Low risk: this is a straightforward rename of a remote feature-flag key used to gate Relay `/execute` gasless behavior, with tests updated accordingly. Main risk is rollout/config mismatch if any clients or flag payloads still send `gaslessEnabled`. > > **Overview** > Renames the Relay gasless execution remote feature flag key from `gaslessEnabled` to `isGaslessEnabled` and updates `isRelayExecuteEnabled` (and its `PayStrategiesConfigRaw` typing) to read the new key. > > Updates unit tests to assert the new flag name and adds an Unreleased changelog entry documenting the rename. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f79da17d07d13c59ffffeee75287af79721da2e7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/transaction-pay-controller/CHANGELOG.md | 4 ++++ .../src/utils/feature-flags.test.ts | 8 ++++---- .../transaction-pay-controller/src/utils/feature-flags.ts | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 5495abc37f..6013645c86 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 - Add Polymarket deposit-wallet support to the Relay strategy for `predictWithdraw` transactions, routed via the `isPolymarketDepositWallet` flag on `TransactionConfig` ([#8754](https://github.com/MetaMask/core/pull/8754)) +### Changed + +- Rename Relay gasless execution feature flag from `gaslessEnabled` to `isGaslessEnabled` ([#8801](https://github.com/MetaMask/core/pull/8801)) + ## [22.4.0] ### Added diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts index d2455f787d..79540a25ab 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -464,14 +464,14 @@ describe('Feature Flags Utils', () => { expect(isRelayExecuteEnabled(messenger)).toBe(false); }); - it('returns true when gaslessEnabled is true', () => { + it('returns true when isGaslessEnabled is true', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { confirmations_pay: { payStrategies: { relay: { - gaslessEnabled: true, + isGaslessEnabled: true, }, }, }, @@ -481,14 +481,14 @@ describe('Feature Flags Utils', () => { expect(isRelayExecuteEnabled(messenger)).toBe(true); }); - it('returns false when gaslessEnabled is false', () => { + it('returns false when isGaslessEnabled is false', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { confirmations_pay: { payStrategies: { relay: { - gaslessEnabled: false, + isGaslessEnabled: false, }, }, }, diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index f3b3144325..4f837f24c1 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -133,7 +133,7 @@ export type PayStrategiesConfigRaw = { across?: AcrossConfigRaw; relay?: { enabled?: boolean; - gaslessEnabled?: boolean; + isGaslessEnabled?: boolean; originGasOverhead?: string; pollingInterval?: number; pollingTimeout?: number; @@ -496,7 +496,7 @@ export function isRelayExecuteEnabled( (state.remoteFeatureFlags?.confirmations_pay as | FeatureFlagsRaw | undefined) ?? {}; - return featureFlags.payStrategies?.relay?.gaslessEnabled ?? false; + return featureFlags.payStrategies?.relay?.isGaslessEnabled ?? false; } /** From 248e7b27b5b7d3afa6730bdc8a0cb0ae0591b937 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 14 May 2026 11:54:05 +0100 Subject: [PATCH 2/7] Add Across Predict withdraw submit support (#8761) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This is PR 3 of 4 in the core stack for Predict withdraws over Across. - Prepends the original Predict withdraw transaction when submitting post-quote Across flows. - Uses `TransactionType.predictAcrossWithdraw` for the Across deposit leg. - Forces 7702 batch submission when the parent withdraw transaction already has an authorization list. - Estimates final 7702 submit gas when the submit-time batch shape differs from the quote-time batch shape. - Moves original transaction gas parsing into a shared Across helper used by quote and submit paths. ## Stack 1. #8759: plumbing to identify Predict Across withdraws 2. #8760: quote support 3. This PR: submit support 4. #8762: gas payment edge cases ## Validation - `yarn workspace @metamask/transaction-pay-controller run jest --no-coverage src/strategy/across/across-submit.test.ts src/strategy/across/across-quotes.test.ts src/strategy/across/AcrossStrategy.test.ts` - Full stack validation was run on the final stacked branch: - `yarn changelog:validate` - `yarn workspace @metamask/transaction-pay-controller run test` - `yarn workspace @metamask/transaction-controller run test` --- > [!NOTE] > **Medium Risk** > Modifies Across submission logic for post-quote flows, including 7702 batch handling and gas estimation, which can impact how transactions are constructed and sent on-chain. > > **Overview** > Adds Across *submit-time* support for post-quote Predict withdraws by optionally prepending the original user transaction to the submitted payload and mapping the bridge leg to `TransactionType.predictAcrossWithdraw`. > > Updates 7702 handling to force batch submission when needed (quoted combined gas limit, parent `authorizationList`, or gas-fee-token post-quote Predict withdraw), and can estimate a final `gasLimit7702` via `TransactionController:estimateGasBatch` with feature-flag gas buffer when the quoted batch shape doesn’t match the actual submit shape. > > Passes `gasFeeToken` plus `excludeNativeTokenForFee` through to single and batch submits, expands tests around these scenarios, and moves original-transaction gas parsing into a shared helper (`getOriginalTransactionGas`) reused by both quote and submit paths. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit df05cc83cc9a8e9ecafaade26d132ccdf5ebc230. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .../transaction-pay-controller/CHANGELOG.md | 1 + .../src/strategy/across/across-quotes.ts | 24 +- .../src/strategy/across/across-submit.test.ts | 537 ++++++++++++++++++ .../src/strategy/across/across-submit.ts | 315 +++++++++- .../src/strategy/across/transactions.ts | 27 + 5 files changed, 856 insertions(+), 48 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 6013645c86..685920b5a2 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add Across submit support for post-quote Predict withdraw flows ([#8761](https://github.com/MetaMask/core/pull/8761)) - Add Polymarket deposit-wallet support to the Relay strategy for `predictWithdraw` transactions, routed via the `isPolymarketDepositWallet` flag on `TransactionConfig` ([#8754](https://github.com/MetaMask/core/pull/8754)) ### Changed diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index d7ff9627bd..8db721b870 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -33,7 +33,10 @@ import { getAcrossDestination } from './across-actions'; import { hasUnsupportedTransactionAuthorizationList } from './authorization-list'; import { normalizeAcrossRequest } from './perps'; import { isAcrossQuoteRequest } from './requests'; -import { getAcrossOrderedTransactions } from './transactions'; +import { + getAcrossOrderedTransactions, + getOriginalTransactionGas, +} from './transactions'; import type { AcrossAction, AcrossActionRequestBody, @@ -796,25 +799,6 @@ function combinePostQuoteGas( }; } -function getOriginalTransactionGas( - transaction: TransactionMeta, -): number | undefined { - const nestedGas = transaction.nestedTransactions?.find((tx) => tx.gas)?.gas; - const rawGas = nestedGas ?? transaction.txParams.gas; - - if (rawGas === undefined) { - return undefined; - } - - const gas = new BigNumber(rawGas); - - if (!gas.isFinite() || gas.isNaN() || !gas.isInteger() || gas.lte(0)) { - return undefined; - } - - return gas.toNumber(); -} - function calculateOriginalSourceNetworkCost({ gas, messenger, diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index 074ee59760..dd453928c1 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -79,6 +79,7 @@ const QUOTE_MOCK: TransactionPayQuote = { }, }, request: { + actions: [], amount: '100', tradeType: 'exactOutput', }, @@ -104,8 +105,10 @@ describe('Across Submit', () => { const { addTransactionBatchMock, addTransactionMock, + estimateGasBatchMock, estimateGasMock, findNetworkClientIdByChainIdMock, + getKeyringControllerStateMock, getRemoteFeatureFlagControllerStateMock, getTransactionControllerStateMock, messenger, @@ -126,6 +129,16 @@ describe('Across Submit', () => { }, }, }); + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [FROM_MOCK], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); estimateGasMock.mockResolvedValue({ gas: '0x5208', @@ -231,6 +244,7 @@ describe('Across Submit', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ + excludeNativeTokenForFee: true, gasFeeToken: QUOTE_MOCK.request.sourceTokenAddress, }), ); @@ -285,6 +299,237 @@ describe('Across Submit', () => { ); }); + it('estimates 7702 batch gas when a post-quote original transaction was not priced in the quote', async () => { + const postQuote = { + ...QUOTE_MOCK, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [{ estimate: 43000, max: 64000 }], + is7702: true, + }, + }, + request: { + ...QUOTE_MOCK.request, + isPostQuote: true, + }, + } as TransactionPayQuote; + + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [FROM_MOCK], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); + estimateGasBatchMock.mockResolvedValue({ + gasLimits: [123456], + }); + + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [postQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.batch, + nestedTransactions: [{ type: TransactionType.predictWithdraw }], + txParams: { + from: FROM_MOCK, + to: '0x000000000000000000000000000000000000dEaD' as Hex, + data: '0x12345678' as Hex, + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(estimateGasBatchMock).toHaveBeenCalledWith({ + chainId: QUOTE_MOCK.request.sourceChainId, + from: FROM_MOCK, + transactions: [ + expect.objectContaining({ + data: '0x12345678', + gas: undefined, + to: '0x000000000000000000000000000000000000dEaD', + }), + expect.objectContaining({ + data: QUOTE_MOCK.original.quote.approvalTxns[0].data, + gas: undefined, + to: QUOTE_MOCK.original.quote.approvalTxns[0].to, + }), + expect.objectContaining({ + data: QUOTE_MOCK.original.quote.swapTx.data, + gas: undefined, + to: QUOTE_MOCK.original.quote.swapTx.to, + }), + ], + }); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: false, + disableHook: true, + disableSequential: true, + gasLimit7702: toHex(123456), + transactions: [ + expect.objectContaining({ + params: expect.objectContaining({ + data: '0x12345678', + gas: undefined, + to: '0x000000000000000000000000000000000000dEaD', + }), + type: TransactionType.predictWithdraw, + }), + expect.objectContaining({ + params: expect.objectContaining({ + data: QUOTE_MOCK.original.quote.approvalTxns[0].data, + gas: undefined, + to: QUOTE_MOCK.original.quote.approvalTxns[0].to, + }), + type: TransactionType.tokenMethodApprove, + }), + expect.objectContaining({ + params: expect.objectContaining({ + data: QUOTE_MOCK.original.quote.swapTx.data, + gas: undefined, + to: QUOTE_MOCK.original.quote.swapTx.to, + }), + type: TransactionType.predictAcrossWithdraw, + }), + ], + }), + ); + }); + + it('reuses quoted 7702 batch gas when the post-quote original transaction already has gas', async () => { + const postQuote = { + ...QUOTE_MOCK, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [{ estimate: 43000, max: 64000 }], + is7702: true, + }, + quote: { + ...QUOTE_MOCK.original.quote, + approvalTxns: [], + }, + }, + request: { + ...QUOTE_MOCK.request, + isPostQuote: true, + }, + } as TransactionPayQuote; + + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [postQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.swap, + txParams: { + from: FROM_MOCK, + to: '0x000000000000000000000000000000000000dEaD' as Hex, + data: '0x12345678' as Hex, + gas: '0x5208', + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(estimateGasBatchMock).not.toHaveBeenCalled(); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: false, + gasLimit7702: toHex(64000), + transactions: [ + expect.objectContaining({ + params: expect.objectContaining({ + data: '0x12345678', + gas: undefined, + to: '0x000000000000000000000000000000000000dEaD', + }), + type: TransactionType.swap, + }), + expect.objectContaining({ + params: expect.objectContaining({ + data: QUOTE_MOCK.original.quote.swapTx.data, + gas: undefined, + to: QUOTE_MOCK.original.quote.swapTx.to, + }), + type: TransactionType.swap, + }), + ], + }), + ); + }); + + it('submits 7702 batches without estimated gas when the account cannot sign authorizations', async () => { + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'Ledger Hardware', + accounts: [FROM_MOCK], + metadata: { id: 'ledger-keyring', name: 'Ledger Hardware' }, + }, + ], + }); + + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [QUOTE_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + ...TRANSACTION_META_MOCK.txParams, + authorizationList: [{ address: '0xabc' as Hex }], + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(estimateGasBatchMock).not.toHaveBeenCalled(); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: false, + gasLimit7702: undefined, + }), + ); + }); + + it('submits 7702 batches without estimated gas when estimation returns multiple gas limits', async () => { + estimateGasBatchMock.mockResolvedValue({ + gasLimits: [123456, 234567], + }); + + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [QUOTE_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + ...TRANSACTION_META_MOCK.txParams, + authorizationList: [{ address: '0xabc' as Hex }], + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(estimateGasBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: false, + gasLimit7702: undefined, + }), + ); + }); + it('submits a single transaction when no approvals', async () => { const noApprovalQuote = { ...QUOTE_MOCK, @@ -340,6 +585,7 @@ describe('Across Submit', () => { expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ + excludeNativeTokenForFee: true, gasFeeToken: QUOTE_MOCK.request.sourceTokenAddress, }), ); @@ -401,6 +647,297 @@ describe('Across Submit', () => { ); }); + it('prepends the original transaction and uses predict withdraw type for post-quote predict withdraws', async () => { + const postQuote = { + ...QUOTE_MOCK, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [ + { estimate: 50000, max: 50000 }, + { estimate: 22000, max: 22000 }, + ], + is7702: false, + }, + quote: { + ...QUOTE_MOCK.original.quote, + approvalTxns: [], + }, + }, + request: { + ...QUOTE_MOCK.request, + isPostQuote: true, + }, + } as TransactionPayQuote; + + await submitAcrossQuotes({ + messenger, + quotes: [postQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.batch, + nestedTransactions: [{ type: TransactionType.predictWithdraw }], + txParams: { + from: FROM_MOCK, + to: '0x000000000000000000000000000000000000dEaD' as Hex, + data: '0x12345678' as Hex, + value: '0x1' as Hex, + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + from: FROM_MOCK, + transactions: [ + { + params: expect.objectContaining({ + data: '0x12345678', + gas: toHex(50000), + to: '0x000000000000000000000000000000000000dEaD', + value: '0x1', + }), + type: TransactionType.predictWithdraw, + }, + { + params: expect.objectContaining({ + data: QUOTE_MOCK.original.quote.swapTx.data, + gas: toHex(22000), + to: QUOTE_MOCK.original.quote.swapTx.to, + }), + type: TransactionType.predictAcrossWithdraw, + }, + ], + }), + ); + }); + + it('keeps Across gas limits aligned when post-quote original gas is absent', async () => { + const postQuote = { + ...QUOTE_MOCK, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [{ estimate: 22000, max: 22000 }], + is7702: false, + }, + quote: { + ...QUOTE_MOCK.original.quote, + approvalTxns: [], + }, + }, + request: { + ...QUOTE_MOCK.request, + isPostQuote: true, + }, + } as TransactionPayQuote; + + await submitAcrossQuotes({ + messenger, + quotes: [postQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.batch, + nestedTransactions: [{ type: TransactionType.predictWithdraw }], + txParams: { + from: FROM_MOCK, + to: '0x000000000000000000000000000000000000dEaD' as Hex, + data: '0x12345678' as Hex, + value: '0x1' as Hex, + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + transactions: [ + { + params: expect.objectContaining({ + data: '0x12345678', + gas: undefined, + to: '0x000000000000000000000000000000000000dEaD', + value: '0x1', + }), + type: TransactionType.predictWithdraw, + }, + { + params: expect.objectContaining({ + data: QUOTE_MOCK.original.quote.swapTx.data, + gas: toHex(22000), + to: QUOTE_MOCK.original.quote.swapTx.to, + }), + type: TransactionType.predictAcrossWithdraw, + }, + ], + }), + ); + }); + + it('passes gas fee token for post-quote predict withdraw batches', async () => { + const postQuote = { + ...QUOTE_MOCK, + fees: { + ...QUOTE_MOCK.fees, + isSourceGasFeeToken: true, + }, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [ + { estimate: 50000, max: 50000 }, + { estimate: 22000, max: 22000 }, + ], + is7702: false, + }, + quote: { + ...QUOTE_MOCK.original.quote, + approvalTxns: [], + }, + }, + request: { + ...QUOTE_MOCK.request, + isPostQuote: true, + }, + } as TransactionPayQuote; + + await submitAcrossQuotes({ + messenger, + quotes: [postQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.predictWithdraw, + txParams: { + from: FROM_MOCK, + to: '0x000000000000000000000000000000000000dEaD' as Hex, + data: '0x12345678' as Hex, + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + excludeNativeTokenForFee: true, + gasFeeToken: QUOTE_MOCK.request.sourceTokenAddress, + }), + ); + }); + + it('submits post-quote predict withdraw parent authorization lists as 7702 batches', async () => { + const postQuote = { + ...QUOTE_MOCK, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [{ estimate: 22000, max: 22000 }], + is7702: false, + }, + quote: { + ...QUOTE_MOCK.original.quote, + approvalTxns: [], + }, + }, + request: { + ...QUOTE_MOCK.request, + isPostQuote: true, + }, + } as TransactionPayQuote; + + await submitAcrossQuotes({ + messenger, + quotes: [postQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.batch, + nestedTransactions: [{ type: TransactionType.predictWithdraw }], + txParams: { + authorizationList: [{ address: '0xabc' as Hex }], + from: FROM_MOCK, + to: '0x000000000000000000000000000000000000dEaD' as Hex, + data: '0x12345678' as Hex, + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: false, + disableHook: true, + disableSequential: true, + transactions: [ + { + params: expect.objectContaining({ + data: '0x12345678', + gas: undefined, + to: '0x000000000000000000000000000000000000dEaD', + }), + type: TransactionType.predictWithdraw, + }, + { + params: expect.objectContaining({ + data: QUOTE_MOCK.original.quote.swapTx.data, + gas: undefined, + to: QUOTE_MOCK.original.quote.swapTx.to, + }), + type: TransactionType.predictAcrossWithdraw, + }, + ], + }), + ); + }); + + it('uses the original transaction type for non-predict post-quote batches', async () => { + const postQuote = { + ...QUOTE_MOCK, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [undefined as never, { estimate: 22000, max: 22000 }], + is7702: false, + }, + quote: { + ...QUOTE_MOCK.original.quote, + approvalTxns: [], + }, + }, + request: { + ...QUOTE_MOCK.request, + isPostQuote: true, + }, + } as TransactionPayQuote; + + await submitAcrossQuotes({ + messenger, + quotes: [postQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.swap, + txParams: { + from: FROM_MOCK, + to: '0x000000000000000000000000000000000000dEaD' as Hex, + data: '0x12345678' as Hex, + }, + } as TransactionMeta, + isSmartTransaction: jest.fn(), + }); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + transactions: expect.arrayContaining([ + expect.objectContaining({ + params: expect.objectContaining({ + gas: undefined, + }), + type: TransactionType.swap, + }), + ]), + }), + ); + }); + it('preserves transaction type when not perps or predict', async () => { const noApprovalQuote = { ...QUOTE_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts index cb09bac1cb..3c52f2395d 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -18,14 +18,20 @@ import type { TransactionPayControllerMessenger, TransactionPayQuote, } from '../../types'; +import { accountSupports7702 } from '../../utils/7702'; import { getPayStrategiesConfig } from '../../utils/feature-flags'; +import { getGasBuffer } from '../../utils/feature-flags'; import { collectTransactionIds, getTransaction, updateTransaction, + isPredictWithdrawTransaction, waitForTransactionConfirmed, } from '../../utils/transaction'; -import { getAcrossOrderedTransactions } from './transactions'; +import { + getAcrossOrderedTransactions, + getOriginalTransactionGas, +} from './transactions'; import type { AcrossQuote } from './types'; const log = createModuleLogger(projectLogger, 'across-strategy'); @@ -33,7 +39,7 @@ const ACROSS_STATUS_POLL_INTERVAL = 1000; type PreparedAcrossTransaction = { params: TransactionParams; - type: TransactionType; + type: TransactionMeta['type']; }; /** @@ -79,10 +85,10 @@ async function executeSingleQuote( }, ); - const acrossDepositType = getAcrossDepositType(transaction.type); + const acrossDepositType = getAcrossDepositType(transaction); const transactionHash = await submitTransactions( quote, - transaction.id, + transaction, acrossDepositType, messenger, ); @@ -105,14 +111,14 @@ async function executeSingleQuote( * Submit transactions for an Across quote. * * @param quote - Across quote. - * @param parentTransactionId - ID of the parent transaction. + * @param parentTransaction - Parent transaction. * @param acrossDepositType - Transaction type used for the swap/deposit step. * @param messenger - Controller messenger. * @returns Hash of the last submitted transaction, if available. */ async function submitTransactions( quote: TransactionPayQuote, - parentTransactionId: string, + parentTransaction: TransactionMeta, acrossDepositType: TransactionType, messenger: TransactionPayControllerMessenger, ): Promise { @@ -124,32 +130,81 @@ async function submitTransactions( quote: quote.original.quote, swapType: acrossDepositType, }); + const shouldPrependOriginalTransaction = + quote.request.isPostQuote === true && + parentTransaction.txParams.to !== undefined; + const hasPrependedOriginalGasLimit = + shouldPrependOriginalTransaction && + !is7702 && + quoteGasLimits.length > orderedTransactions.length; + const gasLimitOffset = hasPrependedOriginalGasLimit ? 1 : 0; + const transactionCount = + orderedTransactions.length + (shouldPrependOriginalTransaction ? 1 : 0); const networkClientId = messenger.call( 'NetworkController:findNetworkClientIdByChainId', chainId, ); - const batchGasLimit = - is7702 && orderedTransactions.length > 1 - ? quoteGasLimits[0]?.max - : undefined; + const is7702Batch = is7702 && transactionCount > 1; + const canUseQuotedBatchGasLimit = + is7702Batch && + (!shouldPrependOriginalTransaction || + hasOriginalTransactionGas(parentTransaction)); + const batchGasLimit = canUseQuotedBatchGasLimit + ? quoteGasLimits[0]?.max + : undefined; - if (is7702 && orderedTransactions.length > 1 && batchGasLimit === undefined) { + if (canUseQuotedBatchGasLimit && batchGasLimit === undefined) { throw new Error('Missing quote gas limit for Across 7702 batch'); } - const gasLimit7702 = + const quotedGasLimit7702 = batchGasLimit === undefined ? undefined : toHex(batchGasLimit); + const parentHasAuthorizationList = Boolean( + parentTransaction.txParams.authorizationList?.length, + ); + + const shouldUseGasFeeToken7702Submit = shouldEstimate7702SubmitBatch( + parentTransaction, + quote, + ) + ? accountSupports7702(messenger, from) + : false; + const shouldUse7702Submit = [ + Boolean(quotedGasLimit7702), + is7702Batch, + parentHasAuthorizationList, + shouldUseGasFeeToken7702Submit, + ].some(Boolean); + + const shouldEstimateGasLimit7702 = !quotedGasLimit7702 && shouldUse7702Submit; + + const estimatedGasLimit7702 = shouldEstimateGasLimit7702 + ? await estimateSubmitBatchGasLimit7702({ + chainId, + from, + messenger, + orderedTransactions, + parentTransaction, + shouldPrependOriginalTransaction, + }) + : undefined; + + const gasLimit7702 = quotedGasLimit7702 ?? estimatedGasLimit7702; + const submitAs7702 = shouldUse7702Submit || Boolean(gasLimit7702); - const transactions: PreparedAcrossTransaction[] = orderedTransactions.map( - (transaction, index) => { - const gasLimit = gasLimit7702 ? undefined : quoteGasLimits[index]?.max; + const acrossTransactions: PreparedAcrossTransaction[] = + orderedTransactions.map((transaction, index) => { + const gasLimit = submitAs7702 + ? undefined + : quoteGasLimits[index + gasLimitOffset]?.max; - if (gasLimit === undefined && !gasLimit7702) { + if (gasLimit === undefined && !submitAs7702) { + const quoteGasIndex = index + gasLimitOffset; const errorMessage = transaction.kind === 'approval' - ? `Missing quote gas limit for Across approval transaction at index ${index}` + ? `Missing quote gas limit for Across approval transaction at index ${quoteGasIndex}` : 'Missing quote gas limit for Across swap transaction'; throw new Error(errorMessage); @@ -167,8 +222,18 @@ async function submitTransactions( }), type: transaction.type ?? acrossDepositType, }; - }, - ); + }); + const originalTransaction = shouldPrependOriginalTransaction + ? [ + buildOriginalTransaction( + parentTransaction, + submitAs7702 || !hasPrependedOriginalGasLimit + ? undefined + : quoteGasLimits[0]?.max, + ), + ] + : []; + const transactions = [...originalTransaction, ...acrossTransactions]; const transactionIds: string[] = []; @@ -181,7 +246,7 @@ async function submitTransactions( updateTransaction( { - transactionId: parentTransactionId, + transactionId: parentTransaction.id, messenger, note: 'Add required transaction ID from Across submission', }, @@ -197,6 +262,7 @@ async function submitTransactions( const gasFeeToken = quote.fees.isSourceGasFeeToken ? quote.request.sourceTokenAddress : undefined; + const excludeNativeTokenForFee = gasFeeToken ? true : undefined; try { if (transactions.length === 1) { @@ -204,6 +270,7 @@ async function submitTransactions( 'TransactionController:addTransaction', transactions[0].params, { + excludeNativeTokenForFee, gasFeeToken, networkClientId, origin: ORIGIN_METAMASK, @@ -218,9 +285,10 @@ async function submitTransactions( })); await messenger.call('TransactionController:addTransactionBatch', { - disable7702: !gasLimit7702, - disableHook: Boolean(gasLimit7702), - disableSequential: Boolean(gasLimit7702), + disable7702: !submitAs7702, + disableHook: submitAs7702, + disableSequential: submitAs7702, + excludeNativeTokenForFee, from, gasFeeToken, gasLimit7702, @@ -260,6 +328,13 @@ type AcrossStatusResponse = { txHash?: Hex; }; +/** + * Poll Across until a submitted deposit reaches a terminal status. + * + * @param transactionHash - Source-chain deposit transaction hash. + * @param messenger - Controller messenger. + * @returns Destination/fill transaction hash when available, otherwise the source hash. + */ async function waitForAcrossCompletion( transactionHash: Hex | undefined, messenger: TransactionPayControllerMessenger, @@ -335,10 +410,168 @@ async function waitForAcrossCompletion( } } -function getAcrossDepositType( - transactionType?: TransactionType, -): TransactionType { - switch (transactionType) { +/** + * Check whether submit should estimate a 7702 batch gas limit. + * + * This is needed for Predict withdraw post-quote flows that pay source-chain + * gas with the source token, because the final submit batch can differ from the + * batch shape that Across quoted. + * + * @param parentTransaction - Original transaction metadata. + * @param quote - Across quote selected for execution. + * @returns Whether submit should try to estimate the final 7702 batch gas. + */ +function shouldEstimate7702SubmitBatch( + parentTransaction: TransactionMeta, + quote: TransactionPayQuote, +): boolean { + return ( + isPredictWithdrawTransaction(parentTransaction) && + quote.request.isPostQuote === true && + quote.fees.isSourceGasFeeToken === true + ); +} + +/** + * Estimate the 7702 batch gas limit for the actual submit payload. + * + * Quotes can contain a combined 7702 gas limit that only covered the Across + * approval/swap legs. When submit prepends the original transaction, estimate + * the final batch shape so the gas limit covers every submitted leg. + * + * @param args - Estimation arguments. + * @param args.chainId - Source chain ID. + * @param args.from - Sender address. + * @param args.messenger - Controller messenger. + * @param args.orderedTransactions - Across approval/swap legs in submission order. + * @param args.parentTransaction - Original transaction that may be prepended. + * @param args.shouldPrependOriginalTransaction - Whether to include the original transaction in the estimate. + * @returns Hex gas limit, or `undefined` when estimation is unavailable. + */ +async function estimateSubmitBatchGasLimit7702({ + chainId, + from, + messenger, + orderedTransactions, + parentTransaction, + shouldPrependOriginalTransaction, +}: { + chainId: Hex; + from: Hex; + messenger: TransactionPayControllerMessenger; + orderedTransactions: ReturnType; + parentTransaction: TransactionMeta; + shouldPrependOriginalTransaction: boolean; +}): Promise { + if (!accountSupports7702(messenger, from)) { + return undefined; + } + + const originalTransaction = shouldPrependOriginalTransaction + ? [buildOriginalTransaction(parentTransaction)] + : []; + + const acrossTransactions = orderedTransactions.map((transaction) => ({ + params: buildTransactionParams(from, { + chainId: transaction.chainId, + data: transaction.data, + maxFeePerGas: transaction.maxFeePerGas, + maxPriorityFeePerGas: transaction.maxPriorityFeePerGas, + to: transaction.to, + value: transaction.value, + }), + type: transaction.type, + })); + + const transactions = [...originalTransaction, ...acrossTransactions]; + + try { + const result = await messenger.call( + 'TransactionController:estimateGasBatch', + { + chainId, + from, + transactions: transactions.map(({ params }) => + toBatchTransactionParams(params), + ), + }, + ); + + if (result.gasLimits.length !== 1) { + return undefined; + } + + const gasLimit = Math.ceil( + result.gasLimits[0] * getGasBuffer(messenger, chainId), + ); + + return toHex(gasLimit); + } catch { + return undefined; + } +} + +/** + * Build the original parent transaction as a prepared batch leg. + * + * @param transaction - Original transaction metadata. + * @param gasLimit - Optional gas limit to pin on the original leg. + * @returns Prepared transaction params and transaction type for the original leg. + */ +function buildOriginalTransaction( + transaction: TransactionMeta, + gasLimit?: number, +): PreparedAcrossTransaction { + return { + params: { + data: transaction.txParams.data, + from: transaction.txParams.from, + gas: gasLimit === undefined ? undefined : toHex(gasLimit), + to: transaction.txParams.to, + value: transaction.txParams.value, + } as TransactionParams, + type: getOriginalTransactionType(transaction), + }; +} + +/** + * Get the transaction type to use for the original batch leg. + * + * @param transaction - Original transaction metadata. + * @returns `predictWithdraw` for Predict withdrawals; otherwise the original type. + */ +function getOriginalTransactionType( + transaction: TransactionMeta, +): TransactionMeta['type'] { + if (isPredictWithdrawTransaction(transaction)) { + return TransactionType.predictWithdraw; + } + + return transaction.type; +} + +/** + * Check whether the original transaction already has a usable gas limit. + * + * @param transaction - Original transaction metadata. + * @returns Whether the original or nested transaction gas is a positive integer. + */ +function hasOriginalTransactionGas(transaction: TransactionMeta): boolean { + return getOriginalTransactionGas(transaction) !== undefined; +} + +/** + * Get the transaction type for the Across bridge/deposit leg. + * + * @param transaction - Original parent transaction. + * @returns Across-specific transaction type for known flows, or the original type. + */ +function getAcrossDepositType(transaction: TransactionMeta): TransactionType { + if (isPredictWithdrawTransaction(transaction)) { + return TransactionType.predictAcrossWithdraw; + } + + switch (transaction.type) { case TransactionType.perpsDeposit: return TransactionType.perpsAcrossDeposit; case TransactionType.predictDeposit: @@ -346,10 +579,24 @@ function getAcrossDepositType( case undefined: return TransactionType.perpsAcrossDeposit; default: - return transactionType; + return transaction.type as TransactionType; } } +/** + * Build TransactionController params for an Across approval or swap leg. + * + * @param from - Sender address. + * @param params - Across transaction fields. + * @param params.chainId - Source chain ID. + * @param params.data - Transaction calldata. + * @param params.gasLimit - Optional gas limit. + * @param params.to - Recipient contract address. + * @param params.value - Optional native value. + * @param params.maxFeePerGas - Optional EIP-1559 max fee. + * @param params.maxPriorityFeePerGas - Optional EIP-1559 priority fee. + * @returns TransactionController params. + */ function buildTransactionParams( from: Hex, params: { @@ -375,6 +622,12 @@ function buildTransactionParams( }; } +/** + * Normalize an optional numeric string or hex string into a hex value. + * + * @param value - Optional value to normalize. + * @returns Hex value, or `undefined` when no value is provided. + */ function normalizeOptionalHex(value?: string): Hex | undefined { if (value === undefined) { return undefined; @@ -383,6 +636,12 @@ function normalizeOptionalHex(value?: string): Hex | undefined { return toHex(value); } +/** + * Convert full TransactionController params into batch transaction params. + * + * @param params - Transaction params. + * @returns Batch-compatible transaction params. + */ function toBatchTransactionParams( params: TransactionParams, ): BatchTransactionParams { diff --git a/packages/transaction-pay-controller/src/strategy/across/transactions.ts b/packages/transaction-pay-controller/src/strategy/across/transactions.ts index f5cb6ef302..38d04fff10 100644 --- a/packages/transaction-pay-controller/src/strategy/across/transactions.ts +++ b/packages/transaction-pay-controller/src/strategy/across/transactions.ts @@ -1,4 +1,6 @@ import { TransactionType } from '@metamask/transaction-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller'; +import { BigNumber } from 'bignumber.js'; import type { AcrossSwapApprovalResponse } from './types'; @@ -38,3 +40,28 @@ export function getAcrossOrderedTransactions({ }, ]; } + +/** + * Get a usable gas limit from the original or nested transaction. + * + * @param transaction - Original transaction metadata. + * @returns Positive integer gas limit if present, otherwise undefined. + */ +export function getOriginalTransactionGas( + transaction: TransactionMeta, +): number | undefined { + const nestedGas = transaction.nestedTransactions?.find((tx) => tx.gas)?.gas; + const rawGas = nestedGas ?? transaction.txParams.gas; + + if (rawGas === undefined) { + return undefined; + } + + const gas = new BigNumber(rawGas); + + if (!gas.isFinite() || gas.isNaN() || !gas.isInteger() || gas.lte(0)) { + return undefined; + } + + return gas.toNumber(); +} From d0b7977f7eed58bc1afc0292be850b5acc726539 Mon Sep 17 00:00:00 2001 From: samiracle <12882259+samir-acle@users.noreply.github.com> Date: Thu, 14 May 2026 09:09:21 -0400 Subject: [PATCH 3/7] feat: notifications push enable updates (#8782) ## Explanation This PR updates notification setup so clients can separate enabling MetaMask notifications from registering for push notifications. Previously, `createOnChainTriggers` and `enableMetamaskNotifications` always attempted FCM/device push registration as part of enabling notifications. Mobile clients need to be able to enable notification preferences without immediately triggering push registration, since OS push permission and FCM registration may happen through a separate native flow. This PR adds a `registerPushNotifications` option to the notification enablement options. It defaults to `true` to preserve existing behavior, but clients can pass `false` to skip push registration while still enabling MetaMask notifications and initializing notification preferences. This PR also adds optional `os` and `appVersion` metadata to push token registration requests. These values are forwarded to the backend when registering push tokens so Firebase errors can be attributed more precisely by mobile OS and app version. Other updates: - Added and updated tests for the new push registration opt-out behavior. - Updated JSDoc and generated messenger action types. - Updated the package changelog. ## References https://consensyssoftware.atlassian.net/browse/GE-217 https://consensyssoftware.atlassian.net/browse/GE-211 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- .../CHANGELOG.md | 5 + ...nServicesController-method-action-types.ts | 3 +- .../NotificationServicesController.test.ts | 61 +++++++++++ .../NotificationServicesController.ts | 35 ++++-- ...NotificationServicesPushController.test.ts | 58 ++++++++++ .../NotificationServicesPushController.ts | 42 ++++++-- .../__fixtures__/mockServices.ts | 9 +- .../services/services.test.ts | 100 ++++++++++++++++-- .../services/services.ts | 11 +- 9 files changed, 299 insertions(+), 25 deletions(-) diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 147522beea..88ae266c9c 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `registerPushNotifications` to `NotificationServicesControllerEnableNotificationsOptions` so clients can enable MetaMask notifications without registering push notifications. ([#8782](https://github.com/MetaMask/core/pull/8782)) +- Add optional mobile OS and app version metadata to push token registrations so clients can provide Firebase error attribution data. ([#8782](https://github.com/MetaMask/core/pull/8782)) + ## [24.0.0] ### Added diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController-method-action-types.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController-method-action-types.ts index df3a4ff636..92eeee08c5 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController-method-action-types.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController-method-action-types.ts @@ -60,6 +60,7 @@ export type NotificationServicesControllerSetFeatureAnnouncementsEnabledAction = * Used only during initialization to seed marketing push notifications. * @param opts.productAnnouncementEnabled - The user's product-announcement flag. * Used only during initialization to seed marketing in-app notifications. + * @param opts.registerPushNotifications - Whether to attempt FCM/device push registration. * @returns The updated or newly created user storage. * @throws {Error} Throws an error if unauthenticated or from other operations. */ @@ -72,7 +73,7 @@ export type NotificationServicesControllerCreateOnChainTriggersAction = { * Enables all MetaMask notifications for the user. * This is identical flow when initializing notifications for the first time. * - * @param opts - Optional settings for first-time AUS notification preferences initialization. + * @param opts - Optional options to mutate this functionality. * @throws {Error} If there is an error during the process of enabling notifications. */ export type NotificationServicesControllerEnableMetamaskNotificationsAction = { diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts index 1ec067c7c1..f65a607fbd 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts @@ -579,6 +579,45 @@ describe('NotificationServicesController', () => { ]); }); + it('skips push registration when registerPushNotifications is false', async () => { + const { + messenger, + mockEnablePushNotifications, + mockGetConfig, + mockUpdateNotifications, + mockKeyringControllerGetState, + } = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); + + mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + accounts: [ADDRESS_1], + type: KeyringTypes.hd, + metadata: { id: 'srp-1', name: 'SRP 1' }, + }, + ], + }); + const mockTriggerQuery = mockGetOnChainNotificationsConfig(); + + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); + + await controller.createOnChainTriggers({ + registerPushNotifications: false, + }); + + expect(mockGetConfig).toHaveBeenCalled(); + expect(mockTriggerQuery.isDone()).toBe(true); + expect(mockUpdateNotifications).toHaveBeenCalled(); + expect(controller.state.isNotificationServicesEnabled).toBe(true); + expect(mockEnablePushNotifications).not.toHaveBeenCalled(); + }); + it('enables all wallet-activity accounts when Trigger API has no enabled accounts for first-time setup', async () => { const { messenger, @@ -1447,6 +1486,28 @@ describe('NotificationServicesController', () => { expect(mocks.mockUpdateNotifications).toHaveBeenCalled(); }); + it('forwards registerPushNotifications false when enabling MetaMask notifications', async () => { + const mocks = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); + const mockTriggerQuery = mockGetOnChainNotificationsConfig(); + + const controller = new NotificationServicesController({ + messenger: mocks.messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); + + await controller.enableMetamaskNotifications({ + registerPushNotifications: false, + }); + + expect(mocks.mockGetConfig).toHaveBeenCalled(); + expect(mockTriggerQuery.isDone()).toBe(true); + expect(mocks.mockUpdateNotifications).toHaveBeenCalled(); + expect(controller.state.isNotificationServicesEnabled).toBe(true); + expect(mocks.mockEnablePushNotifications).not.toHaveBeenCalled(); + }); + it('should not create new notification subscriptions when enabling an account that already has notifications', async () => { const mocks = arrangeMocks({ // Mock fully-initialized existing notifications diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts index 5ddc45c43e..1d479ef99d 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts @@ -204,8 +204,22 @@ export type NotificationServicesControllerEnableNotificationsOptions = { * in-app notifications. */ productAnnouncementEnabled?: boolean; + /** + * Whether to attempt FCM/device push registration after notification + * preferences are initialized or refreshed. This does not request OS push + * permission. + * + * @default true + */ + registerPushNotifications?: boolean; }; +export type NotificationServicesControllerCreateOnChainTriggersOptions = + NotificationServicesControllerEnableNotificationsOptions; + +export type NotificationServicesControllerEnableMetamaskNotificationsOptions = + NotificationServicesControllerEnableNotificationsOptions; + const locallyPersistedNotificationTypes = new Set([ TRIGGER_TYPES.SNAP, ]); @@ -1016,11 +1030,12 @@ export class NotificationServicesController extends BaseController< * Used only during initialization to seed marketing push notifications. * @param opts.productAnnouncementEnabled - The user's product-announcement flag. * Used only during initialization to seed marketing in-app notifications. + * @param opts.registerPushNotifications - Whether to attempt FCM/device push registration. * @returns The updated or newly created user storage. * @throws {Error} Throws an error if unauthenticated or from other operations. */ public async createOnChainTriggers( - opts?: NotificationServicesControllerEnableNotificationsOptions, + opts: NotificationServicesControllerCreateOnChainTriggersOptions = {}, ): Promise { try { this.#setIsUpdatingMetamaskNotifications(true); @@ -1070,12 +1085,14 @@ export class NotificationServicesController extends BaseController< .filter((account) => account.enabled) .map((account) => account.address); - // 2. Lazily enable push notifications (FCM may take some time, so keeps UI unblocked) - this.#pushNotifications - .enablePushNotifications(accountsWithNotifications) - .catch(() => { - // Do Nothing - }); + if (opts.registerPushNotifications ?? true) { + // Attempt FCM/device registration only; clients must request OS permission separately. + this.#pushNotifications + .enablePushNotifications(accountsWithNotifications) + .catch(() => { + // Do Nothing + }); + } // Update the state of the controller this.update((state) => { @@ -1102,11 +1119,11 @@ export class NotificationServicesController extends BaseController< * Enables all MetaMask notifications for the user. * This is identical flow when initializing notifications for the first time. * - * @param opts - Optional settings for first-time AUS notification preferences initialization. + * @param opts - Optional options to mutate this functionality. * @throws {Error} If there is an error during the process of enabling notifications. */ public async enableMetamaskNotifications( - opts?: NotificationServicesControllerEnableNotificationsOptions, + opts: NotificationServicesControllerEnableMetamaskNotificationsOptions = {}, ): Promise { try { this.#setIsUpdatingMetamaskNotifications(true); diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.test.ts b/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.test.ts index 082c3d69d3..bbe4a74827 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.test.ts @@ -124,6 +124,33 @@ describe('NotificationServicesPushController', () => { }); }); + it('should call activatePushNotifications with mobile OS and app version metadata', async () => { + const mocks = arrangeServicesMocks(); + const { controller, messenger } = arrangeMockMessenger({ + platform: 'mobile', + os: 'android', + appVersion: '7.42.0', + }); + mockAuthBearerTokenCall(messenger); + + await controller.enablePushNotifications(MOCK_ADDRESSES); + + expect(mocks.activatePushNotificationsMock).toHaveBeenCalledWith({ + bearerToken: MOCK_JWT, + addresses: MOCK_ADDRESSES, + env: expect.any(Object), + createRegToken: expect.any(Function), + regToken: { + platform: 'mobile', + locale: 'en', + oldToken: '', + os: 'android', + appVersion: '7.42.0', + }, + controllerEnv: 'prd', + }); + }); + it('should not activate push notifications triggers if there is no auth bearer token', async () => { const mocks = arrangeServicesMocks(); const { controller, messenger } = arrangeMockMessenger(); @@ -384,6 +411,37 @@ describe('NotificationServicesPushController', () => { expect(result).toBe(true); }); + it('should call updateLinksAPI with mobile OS and app version metadata', async () => { + const mocks = arrangeServicesMocks(); + const { controller, messenger } = arrangeMockMessenger({ + platform: 'mobile', + os: 'ios', + appVersion: '7.42.0', + state: { + fcmToken: MOCK_FCM_TOKEN, + isPushEnabled: true, + isUpdatingFCMToken: false, + }, + }); + mockAuthBearerTokenCall(messenger); + + const result = await controller.addPushNotificationLinks(MOCK_ADDRESSES); + + expect(mocks.updateLinksAPIMock).toHaveBeenCalledWith({ + bearerToken: MOCK_JWT, + addresses: MOCK_ADDRESSES, + regToken: { + token: MOCK_FCM_TOKEN, + platform: 'mobile', + locale: 'en', + os: 'ios', + appVersion: '7.42.0', + }, + env: 'prd', + }); + expect(result).toBe(true); + }); + it('should return false when push feature is disabled', async () => { const mocks = arrangeServicesMocks(); const { controller } = arrangeMockMessenger({ diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.ts b/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.ts index 22abb736a9..44842609fa 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.ts @@ -11,6 +11,7 @@ import log from 'loglevel'; import type { Types } from '../NotificationServicesController'; import type { NotificationServicesPushControllerMethodActions } from './NotificationServicesPushController-method-action-types'; import type { ENV } from './services/endpoints'; +import type { RegToken } from './services/services'; import { activatePushNotifications, deleteLinksAPI, @@ -120,6 +121,11 @@ export type ControllerConfig = { */ getLocale?: () => string; + /** + * App or extension version to include when registering push tokens. + */ + appVersion?: string; + /** * Global switch to determine to use push notifications * Allows us to control Builds on extension (MV2 vs MV3) @@ -131,6 +137,11 @@ export type ControllerConfig = { */ platform: 'extension' | 'mobile'; + /** + * Mobile operating system to include when registering push tokens. + */ + os?: 'android' | 'ios'; + /** * Push Service Interface * - create reg token @@ -147,6 +158,11 @@ type StateCommand = | { type: 'disable' } | { type: 'update'; fcmToken: string }; +type RegistrationTokenMetadata = Pick< + RegToken, + 'appVersion' | 'locale' | 'os' | 'platform' +>; + /** * Manages push notifications for the application, including enabling, disabling, and updating triggers for push notifications. * This controller integrates with Firebase Cloud Messaging (FCM) to handle the registration and management of push notifications. @@ -239,6 +255,23 @@ export class NotificationServicesPushController extends BaseController< } } + #getRegistrationTokenMetadata(): RegistrationTokenMetadata { + const tokenMetadata: RegistrationTokenMetadata = { + platform: this.#config.platform, + locale: this.#config.getLocale?.() ?? 'en', + }; + + if (this.#config.os) { + tokenMetadata.os = this.#config.os; + } + + if (this.#config.appVersion) { + tokenMetadata.appVersion = this.#config.appVersion; + } + + return tokenMetadata; + } + public async subscribeToPushNotifications(): Promise { if (!this.#config.isPushFeatureEnabled) { return; @@ -293,8 +326,7 @@ export class NotificationServicesPushController extends BaseController< env: this.#env, createRegToken: this.#config.pushService.createRegToken, regToken: { - platform: this.#config.platform, - locale: this.#config.getLocale?.() ?? 'en', + ...this.#getRegistrationTokenMetadata(), oldToken: this.state.fcmToken, }, controllerEnv: this.#config.env ?? 'prd', @@ -383,8 +415,7 @@ export class NotificationServicesPushController extends BaseController< addresses, regToken: { token: this.state.fcmToken, - platform: this.#config.platform, - locale: this.#config.getLocale?.() ?? 'en', + ...this.#getRegistrationTokenMetadata(), }, env: this.#config.env ?? 'prd', }); @@ -453,8 +484,7 @@ export class NotificationServicesPushController extends BaseController< env: this.#env, createRegToken: this.#config.pushService.createRegToken, regToken: { - platform: this.#config.platform, - locale: this.#config.getLocale?.() ?? 'en', + ...this.#getRegistrationTokenMetadata(), oldToken: this.state.fcmToken, }, controllerEnv: this.#config.env ?? 'prd', diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/__fixtures__/mockServices.ts b/packages/notification-services-controller/src/NotificationServicesPushController/__fixtures__/mockServices.ts index d4610a919e..3603734027 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/__fixtures__/mockServices.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/__fixtures__/mockServices.ts @@ -12,6 +12,7 @@ type MockReply = { export const mockEndpointUpdatePushNotificationLinks = ( mockReply?: MockReply, + requestBody?: nock.RequestBodyMatcher, ): nock.Scope => { const mockResponse = getMockUpdatePushNotificationLinksResponse(); const reply = mockReply ?? { @@ -19,9 +20,13 @@ export const mockEndpointUpdatePushNotificationLinks = ( body: mockResponse.response, }; - const mockEndpoint = nock(mockResponse.url).post('').reply(reply.status); + const endpoint = nock(mockResponse.url); + const mockEndpoint = + requestBody === undefined + ? endpoint.post('') + : endpoint.post('', requestBody); - return mockEndpoint; + return mockEndpoint.reply(reply.status); }; export const mockEndpointDeletePushNotificationLinks = ( diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/services/services.test.ts b/packages/notification-services-controller/src/NotificationServicesPushController/services/services.test.ts index 0c302b25ab..f45494d312 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/services/services.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/services/services.test.ts @@ -11,6 +11,7 @@ import { deleteLinksAPI, updateLinksAPI, } from './services'; +import type { RegToken } from './services'; // Testing util to clean up verbose logs when testing errors const mockErrorLog = (): jest.SpyInstance => @@ -21,9 +22,35 @@ const MOCK_NEW_REG_TOKEN = 'NEW_REG_TOKEN'; const MOCK_ADDRESSES = ['0x123', '0x456', '0x789']; const MOCK_JWT = 'MOCK_JWT'; +type CreateRegTokenMock = jest.Mock< + Promise, + [PushNotificationEnv] +>; + +type ArrangeMocksParams = { + bearerToken: string; + addresses: string[]; + createRegToken: CreateRegTokenMock; + regToken: { + platform: Platform; + locale: string; + }; + env: PushNotificationEnv; +}; + +type ArrangeMocksResult = { + params: ArrangeMocksParams<'extension'>; + mobileParams: ArrangeMocksParams<'mobile'>; + apis: { + mockPut: ReturnType; + }; +}; + describe('NotificationServicesPushController Services', () => { describe('updateLinksAPI', () => { - const act = async (): Promise => + const act = async ( + regTokenOverrides?: Partial, + ): Promise => await updateLinksAPI({ bearerToken: MOCK_JWT, addresses: MOCK_ADDRESSES, @@ -31,6 +58,7 @@ describe('NotificationServicesPushController Services', () => { token: MOCK_NEW_REG_TOKEN, platform: 'extension', locale: 'en', + ...regTokenOverrides, }, }); @@ -55,16 +83,44 @@ describe('NotificationServicesPushController Services', () => { const result = await act(); expect(result).toBe(false); }); + + it('should include mobile metadata when provided', async () => { + const mockAPI = mockEndpointUpdatePushNotificationLinks(undefined, { + addresses: MOCK_ADDRESSES, + registration_token: { + token: MOCK_NEW_REG_TOKEN, + platform: 'mobile', + locale: 'en', + os: 'ios', + appVersion: '7.42.0', + }, + }); + + const result = await act({ + platform: 'mobile', + os: 'ios', + appVersion: '7.42.0', + }); + + expect(mockAPI.isDone()).toBe(true); + expect(result).toBe(true); + }); }); describe('activatePushNotifications', () => { - // Internal testing utility - return type is inferred - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const arrangeMocks = (override?: { mockPut?: { status: number } }) => { + const arrangeMocks = (override?: { + mockPut?: { status: number }; + requestBody?: Parameters< + typeof mockEndpointUpdatePushNotificationLinks + >[1]; + }): ArrangeMocksResult => { + const createRegToken: CreateRegTokenMock = jest + .fn, [PushNotificationEnv]>() + .mockResolvedValue(MOCK_NEW_REG_TOKEN); const params = { bearerToken: MOCK_JWT, addresses: MOCK_ADDRESSES, - createRegToken: jest.fn().mockResolvedValue(MOCK_NEW_REG_TOKEN), + createRegToken, regToken: { platform: 'extension' as const, locale: 'en', @@ -84,7 +140,10 @@ describe('NotificationServicesPushController Services', () => { params, mobileParams, apis: { - mockPut: mockEndpointUpdatePushNotificationLinks(override?.mockPut), + mockPut: mockEndpointUpdatePushNotificationLinks( + override?.mockPut, + override?.requestBody, + ), }, }; }; @@ -127,6 +186,35 @@ describe('NotificationServicesPushController Services', () => { expect(apis.mockPut.isDone()).toBe(true); expect(result).toBe(MOCK_NEW_REG_TOKEN); }); + + it('should pass mobile metadata when provided', async () => { + const { mobileParams, apis } = arrangeMocks({ + requestBody: { + addresses: MOCK_ADDRESSES, + registration_token: { + token: MOCK_NEW_REG_TOKEN, + platform: 'mobile', + locale: 'en', + os: 'android', + appVersion: '7.42.0', + }, + }, + }); + const paramsWithMetadata = { + ...mobileParams, + regToken: { + ...mobileParams.regToken, + os: 'android' as const, + appVersion: '7.42.0', + }, + }; + + const result = await activatePushNotifications(paramsWithMetadata); + + expect(mobileParams.createRegToken).toHaveBeenCalled(); + expect(apis.mockPut.isDone()).toBe(true); + expect(result).toBe(MOCK_NEW_REG_TOKEN); + }); }); describe('deleteLinksAPI', () => { diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/services/services.ts b/packages/notification-services-controller/src/NotificationServicesPushController/services/services.ts index 26895745a1..95067488ea 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/services/services.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/services/services.ts @@ -10,6 +10,8 @@ export type RegToken = { token: string; platform: 'extension' | 'mobile' | 'portfolio'; locale: string; + os?: 'android' | 'ios'; + appVersion?: string; oldToken?: string; }; @@ -26,6 +28,8 @@ export type PushTokenRequest = { token: string; platform: 'extension' | 'mobile' | 'portfolio'; locale: string; + os?: 'android' | 'ios'; + appVersion?: string; oldToken?: string; }; }; @@ -129,7 +133,10 @@ type ActivatePushNotificationsParams = { // Other Request Parameters bearerToken: string; addresses: string[]; - regToken: Pick; + regToken: Pick< + RegToken, + 'appVersion' | 'locale' | 'oldToken' | 'os' | 'platform' + >; }; /** @@ -155,6 +162,8 @@ export async function activatePushNotifications( token: regToken, platform: params.regToken.platform, locale: params.regToken.locale, + os: params.regToken.os, + appVersion: params.regToken.appVersion, oldToken: params.regToken.oldToken, }, env: params.controllerEnv, From bf77444187de0a1cc0bb318f23be18f8616657b4 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Thu, 14 May 2026 14:09:22 +0100 Subject: [PATCH 4/7] Release/984.0.0 (#8813) ## Explanation New minor release for core-backend. Preview with changes here: https://github.com/MetaMask/metamask-mobile/pull/29739 ## 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](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Low risk release bookkeeping: updates package versions/changelogs and bumps `@metamask/core-backend` dependency ranges without changing runtime code in this PR. > > **Overview** > Bumps the monorepo version to `984.0.0` and publishes `@metamask/core-backend@6.3.0` (updating its changelog and compare links). > > Updates `@metamask/assets-controller`, `@metamask/assets-controllers`, and `@metamask/transaction-controller` to depend on `@metamask/core-backend@^6.3.0`, with corresponding changelog entries and `yarn.lock` refresh. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1b7bb1a052e45ab29f44dab75a0338dbc6e78921. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- package.json | 2 +- packages/assets-controller/CHANGELOG.md | 1 + packages/assets-controller/package.json | 2 +- packages/assets-controllers/CHANGELOG.md | 1 + packages/assets-controllers/package.json | 2 +- packages/core-backend/CHANGELOG.md | 7 +++++-- packages/core-backend/package.json | 2 +- packages/transaction-controller/CHANGELOG.md | 4 ++++ packages/transaction-controller/package.json | 2 +- yarn.lock | 8 ++++---- 10 files changed, 20 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index ad2f9197d1..d6208bfc3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "983.0.0", + "version": "984.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 811be3964c..183b1518af 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/transaction-controller` from `^65.3.0` to `^65.4.0` ([#8796](https://github.com/MetaMask/core/pull/8796)) +- Bump `@metamask/core-backend` from `^6.2.2` to `^6.3.0` ([#8813](https://github.com/MetaMask/core/pull/8813)) ## [7.1.2] diff --git a/packages/assets-controller/package.json b/packages/assets-controller/package.json index 1d704de98d..d6450c7c96 100644 --- a/packages/assets-controller/package.json +++ b/packages/assets-controller/package.json @@ -62,7 +62,7 @@ "@metamask/base-controller": "^9.1.0", "@metamask/client-controller": "^1.0.1", "@metamask/controller-utils": "^12.1.0", - "@metamask/core-backend": "^6.2.2", + "@metamask/core-backend": "^6.3.0", "@metamask/keyring-api": "^23.1.0", "@metamask/keyring-controller": "^25.5.0", "@metamask/keyring-internal-api": "^11.0.1", diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 8b75745cd8..7e7c8a9077 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/transaction-controller` from `^65.3.0` to `^65.4.0` ([#8796](https://github.com/MetaMask/core/pull/8796)) +- Bump `@metamask/core-backend` from `^6.2.2` to `^6.3.0` ([#8813](https://github.com/MetaMask/core/pull/8813)) ## [108.1.0] diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 7e78eb1842..2f5f583c50 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -66,7 +66,7 @@ "@metamask/base-controller": "^9.1.0", "@metamask/contract-metadata": "^2.4.0", "@metamask/controller-utils": "^12.1.0", - "@metamask/core-backend": "^6.2.2", + "@metamask/core-backend": "^6.3.0", "@metamask/eth-query": "^4.0.0", "@metamask/keyring-api": "^23.1.0", "@metamask/keyring-controller": "^25.5.0", diff --git a/packages/core-backend/CHANGELOG.md b/packages/core-backend/CHANGELOG.md index eb49833023..ef66ee9b21 100644 --- a/packages/core-backend/CHANGELOG.md +++ b/packages/core-backend/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [6.3.0] + ### Added - Add `OHLCVService` for real-time OHLCV (candlestick) data streaming via WebSocket ([#8695](https://github.com/MetaMask/core/pull/8695)) @@ -68,7 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/accounts-controller` from `^36.0.0` to `^37.0.0` ([#7996](https://github.com/MetaMask/core/pull/7996)), ([#8140](https://github.com/MetaMask/core/pull/8140)) +- Bump `@metamask/accounts-controller` from `^36.0.0` to `^37.0.0` ([#7996](https://github.com/MetaMask/core/pull/7996), [#8140](https://github.com/MetaMask/core/pull/8140)) - Bump `@metamask/controller-utils` from `^11.18.0` to `^11.19.0` ([#7995](https://github.com/MetaMask/core/pull/7995)) ## [6.0.0] @@ -282,7 +284,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Type definitions** - Comprehensive TypeScript types for transactions, balances, WebSocket messages, and service configurations - **Logging infrastructure** - Structured logging with module-specific loggers for debugging and monitoring -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/core-backend@6.2.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/core-backend@6.3.0...HEAD +[6.3.0]: https://github.com/MetaMask/core/compare/@metamask/core-backend@6.2.2...@metamask/core-backend@6.3.0 [6.2.2]: https://github.com/MetaMask/core/compare/@metamask/core-backend@6.2.1...@metamask/core-backend@6.2.2 [6.2.1]: https://github.com/MetaMask/core/compare/@metamask/core-backend@6.2.0...@metamask/core-backend@6.2.1 [6.2.0]: https://github.com/MetaMask/core/compare/@metamask/core-backend@6.1.1...@metamask/core-backend@6.2.0 diff --git a/packages/core-backend/package.json b/packages/core-backend/package.json index 6d9f3276db..18aaf432ad 100644 --- a/packages/core-backend/package.json +++ b/packages/core-backend/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-backend", - "version": "6.2.2", + "version": "6.3.0", "description": "Core backend services for MetaMask", "keywords": [ "Ethereum", diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 50b409eb64..38f10fd91d 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@metamask/core-backend` from `^6.2.2` to `^6.3.0` ([#8813](https://github.com/MetaMask/core/pull/8813)) + ## [65.4.0] ### Added diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 87a81c9bd1..4eda1c387c 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -64,7 +64,7 @@ "@metamask/approval-controller": "^9.0.1", "@metamask/base-controller": "^9.1.0", "@metamask/controller-utils": "^12.1.0", - "@metamask/core-backend": "^6.2.2", + "@metamask/core-backend": "^6.3.0", "@metamask/gas-fee-controller": "^26.2.1", "@metamask/messenger": "^1.2.0", "@metamask/metamask-eth-abis": "^3.1.1", diff --git a/yarn.lock b/yarn.lock index 87c2892c8e..d57ba7d5d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2782,7 +2782,7 @@ __metadata: "@metamask/base-controller": "npm:^9.1.0" "@metamask/client-controller": "npm:^1.0.1" "@metamask/controller-utils": "npm:^12.1.0" - "@metamask/core-backend": "npm:^6.2.2" + "@metamask/core-backend": "npm:^6.3.0" "@metamask/keyring-api": "npm:^23.1.0" "@metamask/keyring-controller": "npm:^25.5.0" "@metamask/keyring-internal-api": "npm:^11.0.1" @@ -2835,7 +2835,7 @@ __metadata: "@metamask/base-controller": "npm:^9.1.0" "@metamask/contract-metadata": "npm:^2.4.0" "@metamask/controller-utils": "npm:^12.1.0" - "@metamask/core-backend": "npm:^6.2.2" + "@metamask/core-backend": "npm:^6.3.0" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-provider-http": "npm:^0.3.0" "@metamask/keyring-api": "npm:^23.1.0" @@ -3368,7 +3368,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/core-backend@npm:^6.2.2, @metamask/core-backend@workspace:packages/core-backend": +"@metamask/core-backend@npm:^6.3.0, @metamask/core-backend@workspace:packages/core-backend": version: 0.0.0-use.local resolution: "@metamask/core-backend@workspace:packages/core-backend" dependencies: @@ -5724,7 +5724,7 @@ __metadata: "@metamask/base-controller": "npm:^9.1.0" "@metamask/connectivity-controller": "npm:^0.2.0" "@metamask/controller-utils": "npm:^12.1.0" - "@metamask/core-backend": "npm:^6.2.2" + "@metamask/core-backend": "npm:^6.3.0" "@metamask/eth-block-tracker": "npm:^15.0.0" "@metamask/eth-json-rpc-provider": "npm:^6.0.1" "@metamask/ethjs-provider-http": "npm:^0.3.0" From d81aeeaf4b522b7d4b147fc932e2fbc1232a47e9 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 14 May 2026 14:19:06 +0100 Subject: [PATCH 5/7] fix: Move gasless flag to extended pay config (#8810) ## What changed - Moved the Relay execute toggle to `confirmations_pay_extended.payStrategies.relay.gaslessEnabled`. - Renamed the flag back to `gaslessEnabled` and removed transaction-pay-controller source/test references to `isGaslessEnabled`. - Updated feature flag tests for the new remote flag namespace. - Updated the transaction-pay-controller changelog entry to reference this PR. ## Why This keeps `confirmations_pay` available for the existing release-scoped flag while allowing the new release behavior to be controlled from `confirmations_pay_extended`. ## Validation - `yarn workspace @metamask/transaction-pay-controller run jest --no-coverage src/utils/feature-flags.test.ts` - `yarn workspace @metamask/transaction-pay-controller run test` - `yarn workspace @metamask/transaction-pay-controller run changelog:validate` - `yarn build` --- > [!NOTE] > **Medium Risk** > Medium risk because it changes the remote feature-flag lookup path/name that gates the Relay `/execute` gasless flow; misconfiguration could unintentionally enable/disable gasless execution. > > **Overview** > Updates the Relay gasless execute toggle to read from `remoteFeatureFlags.confirmations_pay_extended.payStrategies.relay.gaslessEnabled` (renaming from `isGaslessEnabled` and removing the old `confirmations_pay` location). > > Adjusts `isRelayExecuteEnabled` unit tests to match the new namespace/key, and updates the changelog to document the flag move. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9b57908519602e11dec32f1bc0c3f8cc6fac7908. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/transaction-pay-controller/CHANGELOG.md | 1 + .../src/utils/feature-flags.test.ts | 12 ++++++------ .../src/utils/feature-flags.ts | 15 +++++++++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 685920b5a2..c28dec846b 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Rename Relay gasless execution feature flag from `gaslessEnabled` to `isGaslessEnabled` ([#8801](https://github.com/MetaMask/core/pull/8801)) +- Move the Relay gasless execution feature flag to `confirmations_pay_extended.payStrategies.relay.gaslessEnabled` ([#8810](https://github.com/MetaMask/core/pull/8810)) ## [22.4.0] diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts index 79540a25ab..b11a84ac28 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -464,14 +464,14 @@ describe('Feature Flags Utils', () => { expect(isRelayExecuteEnabled(messenger)).toBe(false); }); - it('returns true when isGaslessEnabled is true', () => { + it('returns true when gaslessEnabled is true', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { - confirmations_pay: { + confirmations_pay_extended: { payStrategies: { relay: { - isGaslessEnabled: true, + gaslessEnabled: true, }, }, }, @@ -481,14 +481,14 @@ describe('Feature Flags Utils', () => { expect(isRelayExecuteEnabled(messenger)).toBe(true); }); - it('returns false when isGaslessEnabled is false', () => { + it('returns false when gaslessEnabled is false', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { - confirmations_pay: { + confirmations_pay_extended: { payStrategies: { relay: { - isGaslessEnabled: false, + gaslessEnabled: false, }, }, }, diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index 4f837f24c1..ded5bcac2c 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -133,13 +133,20 @@ export type PayStrategiesConfigRaw = { across?: AcrossConfigRaw; relay?: { enabled?: boolean; - isGaslessEnabled?: boolean; originGasOverhead?: string; pollingInterval?: number; pollingTimeout?: number; }; }; +type FeatureFlagsExtendedRaw = { + payStrategies?: { + relay?: { + gaslessEnabled?: boolean; + }; + }; +}; + export type PayStrategiesConfig = { across: AcrossConfig; relay: { @@ -493,10 +500,10 @@ export function isRelayExecuteEnabled( ): boolean { const state = messenger.call('RemoteFeatureFlagController:getState'); const featureFlags = - (state.remoteFeatureFlags?.confirmations_pay as - | FeatureFlagsRaw + (state.remoteFeatureFlags?.confirmations_pay_extended as + | FeatureFlagsExtendedRaw | undefined) ?? {}; - return featureFlags.payStrategies?.relay?.isGaslessEnabled ?? false; + return featureFlags.payStrategies?.relay?.gaslessEnabled ?? false; } /** From 848b57015204ef435624323205ed05ca725e762a Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Thu, 14 May 2026 14:30:27 +0100 Subject: [PATCH 6/7] fix non-evm token type detection (#8811) ## Explanation Currently, all non-evm tokens are being stored as 'erc20', which is wrong. It is not causing any issues in the client, but needs to be corrected. This fixes it. image ## 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](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Medium Risk** > Adjusts asset-type classification logic used to populate `assetsInfo`, which could affect downstream rendering/formatting for non-EVM assets if CAIP-19 parsing assumptions are wrong, but scope is limited and covered by updated unit tests. > > **Overview** > Fixes incorrect `assetsInfo.type` classification for non-EVM assets by treating any CAIP-19 `*/slip44:*` identifier as `native` (not `erc20`), and by recognizing Solana SPL tokens as `spl` when the chain namespace is Solana and the asset namespace is `token`. > > Updates both the token-metadata transform (`TokenDataSource`) and the `addCustomAsset` pending-metadata path (`AssetsController`) to use the same namespace-aware rules, exports the shared `CaipAssetNamespace` enum, and refreshes docs/tests (including switching examples/fixtures from `solana:.../spl:` to `solana:.../token:`). > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fe92f4bb32439400c23a5edb86000320ff2f47fd. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/assets-controller/CHANGELOG.md | 5 ++ .../assets-controller/src/AssetsController.ts | 11 +++- packages/assets-controller/src/README.md | 2 +- .../src/data-sources/TokenDataSource.test.ts | 58 ++++++++++++++++++- .../src/data-sources/TokenDataSource.ts | 13 ++++- packages/assets-controller/src/types.ts | 2 +- .../bridge-controller/src/selectors.test.ts | 2 +- 7 files changed, 84 insertions(+), 9 deletions(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 183b1518af..3074ae675c 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/transaction-controller` from `^65.3.0` to `^65.4.0` ([#8796](https://github.com/MetaMask/core/pull/8796)) - Bump `@metamask/core-backend` from `^6.2.2` to `^6.3.0` ([#8813](https://github.com/MetaMask/core/pull/8813)) +### Fixed + +- Non-EVM assets with a `slip44` asset namespace (e.g. Bitcoin, Solana native, TRON) are now correctly typed as `native` instead of `erc20` in `assetsInfo` ([#8811](https://github.com/MetaMask/core/pull/8811)) +- Solana SPL tokens (CAIP-19 `solana:.../token:
`) are now correctly typed as `spl` instead of `erc20` in `assetsInfo` ([#8811](https://github.com/MetaMask/core/pull/8811)) + ## [7.1.2] ### Changed diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index 0024243cf6..997e62e13b 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -57,6 +57,7 @@ import type { Hex } from '@metamask/utils'; import { isCaipChainId, isStrictHexString, + KnownCaipNamespace, parseCaipAssetType, parseCaipChainId, } from '@metamask/utils'; @@ -82,7 +83,10 @@ import type { AccountsControllerAccountBalancesUpdatedEvent } from './data-sourc import { SnapDataSource } from './data-sources/SnapDataSource'; import type { StakedBalanceDataSourceConfig } from './data-sources/StakedBalanceDataSource'; import { StakedBalanceDataSource } from './data-sources/StakedBalanceDataSource'; -import { TokenDataSource } from './data-sources/TokenDataSource'; +import { + CaipAssetNamespace, + TokenDataSource, +} from './data-sources/TokenDataSource'; import { CHAINS_WITH_DEFAULT_TRACKED_ASSETS, DEFAULT_TRACKED_ASSETS_BY_CHAIN, @@ -1663,7 +1667,10 @@ export class AssetsController extends BaseController< let tokenType: FungibleAssetMetadata['type'] = 'erc20'; if (this.#isNativeAsset(normalizedAssetId)) { tokenType = 'native'; - } else if (parsed.assetNamespace === 'spl') { + } else if ( + parsed.chain.namespace === KnownCaipNamespace.Solana && + parsed.assetNamespace === CaipAssetNamespace.Token + ) { tokenType = 'spl'; } diff --git a/packages/assets-controller/src/README.md b/packages/assets-controller/src/README.md index 892cfc1623..baebf8606f 100644 --- a/packages/assets-controller/src/README.md +++ b/packages/assets-controller/src/README.md @@ -653,7 +653,7 @@ type Caip19AssetId = string; // - Native ETH: "eip155:1/slip44:60" // - USDC on Ethereum: "eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // - SOL: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501" -// - SPL Token: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/spl:EPjFWdd5..." +// - SPL Token: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5..." // CAIP-2 chain identifier type ChainId = string; diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.test.ts b/packages/assets-controller/src/data-sources/TokenDataSource.test.ts index 15932f3936..8b8cc63df4 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.test.ts @@ -20,8 +20,13 @@ const MOCK_ADDRESS = '0x1234567890123456789012345678901234567890'; const MOCK_TOKEN_ASSET = 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as Caip19AssetId; const MOCK_NATIVE_ASSET = 'eip155:1/slip44:60' as Caip19AssetId; +const MOCK_BTC_ASSET = + 'bip122:000000000019d6689c085ae165831e93/slip44:0' as Caip19AssetId; +const MOCK_SOL_NATIVE_ASSET = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501' as Caip19AssetId; +const MOCK_TRX_ASSET = 'tron:728126428/slip44:195' as Caip19AssetId; const MOCK_SPL_ASSET = - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/spl:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' as Caip19AssetId; + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' as Caip19AssetId; type MockApiClient = { tokens: { @@ -516,6 +521,57 @@ describe('TokenDataSource', () => { expect(context.response.assetsInfo?.[MOCK_SPL_ASSET]?.type).toBe('spl'); }); + it.each([ + { + label: 'Bitcoin (bip122/slip44)', + assetId: MOCK_BTC_ASSET, + chainId: 'bip122:000000000019d6689c085ae165831e93', + name: 'Bitcoin', + symbol: 'BTC', + decimals: 8, + }, + { + label: 'SOL native (solana/slip44)', + assetId: MOCK_SOL_NATIVE_ASSET, + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana', + symbol: 'SOL', + decimals: 9, + }, + { + label: 'TRX native (tron/slip44)', + assetId: MOCK_TRX_ASSET, + chainId: 'tron:728126428', + name: 'TRON', + symbol: 'TRX', + decimals: 6, + }, + ])( + 'middleware types non-EVM slip44 asset as native: $label', + async ({ assetId, chainId, name, symbol, decimals }) => { + const { controller } = setupController({ + messenger: createTestMessenger(), + supportedNetworks: [chainId], + assetsResponse: [ + createMockAssetResponse(assetId, { name, symbol, decimals }), + ], + }); + + const next = jest.fn().mockResolvedValue(undefined); + const context = createMiddlewareContext({ + response: { + detectedAssets: { + 'mock-account-id': [assetId], + }, + }, + }); + + await controller.assetsMiddleware(context, next); + + expect(context.response.assetsInfo?.[assetId]?.type).toBe('native'); + }, + ); + it('middleware merges metadata into existing response', async () => { const anotherAsset = 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f' as Caip19AssetId; diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.ts b/packages/assets-controller/src/data-sources/TokenDataSource.ts index 110831664f..c94198e73d 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.ts @@ -45,7 +45,7 @@ const BULK_SCAN_BATCH_SIZE = 100; const MIN_TOKEN_OCCURRENCES = 3; /** CAIP-19 `assetNamespace` segments used across filtering logic. */ -enum CaipAssetNamespace { +export enum CaipAssetNamespace { Slip44 = 'slip44', Erc20 = 'erc20', Token = 'token', @@ -102,11 +102,18 @@ function transformV3AssetResponseToMetadata( const parsed = parseCaipAssetType(assetId); let tokenType: 'native' | 'erc20' | 'spl' = 'erc20'; - if (nativeAssetIds.has(assetId.toLowerCase())) { + if ( + nativeAssetIds.has(assetId.toLowerCase()) || + parsed.assetNamespace === CaipAssetNamespace.Slip44 + ) { tokenType = 'native'; - } else if (parsed.assetNamespace === 'spl') { + } else if ( + parsed.chain.namespace === KnownCaipNamespace.Solana && + parsed.assetNamespace === CaipAssetNamespace.Token + ) { tokenType = 'spl'; } + // TODO: Add support for Tron trc20 standard const metadata: FungibleAssetMetadata = { // Type derived from assetId diff --git a/packages/assets-controller/src/types.ts b/packages/assets-controller/src/types.ts index d0bf7fe7bc..038fe74df9 100644 --- a/packages/assets-controller/src/types.ts +++ b/packages/assets-controller/src/types.ts @@ -9,7 +9,7 @@ import type { CaipAssetType, CaipChainId, Json } from '@metamask/utils'; * - Native: "eip155:1/slip44:60" (ETH) * - ERC20: "eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" (USDC) * - ERC721: "eip155:1/erc721:0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D/1234" (BAYC #1234) - * - SPL: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/spl:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + * - SPL: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" */ export type Caip19AssetId = CaipAssetType; diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index bd5162f310..2d73863857 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -39,7 +39,7 @@ describe('Bridge Selectors', () => { exchangeRate: '2.5', usdExchangeRate: '1.5', }, - 'solana:101/spl:456': { + 'solana:101/token:456': { exchangeRate: '3.0', }, }, From 59f2ff1eed47c0333b8b5d515d1be7e8c6ec7ee4 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 14 May 2026 14:46:36 +0100 Subject: [PATCH 7/7] Release 985.0.0 (#8814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor release of `@metamask/transaction-pay-controller`. --- > [!NOTE] > **Low Risk** > This is a version/changelog-only release bump with no functional code changes in the diff. > > **Overview** > Updates release metadata by bumping the root `package.json` version to `985.0.0` and `@metamask/transaction-pay-controller` to `22.5.0`. > > Adds a `22.5.0` section to `transaction-pay-controller`’s `CHANGELOG.md` and updates the compare links for the new release. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9c697347c327e846b9069523a08a03ff1ba4ea82. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- package.json | 2 +- packages/transaction-pay-controller/CHANGELOG.md | 6 ++++-- packages/transaction-pay-controller/package.json | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d6208bfc3e..cbbbc9ff43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "984.0.0", + "version": "985.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index c28dec846b..e5c47314c3 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [22.5.0] + ### Added - Add Across submit support for post-quote Predict withdraw flows ([#8761](https://github.com/MetaMask/core/pull/8761)) @@ -14,7 +16,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Rename Relay gasless execution feature flag from `gaslessEnabled` to `isGaslessEnabled` ([#8801](https://github.com/MetaMask/core/pull/8801)) - Move the Relay gasless execution feature flag to `confirmations_pay_extended.payStrategies.relay.gaslessEnabled` ([#8810](https://github.com/MetaMask/core/pull/8810)) ## [22.4.0] @@ -895,7 +896,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#6820](https://github.com/MetaMask/core/pull/6820)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.4.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.5.0...HEAD +[22.5.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.4.0...@metamask/transaction-pay-controller@22.5.0 [22.4.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.3.1...@metamask/transaction-pay-controller@22.4.0 [22.3.1]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.3.0...@metamask/transaction-pay-controller@22.3.1 [22.3.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.2.0...@metamask/transaction-pay-controller@22.3.0 diff --git a/packages/transaction-pay-controller/package.json b/packages/transaction-pay-controller/package.json index 18355ad1d7..1a7d37b1ef 100644 --- a/packages/transaction-pay-controller/package.json +++ b/packages/transaction-pay-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/transaction-pay-controller", - "version": "22.4.0", + "version": "22.5.0", "description": "Manages alternate payment strategies to provide required funds for transactions in MetaMask", "keywords": [ "Ethereum",