From a3d06db77d92a9a60b008af12e0dcbf994137c7a Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 19 Jun 2026 01:36:29 -0700 Subject: [PATCH 1/7] chore: Reassign Wallet Integrations CODEOWNERS to Core Platform (#9133) ## Explanation Reassigns Wallet Integrations owned files to Core platform ## References Fixes: https://consensyssoftware.atlassian.net/browse/WAPI-1546 ## 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) - [x] 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 --- .github/CODEOWNERS | 72 +++++++++++++++++++--------------------------- teams.json | 24 ++++++++-------- 2 files changed, 42 insertions(+), 54 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f5c519f5b9..f1787a0bb7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -80,31 +80,35 @@ /packages/analytics-data-regulation-controller @MetaMask/mobile-platform @MetaMask/extension-platform /packages/geolocation-controller @MetaMask/mobile-platform -## Wallet Integrations Team -/packages/chain-agnostic-permission @MetaMask/wallet-integrations -/packages/eip1193-permission-middleware @MetaMask/wallet-integrations -/packages/multichain-api-middleware @MetaMask/wallet-integrations -/packages/selected-network-controller @MetaMask/wallet-integrations -/packages/eip-5792-middleware @MetaMask/wallet-integrations - ## Core Platform Team -/packages/base-controller @MetaMask/core-platform -/packages/base-data-service @MetaMask/core-platform -/packages/build-utils @MetaMask/core-platform -/packages/composable-controller @MetaMask/core-platform -/packages/connectivity-controller @MetaMask/core-platform -/packages/controller-utils @MetaMask/core-platform -/packages/eth-json-rpc-middleware @MetaMask/core-platform -/packages/messenger @MetaMask/core-platform -/packages/messenger-cli @MetaMask/core-platform -/packages/sample-controllers @MetaMask/core-platform -/packages/polling-controller @MetaMask/core-platform -/packages/preferences-controller @MetaMask/core-platform -/packages/rate-limit-controller @MetaMask/core-platform -/packages/react-data-query @MetaMask/core-platform -/packages/wallet @MetaMask/core-platform -/packages/wallet-cli @MetaMask/core-platform @MetaMask/ocap-kernel -/packages/wallet-framework-docs @MetaMask/core-platform +/packages/base-controller @MetaMask/core-platform +/packages/base-data-service @MetaMask/core-platform +/packages/build-utils @MetaMask/core-platform +/packages/chain-agnostic-permission @MetaMask/core-platform +/packages/composable-controller @MetaMask/core-platform +/packages/connectivity-controller @MetaMask/core-platform +/packages/controller-utils @MetaMask/core-platform +/packages/eip-5792-middleware @MetaMask/core-platform +/packages/eip1193-permission-middleware @MetaMask/core-platform +/packages/eth-block-tracker @MetaMask/core-platform +/packages/eth-json-rpc-middleware @MetaMask/core-platform +/packages/eth-json-rpc-provider @MetaMask/core-platform +/packages/json-rpc-engine @MetaMask/core-platform +/packages/json-rpc-middleware-stream @MetaMask/core-platform +/packages/messenger @MetaMask/core-platform +/packages/messenger-cli @MetaMask/core-platform +/packages/multichain-api-middleware @MetaMask/core-platform +/packages/permission-controller @MetaMask/core-platform +/packages/permission-log-controller @MetaMask/core-platform +/packages/polling-controller @MetaMask/core-platform +/packages/preferences-controller @MetaMask/core-platform +/packages/rate-limit-controller @MetaMask/core-platform +/packages/react-data-query @MetaMask/core-platform +/packages/sample-controllers @MetaMask/core-platform +/packages/selected-network-controller @MetaMask/core-platform +/packages/wallet @MetaMask/core-platform +/packages/wallet-cli @MetaMask/core-platform @MetaMask/ocap-kernel +/packages/wallet-framework-docs @MetaMask/core-platform ## Web3Auth Team /packages/seedless-onboarding-controller @MetaMask/web3auth @@ -116,18 +120,12 @@ ## Joint team ownership /packages/announcement-controller @MetaMask/core-extension-ux @MetaMask/mobile-core-ux /packages/core-backend @MetaMask/core-platform @MetaMask/metamask-assets -/packages/eth-block-tracker @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/eth-json-rpc-middleware/src/methods @MetaMask/confirmations @MetaMask/wallet-api-platform-engineers -/packages/eth-json-rpc-middleware/src/wallet.* @MetaMask/confirmations @MetaMask/wallet-api-platform-engineers -/packages/eth-json-rpc-provider @MetaMask/wallet-integrations @MetaMask/core-platform +/packages/eth-json-rpc-middleware/src/methods @MetaMask/confirmations @MetaMask/core-platform +/packages/eth-json-rpc-middleware/src/wallet.* @MetaMask/confirmations @MetaMask/core-platform /packages/foundryup @MetaMask/mobile-platform @MetaMask/extension-platform -/packages/json-rpc-engine @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/json-rpc-middleware-stream @MetaMask/wallet-integrations @MetaMask/core-platform /packages/keyring-controller @MetaMask/accounts-engineers @MetaMask/core-platform /packages/multichain-network-controller @MetaMask/core-platform @MetaMask/accounts-engineers @MetaMask/metamask-assets /packages/network-controller @MetaMask/core-platform @MetaMask/metamask-assets -/packages/permission-controller @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/permission-log-controller @MetaMask/wallet-integrations @MetaMask/core-platform /packages/remote-feature-flag-controller @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform /packages/storage-service @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform /packages/client-controller @MetaMask/core-platform @MetaMask/extension-platform @MetaMask/mobile-platform @@ -160,8 +158,6 @@ /packages/assets-controllers/CHANGELOG.md @MetaMask/metamask-assets @MetaMask/core-platform /packages/assets-controller/package.json @MetaMask/metamask-assets @MetaMask/core-platform /packages/assets-controller/CHANGELOG.md @MetaMask/metamask-assets @MetaMask/core-platform -/packages/chain-agnostic-permission/package.json @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/chain-agnostic-permission/CHANGELOG.md @MetaMask/wallet-integrations @MetaMask/core-platform /packages/config-registry-controller/CHANGELOG.md @MetaMask/networks @MetaMask/core-platform /packages/config-registry-controller/package.json @MetaMask/networks @MetaMask/core-platform /packages/delegation-controller/package.json @MetaMask/delegation @MetaMask/core-platform @@ -170,10 +166,6 @@ /packages/earn-controller/CHANGELOG.md @MetaMask/earn @MetaMask/core-platform /packages/money-account-balance-service/package.json @MetaMask/earn @MetaMask/core-platform /packages/money-account-balance-service/CHANGELOG.md @MetaMask/earn @MetaMask/core-platform -/packages/eip-5792-middleware/package.json @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/eip-5792-middleware/CHANGELOG.md @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/eip1193-permission-middleware/package.json @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/eip1193-permission-middleware/CHANGELOG.md @MetaMask/wallet-integrations @MetaMask/core-platform /packages/ens-controller/package.json @MetaMask/confirmations @MetaMask/core-platform /packages/ens-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/core-platform /packages/gas-fee-controller/package.json @MetaMask/confirmations @MetaMask/core-platform @@ -192,8 +184,6 @@ /packages/message-manager/CHANGELOG.md @MetaMask/confirmations @MetaMask/core-platform /packages/multichain-account-service/package.json @MetaMask/accounts-engineers @MetaMask/core-platform /packages/multichain-account-service/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform -/packages/multichain-api-middleware/package.json @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/multichain-api-middleware/CHANGELOG.md @MetaMask/wallet-integrations @MetaMask/core-platform /packages/name-controller/package.json @MetaMask/confirmations @MetaMask/core-platform /packages/name-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/core-platform /packages/notification-services-controller/package.json @MetaMask/notifications @MetaMask/core-platform @@ -212,8 +202,6 @@ /packages/profile-metrics-controller/CHANGELOG.md @MetaMask/mobile-platform @MetaMask/extension-platform @MetaMask/core-platform /packages/profile-sync-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform /packages/profile-sync-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform -/packages/selected-network-controller/package.json @MetaMask/wallet-integrations @MetaMask/core-platform -/packages/selected-network-controller/CHANGELOG.md @MetaMask/wallet-integrations @MetaMask/core-platform /packages/signature-controller/package.json @MetaMask/confirmations @MetaMask/core-platform /packages/signature-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/core-platform /packages/smart-transactions-controller/package.json @MetaMask/transactions @MetaMask/core-platform diff --git a/teams.json b/teams.json index f16045176a..637a89fc1f 100644 --- a/teams.json +++ b/teams.json @@ -34,12 +34,12 @@ "metamask/bridge-status-controller": "team-swaps-and-bridge", "metamask/app-metadata-controller": "team-mobile-platform", "metamask/delegation-controller": "team-delegation", - "metamask/chain-agnostic-permission": "team-wallet-integrations", + "metamask/chain-agnostic-permission": "team-core-platform", "metamask/chomp-api-service": "team-earn", - "metamask/eip1193-permission-middleware": "team-wallet-integrations", - "metamask/multichain-api-middleware": "team-wallet-integrations", - "metamask/selected-network-controller": "team-wallet-integrations", - "metamask/eip-5792-middleware": "team-wallet-integrations", + "metamask/eip1193-permission-middleware": "team-core-platform", + "metamask/multichain-api-middleware": "team-core-platform", + "metamask/selected-network-controller": "team-core-platform", + "metamask/eip-5792-middleware": "team-core-platform", "metamask/client-controller": "team-core-platform,team-extension-platform,team-mobile-platform", "metamask/base-controller": "team-core-platform", "metamask/base-data-service": "team-core-platform", @@ -65,17 +65,17 @@ "metamask/claims-controller": "team-shield", "metamask/announcement-controller": "team-core-extension-ux,team-mobile-ux", "metamask/core-backend": "team-core-platform,team-assets", - "metamask/eth-block-tracker": "team-wallet-integrations,team-core-platform", - "metamask/eth-json-rpc-middleware": "team-core-platform,team-confirmations,team-wallet-integrations", - "metamask/eth-json-rpc-provider": "team-wallet-integrations,team-core-platform", + "metamask/eth-block-tracker": "team-core-platform", + "metamask/eth-json-rpc-middleware": "team-core-platform,team-confirmations", + "metamask/eth-json-rpc-provider": "team-core-platform", "metamask/foundryup": "team-mobile-platform,team-extension-platform", - "metamask/json-rpc-engine": "team-wallet-integrations,team-core-platform", - "metamask/json-rpc-middleware-stream": "team-wallet-integrations,team-core-platform", + "metamask/json-rpc-engine": "team-core-platform", + "metamask/json-rpc-middleware-stream": "team-core-platform", "metamask/keyring-controller": "team-accounts-framework,team-core-platform", "metamask/multichain-network-controller": "team-core-platform,team-accounts-framework,team-assets", "metamask/network-controller": "team-core-platform,team-assets", - "metamask/permission-controller": "team-wallet-integrations,team-core-platform", - "metamask/permission-log-controller": "team-wallet-integrations,team-core-platform", + "metamask/permission-controller": "team-core-platform", + "metamask/permission-log-controller": "team-core-platform", "metamask/analytics-controller": "team-extension-platform,team-mobile-platform", "metamask/analytics-data-regulation-controller": "team-extension-platform,team-mobile-platform", "metamask/remote-feature-flag-controller": "team-extension-platform,team-mobile-platform", From 3a224b9227b2a2c6a68956eebd4cacc46c2c6cb0 Mon Sep 17 00:00:00 2001 From: Juanmi <95381763+juanmigdr@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:53:10 +0200 Subject: [PATCH 2/7] chore: deprecate TokensController (#9186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds the optional `isDeprecated` constructor callback to `TokensController`, following the same pattern introduced in [#9182](https://github.com/MetaMask/core/pull/9182) for `CurrencyRateController`, [#9044](https://github.com/MetaMask/core/pull/9044) for `MultichainAssetsRatesController` and `MultichainBalancesController`, [#9033](https://github.com/MetaMask/core/pull/9033) for `TokenRatesController`, `TokenBalancesController`, and `AccountTrackerController`, and [#8945](https://github.com/MetaMask/core/pull/8945) for `TokenListController`. When `isDeprecated()` returns `true`: - **TokensController** — no network requests are issued and no token list enrichment runs; `allTokens`, `allIgnoredTokens`, and `allDetectedTokens` are reset to `{}` at construction and at every entry point (`addToken`, `addTokens`, `ignoreTokens`, `addDetectedTokens`, `updateTokenType`, `watchAsset`, `clearIgnoredTokens`, `NetworkController:stateChange`, and `KeyringController:accountRemoved`). The callback is re-evaluated on each entry point so deprecation can be toggled at runtime without reconstructing the controller — intended for use when `AssetsController` supersedes this controller. Ticket: https://consensyssoftware.atlassian.net/browse/ASSETS-3330 ## Test plan - [x] Added unit tests for construction, runtime toggles, and no-fetch guarantees - [x] `yarn workspace @metamask/assets-controllers run jest --no-coverage src/TokensController.test.ts` --- > [!NOTE] > **Medium Risk** > Clears all persisted token maps when deprecated, which can briefly empty the UI if the flag is misconfigured; behavior is opt-in and aligned with existing controller deprecation patterns. > > **Overview** > Adds an optional **`isDeprecated`** constructor callback to **`TokensController`**, matching the deprecation pattern used on other assets controllers when **`AssetsController`** takes over. > > When **`isDeprecated()`** is true, init skips token-list enrichment, public methods and messenger-driven handlers no-op (or **`updateTokenType`** throws), and **`#enforceDisabledState`** wipes **`allTokens`**, **`allIgnoredTokens`**, and **`allDetectedTokens`** so persisted token lists cannot linger. The flag is re-checked on each entry point so deprecation can flip at runtime without recreating the controller. > > Changelog and broad unit tests cover construction, runtime toggles, and network/event entry points. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0d406f6a205c34e6087f2617b89f4dbefd9bb16d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/assets-controllers/CHANGELOG.md | 2 + .../src/TokensController.test.ts | 252 ++++++++++++++++++ .../src/TokensController.ts | 91 ++++++- 3 files changed, 342 insertions(+), 3 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index edef6dc8c5..95e95a83b0 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `isDeprecated` option to `TokensController` constructor ([#9186](https://github.com/MetaMask/core/pull/9186)) + - When `isDeprecated()` returns `true`, no token list enrichment runs and `allTokens`, `allIgnoredTokens`, and `allDetectedTokens` are reset to `{}` at construction and at every entry point (`addToken`, `addTokens`, `ignoreTokens`, `addDetectedTokens`, `updateTokenType`, `watchAsset`, `clearIgnoredTokens`, `NetworkController:stateChange`, and `KeyringController:accountRemoved`), so no stale token data remains in state. - Add `isDeprecated` option to `CurrencyRateController` constructor ([#9182](https://github.com/MetaMask/core/pull/9182)) - When `isDeprecated()` returns `true`, no API requests are sent and `currencyRates` is reset to `{}` at construction and at every entry point (`setCurrentCurrency`, `updateExchangeRate`, and `_executePoll`), so no stale rates remain in state. - The function is re-evaluated on each entry point so it can be toggled at runtime without reconstructing the controller. diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index db3bf37c7a..e88f0eb588 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -3740,6 +3740,258 @@ describe('TokensController', () => { }); }); + describe('isDeprecated', () => { + const initialState: TokensControllerState = { + allTokens: { + [ChainId.mainnet]: { + '0x0001': [ + { + address: '0x03', + symbol: 'barC', + decimals: 2, + aggregators: [], + image: undefined, + name: undefined, + }, + ], + }, + }, + allIgnoredTokens: { + [ChainId.mainnet]: { + '0x0001': ['0x03'], + }, + }, + allDetectedTokens: { + [ChainId.mainnet]: { + '0x0001': [ + { + address: '0x01', + symbol: 'barA', + decimals: 2, + aggregators: [], + image: undefined, + name: undefined, + }, + ], + }, + }, + }; + + const emptyState: TokensControllerState = { + allTokens: {}, + allIgnoredTokens: {}, + allDetectedTokens: {}, + }; + + it('clears all persisted state at construction when isDeprecated() returns true', async () => { + await withController( + { options: { state: initialState, isDeprecated: () => true } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('preserves persisted state at construction when isDeprecated() returns false', async () => { + await withController( + { options: { state: initialState, isDeprecated: () => false } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + }, + ); + }); + + it('does not throw at construction when isDeprecated() is true and state is already empty', async () => { + await withController( + { options: { isDeprecated: () => true } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not call tokenListService.fetchTokensByChainId at construction when isDeprecated() returns true', async () => { + await withController( + { options: { state: initialState, isDeprecated: () => true } }, + async ({ controller }) => { + // Give any async init work a chance to settle + await new Promise((resolve) => process.nextTick(resolve)); + + // The tokenListService mock is accessed via the controller factory; + // we verify by checking that state was not modified by enrichment + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not add tokens and clears stale state when isDeprecated toggles to true at runtime via addToken', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + const result = await controller.addToken({ + address: '0x05', + symbol: 'NEW', + decimals: 18, + networkClientId: 'mainnet', + }); + + expect(result).toStrictEqual([]); + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not add tokens and clears stale state when isDeprecated toggles to true at runtime via addTokens', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + await controller.addTokens( + [{ address: '0x05', symbol: 'NEW', decimals: 18 }], + 'mainnet', + ); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not ignore tokens and clears stale state when isDeprecated toggles to true at runtime via ignoreTokens', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + controller.ignoreTokens(['0x03'], 'mainnet'); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not add detected tokens and clears stale state when isDeprecated toggles to true at runtime via addDetectedTokens', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + await controller.addDetectedTokens( + [{ address: '0x05', symbol: 'NEW', decimals: 18 }], + { chainId: ChainId.mainnet }, + ); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('throws and clears stale state when isDeprecated toggles to true at runtime via updateTokenType', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + await expect( + controller.updateTokenType('0x03', 'mainnet'), + ).rejects.toThrow('TokensController is deprecated'); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not process watchAsset and clears stale state when isDeprecated toggles to true at runtime via watchAsset', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + await controller.watchAsset({ + asset: { address: '0x05', symbol: 'NEW', decimals: 18 }, + type: 'ERC20', + networkClientId: 'mainnet', + }); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('clears all stale state when isDeprecated toggles to true at runtime via clearIgnoredTokens', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + controller.clearIgnoredTokens(); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('clears stale state on NetworkController:stateChange when isDeprecated toggles to true at runtime', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + ({ controller, triggerNetworkStateChange }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + triggerNetworkStateChange({} as NetworkState, [ + { + op: 'remove', + path: ['networkConfigurationsByChainId', ChainId.mainnet], + }, + ]); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('clears stale state on KeyringController:accountRemoved when isDeprecated toggles to true at runtime', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + ({ controller, triggerAccountRemoved }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + triggerAccountRemoved('0x0001'); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + }); + describe('metadata', () => { it('includes expected state in debug snapshots', async () => { await withController(({ controller }) => { diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index c386f4e4f1..f528a12b52 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -206,6 +206,8 @@ export class TokensController extends BaseController< readonly #abortController: AbortController; + readonly #isDeprecated: () => boolean; + /** * Tokens controller options * @@ -215,18 +217,27 @@ export class TokensController extends BaseController< * @param options.state - Initial state to set on this controller. * @param options.messenger - The messenger. * @param options.tokenListService - Shared service for fetching token metadata per chain. + * @param options.isDeprecated - Optional function that returns true to completely + * disable this controller (no requests, no state updates). When it returns + * `true`, `allTokens`, `allIgnoredTokens`, and `allDetectedTokens` are reset to + * `{}` at construction and at every entry point, so no stale token data remains + * in state. The function is evaluated dynamically on each entry point so it can + * be toggled at runtime. Intended for use when a higher-level controller + * (e.g. AssetsController) supersedes this one. */ constructor({ provider, state, messenger, tokenListService, + isDeprecated = (): boolean => false, }: { chainId: Hex; provider: Provider; state?: Partial; messenger: TokensControllerMessenger; tokenListService: TokenListService; + isDeprecated?: () => boolean; }) { super({ name: controllerName, @@ -239,6 +250,7 @@ export class TokensController extends BaseController< }); this.#provider = provider; + this.#isDeprecated = isDeprecated; this.#selectedAccountId = this.#getSelectedAccount().id; @@ -261,9 +273,37 @@ export class TokensController extends BaseController< (accountAddress: string) => this.#handleOnAccountRemoved(accountAddress), ); - // Enrich persisted tokens with name/rwaData from the token list once at init. - this.#enrichTokensFromTokenList(tokenListService).catch(() => { - // Tokens remain usable without metadata enrichment + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + } else { + // Enrich persisted tokens with name/rwaData from the token list once at init. + this.#enrichTokensFromTokenList(tokenListService).catch(() => { + // Tokens remain usable without metadata enrichment + }); + } + } + + /** + * Clears all persisted token state so that no stale data remains. + * + * Called from every entry point when `isDeprecated()` is true so that a + * runtime toggle propagates to state immediately, even if the controller was + * originally constructed while it was enabled. The update is skipped when + * all three maps are already empty to avoid emitting redundant state changes. + */ + #enforceDisabledState(): void { + const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; + if ( + Object.keys(allTokens).length === 0 && + Object.keys(allIgnoredTokens).length === 0 && + Object.keys(allDetectedTokens).length === 0 + ) { + return; + } + this.update((state) => { + state.allTokens = {}; + state.allIgnoredTokens = {}; + state.allDetectedTokens = {}; }); } @@ -322,6 +362,11 @@ export class TokensController extends BaseController< } #handleOnAccountRemoved(accountAddress: string) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + const isEthAddress = isStrictHexString(accountAddress.toLowerCase()) && isValidHexAddress(accountAddress); @@ -367,6 +412,11 @@ export class TokensController extends BaseController< * @param patches - An array of patch operations performed on the network state. */ #onNetworkStateChange(_: NetworkState, patches: Patch[]) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + // Remove state for deleted networks for (const patch of patches) { if ( @@ -455,6 +505,11 @@ export class TokensController extends BaseController< networkClientId: NetworkClientId; rwaData?: TokenRwaData; }): Promise { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return []; + } + const releaseLock = await this.#mutex.acquire(); const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; @@ -542,6 +597,11 @@ export class TokensController extends BaseController< * @param networkClientId - Optional network client ID used to determine interacting chain ID. */ async addTokens(tokensToImport: Token[], networkClientId: NetworkClientId) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + const releaseLock = await this.#mutex.acquire(); const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; const importedTokensMap: { [key: string]: true } = {}; @@ -622,6 +682,11 @@ export class TokensController extends BaseController< tokenAddressesToIgnore: string[], networkClientId: NetworkClientId, ) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + const interactingChainId = this.messenger.call( 'NetworkController:getNetworkClientById', networkClientId, @@ -679,6 +744,11 @@ export class TokensController extends BaseController< incomingDetectedTokens: Token[], detectionDetails: { selectedAddress?: string; chainId: Hex }, ) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + const releaseLock = await this.#mutex.acquire(); const { chainId } = detectionDetails; @@ -782,6 +852,11 @@ export class TokensController extends BaseController< tokenAddress: string, networkClientId: NetworkClientId, ): Promise { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + throw new Error('TokensController is deprecated'); + } + const chainIdToUse = this.messenger.call( 'NetworkController:getNetworkClientById', networkClientId, @@ -894,6 +969,11 @@ export class TokensController extends BaseController< pageMeta?: Record; requestMetadata?: WatchAssetRequestMetadata; }): Promise { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + if (type !== ERC20) { throw new Error(`Asset of type ${type} not supported`); } @@ -1121,6 +1201,11 @@ export class TokensController extends BaseController< * Removes all tokens from the ignored list. */ clearIgnoredTokens() { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + this.update((state) => { state.allIgnoredTokens = {}; }); From 4d940628bf912a0784ce4d6fc60f8b9fdd57ba47 Mon Sep 17 00:00:00 2001 From: Juanmi <95381763+juanmigdr@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:00:56 +0200 Subject: [PATCH 3/7] Release/1056.0.0 (#9202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Explanation Minor update to assets-controllers (109.2.0) ## References https://consensyssoftware.atlassian.net/browse/ASSETS-3328 https://consensyssoftware.atlassian.net/browse/ASSETS-3330 ## 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 --- > [!NOTE] > **Low Risk** > Release-only version and changelog/lockfile updates; behavioral changes are additive opt-in `isDeprecated` flags documented in 109.2.0, not new logic in this diff. > > **Overview** > **Monorepo release 1056.0.0** that cuts **`@metamask/assets-controllers` at 109.2.0** and updates dependents to depend on `^109.2.0`. > > The diff is **versioning and dependency wiring only** (root `package.json`, `assets-controllers` / `assets-controller` / `bridge-controller` / `transaction-pay-controller` `package.json` files, changelogs, and `yarn.lock`). No controller source changes appear in this PR. > > Per the **109.2.0** changelog entry being released, consumers pick up optional **`isDeprecated`** constructor hooks on **`TokensController`**, **`CurrencyRateController`**, **`MultichainAssetsRatesController`**, and **`MultichainBalancesController`**—when deprecated, those controllers skip network/Snap work and clear relevant state at construction and on each public entry point (re-evaluated at runtime). > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit debcdc7bc17757e12f2e58b4611b6131ef39f1cd. 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 | 2 +- packages/assets-controller/package.json | 2 +- packages/assets-controllers/CHANGELOG.md | 5 ++++- packages/assets-controllers/package.json | 2 +- packages/bridge-controller/CHANGELOG.md | 2 +- packages/bridge-controller/package.json | 2 +- packages/transaction-pay-controller/CHANGELOG.md | 1 + packages/transaction-pay-controller/package.json | 2 +- yarn.lock | 8 ++++---- 10 files changed, 16 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 350be0b05a..29eaed4d97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "1055.0.0", + "version": "1056.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 fe3376bd7f..abfd9c06e0 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/assets-controllers` from `^109.0.0` to `^109.1.0` ([#9110](https://github.com/MetaMask/core/pull/9110)) +- Bump `@metamask/assets-controllers` from `^109.0.0` to `^109.2.0` ([#9110](https://github.com/MetaMask/core/pull/9110), [#9202](https://github.com/MetaMask/core/pull/9202)) - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/transaction-controller` from `^67.1.0` to `^68.0.1` ([#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) - Bump `@metamask/keyring-controller` from `^27.0.0` to `^27.1.0` ([#9129](https://github.com/MetaMask/core/pull/9129)) diff --git a/packages/assets-controller/package.json b/packages/assets-controller/package.json index 69e535aa3a..9eedc9b4b2 100644 --- a/packages/assets-controller/package.json +++ b/packages/assets-controller/package.json @@ -58,7 +58,7 @@ "@ethersproject/providers": "^5.7.0", "@metamask/account-tree-controller": "^7.5.2", "@metamask/accounts-controller": "^39.0.1", - "@metamask/assets-controllers": "^109.1.0", + "@metamask/assets-controllers": "^109.2.0", "@metamask/base-controller": "^9.1.0", "@metamask/client-controller": "^1.0.1", "@metamask/controller-utils": "^12.2.0", diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 95e95a83b0..601b3c529c 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [109.2.0] + ### Added - Add `isDeprecated` option to `TokensController` constructor ([#9186](https://github.com/MetaMask/core/pull/9186)) @@ -3228,7 +3230,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@109.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@109.2.0...HEAD +[109.2.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@109.1.0...@metamask/assets-controllers@109.2.0 [109.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@109.0.0...@metamask/assets-controllers@109.1.0 [109.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@108.6.0...@metamask/assets-controllers@109.0.0 [108.6.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@108.5.0...@metamask/assets-controllers@108.6.0 diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 51d53c136d..58076a70c6 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/assets-controllers", - "version": "109.1.0", + "version": "109.2.0", "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", "keywords": [ "Ethereum", diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 4f1c18a1cd..3da70d90ff 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/assets-controllers` from `^109.0.0` to `^109.1.0` ([#9110](https://github.com/MetaMask/core/pull/9110)) +- Bump `@metamask/assets-controllers` from `^109.0.0` to `^109.2.0` ([#9110](https://github.com/MetaMask/core/pull/9110), [#9202](https://github.com/MetaMask/core/pull/9202)) - Refactor selector unit tests to prepare for V2 QuoteResponse migration ([#9098](https://github.com/MetaMask/core/pull/9098)) - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/assets-controller` from `^9.0.0` to `^9.0.1` ([#9083](https://github.com/MetaMask/core/pull/9083)) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 2089624e90..6dbebfdbc0 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -59,7 +59,7 @@ "@ethersproject/providers": "^5.7.0", "@metamask/accounts-controller": "^39.0.1", "@metamask/assets-controller": "^9.0.1", - "@metamask/assets-controllers": "^109.1.0", + "@metamask/assets-controllers": "^109.2.0", "@metamask/base-controller": "^9.1.0", "@metamask/controller-utils": "^12.2.0", "@metamask/gas-fee-controller": "^26.2.2", diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 0a26f749b7..db7d7b075e 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/ramps-controller` from `^14.2.0` to `^14.3.0` ([#9199](https://github.com/MetaMask/core/pull/9199)) +- Bump `@metamask/assets-controllers` from `^109.1.0` to `^109.2.0` ([#9202](https://github.com/MetaMask/core/pull/9202)) ## [23.12.0] diff --git a/packages/transaction-pay-controller/package.json b/packages/transaction-pay-controller/package.json index 84f04b95ee..f0bf80c771 100644 --- a/packages/transaction-pay-controller/package.json +++ b/packages/transaction-pay-controller/package.json @@ -58,7 +58,7 @@ "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@metamask/assets-controller": "^9.0.1", - "@metamask/assets-controllers": "^109.1.0", + "@metamask/assets-controllers": "^109.2.0", "@metamask/base-controller": "^9.1.0", "@metamask/bridge-controller": "^75.1.1", "@metamask/bridge-status-controller": "^72.1.0", diff --git a/yarn.lock b/yarn.lock index f68fd7e449..1df6fe25f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5650,7 +5650,7 @@ __metadata: "@ethersproject/providers": "npm:^5.7.0" "@metamask/account-tree-controller": "npm:^7.5.2" "@metamask/accounts-controller": "npm:^39.0.1" - "@metamask/assets-controllers": "npm:^109.1.0" + "@metamask/assets-controllers": "npm:^109.2.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/client-controller": "npm:^1.0.1" @@ -5688,7 +5688,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/assets-controllers@npm:^109.1.0, @metamask/assets-controllers@workspace:packages/assets-controllers": +"@metamask/assets-controllers@npm:^109.2.0, @metamask/assets-controllers@workspace:packages/assets-controllers": version: 0.0.0-use.local resolution: "@metamask/assets-controllers@workspace:packages/assets-controllers" dependencies: @@ -5898,7 +5898,7 @@ __metadata: "@ethersproject/providers": "npm:^5.7.0" "@metamask/accounts-controller": "npm:^39.0.1" "@metamask/assets-controller": "npm:^9.0.1" - "@metamask/assets-controllers": "npm:^109.1.0" + "@metamask/assets-controllers": "npm:^109.2.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/controller-utils": "npm:^12.2.0" @@ -8692,7 +8692,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/assets-controller": "npm:^9.0.1" - "@metamask/assets-controllers": "npm:^109.1.0" + "@metamask/assets-controllers": "npm:^109.2.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/bridge-controller": "npm:^75.1.1" From 72d257e9a4d02470fb9f55320e7857ad95fdefc2 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 19 Jun 2026 11:02:51 +0100 Subject: [PATCH 4/7] fix: add context to RPC provider errors (#9144) ## Explanation Provider RPC failures currently preserve only the original provider message, which can make transaction and MetaMask Pay errors difficult to group or diagnose when the same message can come from different chains, endpoints, or RPC methods. This PR prefixes errors thrown from the TransactionController and TransactionPayController provider utilities with the chain ID, endpoint type, and RPC method before rethrowing the original error object. The original error metadata is preserved, while the message now follows the format `RPC : `. Both controller packages are updated so errors are consistent across transaction submission paths and Pay-specific live-balance/RPC paths. ## References ## 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 --- > [!NOTE] > **Medium Risk** > Error message shape changes (replacing the prior `RPC submit:` prefix), which may affect client UX or metrics that match on the old strings; logic is localized to provider error handling. > > **Overview** > RPC failures from **TransactionController** and **TransactionPayController** now carry **chain ID**, **Infura vs Custom endpoint**, and **RPC method** in the error message, using the form `RPC : `. > > Context is applied in shared **`rpcRequest`** helpers: on `provider.request` failure, the **original error** is rethrown after its `message` is updated (preferring nested `data.message` when present). **`#publishTransaction`** no longer wraps publish failures in a new `RPC submit:` error, so **`eth_sendRawTransaction`** and other RPC paths share the same formatting. > > Tests and changelogs are updated for the new messages and behavior. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 409124f13ea8d80ac94d6e362a0b186f3034a7e7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/transaction-controller/CHANGELOG.md | 4 + .../src/TransactionController.test.ts | 29 ++-- .../src/TransactionController.ts | 42 ++--- .../src/utils/provider.test.ts | 64 +++++++- .../src/utils/provider.ts | 73 ++++++++- .../transaction-pay-controller/CHANGELOG.md | 1 + .../src/utils/provider.test.ts | 147 +++++++++++++++++- .../src/utils/provider.ts | 67 +++++++- .../src/utils/transaction.test.ts | 13 +- 9 files changed, 370 insertions(+), 70 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 24443eb4cb..4a653b0df5 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 + +- Add RPC method, chain ID, and endpoint type context to transaction provider errors, including raw transaction submission failures ([#9144](https://github.com/MetaMask/core/pull/9144)) + ## [68.0.1] ### Fixed diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 5af2e7e58a..8a91dc6b17 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -2873,7 +2873,8 @@ describe('TransactionController', () => { ); }); - it('throws with data message if publish fails', async () => { + it('throws with contextual rpcRequest message if publish fails', async () => { + const rpcErrorMessage = `RPC 0x5 Custom eth_sendRawTransaction: ${ERROR_MESSAGE_MOCK}`; const { controller } = setupController({ messengerOptions: { addTransactionApprovalRequest: { @@ -2884,12 +2885,7 @@ describe('TransactionController', () => { rpcRequestMock.mockImplementation(async ({ method }) => { if (method === 'eth_sendRawTransaction') { - // eslint-disable-next-line @typescript-eslint/only-throw-error - throw { - data: { - message: ERROR_MESSAGE_MOCK, - }, - }; + throw new Error(rpcErrorMessage); } if (method === 'eth_getBalance') { @@ -2912,7 +2908,7 @@ describe('TransactionController', () => { }, ); - await expect(result).rejects.toThrow(ERROR_MESSAGE_MOCK); + await expect(result).rejects.toThrow(rpcErrorMessage); }); it('throws with standard message if publish fails', async () => { @@ -3862,7 +3858,7 @@ describe('TransactionController', () => { rpcRequestMock.mockRejectedValueOnce(error); await expect(controller.stopTransaction('2')).rejects.toThrow( - 'RPC submit: Another reason', + 'Another reason', ); const sendRawTransactionCalls = rpcRequestMock.mock.calls.filter( @@ -4224,7 +4220,7 @@ describe('TransactionController', () => { rpcRequestMock.mockRejectedValueOnce(error); await expect(controller.speedUpTransaction('2')).rejects.toThrow( - 'RPC submit: Another reason', + 'Another reason', ); const sendRawTransactionCalls = rpcRequestMock.mock.calls.filter( @@ -4234,11 +4230,10 @@ describe('TransactionController', () => { expect(controller.state.transactions).toHaveLength(1); }); - it('extracts nested data.message and prefixes it with RPC submit', async () => { - const error = { - message: 'Outer message', - data: { message: 'Nested rpc error message' }, - }; + it('propagates contextual rpcRequest errors without masking the message', async () => { + const error = new Error( + 'RPC 0x5 Custom eth_sendRawTransaction: Nested rpc error message', + ); const { controller } = setupController({ options: { state: { @@ -4262,9 +4257,7 @@ describe('TransactionController', () => { rpcRequestMock.mockRejectedValueOnce(error); - await expect(controller.speedUpTransaction('2')).rejects.toThrow( - 'RPC submit: Nested rpc error message', - ); + await expect(controller.speedUpTransaction('2')).rejects.toBe(error); }); it('creates additional transaction with increased gas', async () => { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 442d388d74..7d94dc859a 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -3140,38 +3140,24 @@ export class TransactionController extends BaseController< transactionMeta: TransactionMeta, { skipSubmitHistory }: { skipSubmitHistory?: boolean } = {}, ): Promise { - try { - const { networkClientId, rawTx } = transactionMeta; - - if (!rawTx) { - throw new Error('Missing raw transaction'); - } - - const transactionHash = (await rpcRequest({ - messenger: this.messenger, - networkClientId, - method: 'eth_sendRawTransaction', - params: [rawTx], - })) as string; - - if (skipSubmitHistory !== true) { - this.#updateSubmitHistory(transactionMeta, transactionHash); - } + const { networkClientId, rawTx } = transactionMeta; - return transactionHash; - } catch (error: unknown) { - const errorObject = error as - | { - data?: { message?: string }; - message?: string; - } - | undefined; + if (!rawTx) { + throw new Error('Missing raw transaction'); + } - const errorMessage = - errorObject?.data?.message ?? errorObject?.message ?? String(error); + const transactionHash = (await rpcRequest({ + messenger: this.messenger, + networkClientId, + method: 'eth_sendRawTransaction', + params: [rawTx], + })) as string; - throw new Error(`RPC submit: ${errorMessage}`); + if (skipSubmitHistory !== true) { + this.#updateSubmitHistory(transactionMeta, transactionHash); } + + return transactionHash; } /** diff --git a/packages/transaction-controller/src/utils/provider.test.ts b/packages/transaction-controller/src/utils/provider.test.ts index 09e39134ea..248124da35 100644 --- a/packages/transaction-controller/src/utils/provider.test.ts +++ b/packages/transaction-controller/src/utils/provider.test.ts @@ -1,4 +1,6 @@ import type { NetworkClientId, Provider } from '@metamask/network-controller'; +import { NetworkClientType } from '@metamask/network-controller'; +import { RpcEndpointType } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import type { TransactionControllerMessenger } from '../TransactionController'; @@ -9,6 +11,7 @@ describe('provider utils', () => { const mockProvider = { request: requestMock, } as unknown as Provider; + const chainIdMock = '0xa4b1' as Hex; let messengerCallMock: jest.Mock; let messengerMock: TransactionControllerMessenger; @@ -20,7 +23,27 @@ describe('provider utils', () => { .fn() .mockImplementation((action: string, ...args: unknown[]) => { if (action === 'NetworkController:getNetworkClientById') { - return { provider: mockProvider }; + const networkClientId = args[0] as NetworkClientId; + + return { + configuration: { + chainId: chainIdMock, + type: + networkClientId === 'infuraNetworkClientId' + ? NetworkClientType.Infura + : NetworkClientType.Custom, + rpcEndpoints: [ + { + networkClientId, + type: + networkClientId === 'infuraNetworkClientId' + ? RpcEndpointType.Infura + : RpcEndpointType.Custom, + }, + ], + }, + provider: mockProvider, + }; } if (action === 'NetworkController:findNetworkClientIdByChainId') { @@ -106,6 +129,45 @@ describe('provider utils', () => { params: ['0x123', 'latest'], }), ).rejects.toBe(error); + expect(error.message).toBe( + 'RPC 0xa4b1 Custom eth_getBalance: RPC failed', + ); + }); + + it('identifies Infura provider.request errors', async () => { + const error = new Error('Unauthorized.'); + requestMock.mockRejectedValue(error); + + await expect( + rpcRequest({ + messenger: messengerMock, + networkClientId: 'infuraNetworkClientId' as NetworkClientId, + method: 'eth_getBalance', + params: ['0x123', 'latest'], + }), + ).rejects.toBe(error); + expect(error.message).toBe( + 'RPC 0xa4b1 Infura eth_getBalance: Unauthorized.', + ); + }); + + it('uses nested RPC data messages when available', async () => { + const error = Object.assign(new Error('Outer message'), { + data: { message: 'Nested rpc error message' }, + }); + requestMock.mockRejectedValue(error); + + await expect( + rpcRequest({ + messenger: messengerMock, + networkClientId: 'networkClientIdA' as NetworkClientId, + method: 'eth_getBalance', + params: ['0x123', 'latest'], + }), + ).rejects.toBe(error); + expect(error.message).toBe( + 'RPC 0xa4b1 Custom eth_getBalance: Nested rpc error message', + ); }); it('works when params are undefined', async () => { diff --git a/packages/transaction-controller/src/utils/provider.ts b/packages/transaction-controller/src/utils/provider.ts index 1b4e7fabb1..0187a15852 100644 --- a/packages/transaction-controller/src/utils/provider.ts +++ b/packages/transaction-controller/src/utils/provider.ts @@ -1,4 +1,9 @@ -import type { NetworkClientId, Provider } from '@metamask/network-controller'; +import type { + NetworkClient, + NetworkClientId, + Provider, +} from '@metamask/network-controller'; +import { NetworkClientType } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger, projectLogger } from '../logger'; @@ -32,11 +37,8 @@ export function getProvider({ chainId, networkClientId, }); - return ( - messenger.call('NetworkController:getNetworkClientById', resolvedId) as { - provider: Provider; - } - ).provider; + + return getNetworkClient(messenger, resolvedId).provider; } /** @@ -63,9 +65,24 @@ export async function rpcRequest({ method: string; params?: ProviderRequestParams; }): Promise { - const provider = getProvider({ messenger, chainId, networkClientId }); + const resolvedNetworkClientId = getNetworkClientId({ + messenger, + chainId, + networkClientId, + }); - const response = await provider.request({ method, params }); + const networkClient = getNetworkClient(messenger, resolvedNetworkClientId); + const { provider } = networkClient; + + let response: unknown; + try { + response = await provider.request({ method, params }); + } catch (error) { + throwWithRpcContext(error, { + method, + networkClient, + }); + } log(method, { params, response }); @@ -128,3 +145,43 @@ export function getChainId({ } ).configuration.chainId; } + +function getNetworkClient( + messenger: TransactionControllerMessenger, + networkClientId: NetworkClientId, +): NetworkClient { + return messenger.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ) as NetworkClient; +} + +function throwWithRpcContext( + error: unknown, + { + method, + networkClient, + }: { + method: string; + networkClient: NetworkClient; + }, +): never { + const errorObject = error as { + data?: { message?: string }; + message?: string; + }; + const message = errorObject.data?.message ?? errorObject.message; + const prefix = `RPC ${networkClient.configuration.chainId} ${getEndpointLabel( + networkClient, + )} ${method}`; + const prefixedMessage = `${prefix}: ${message}`; + + errorObject.message = prefixedMessage; + throw error; +} + +function getEndpointLabel(networkClient: NetworkClient): 'Infura' | 'Custom' { + return networkClient.configuration.type === NetworkClientType.Infura + ? 'Infura' + : 'Custom'; +} diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index db7d7b075e..23e43718c8 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 ### Changed +- Add RPC method, chain ID, and endpoint type context to transaction pay provider errors ([#9144](https://github.com/MetaMask/core/pull/9144)) - Bump `@metamask/ramps-controller` from `^14.2.0` to `^14.3.0` ([#9199](https://github.com/MetaMask/core/pull/9199)) - Bump `@metamask/assets-controllers` from `^109.1.0` to `^109.2.0` ([#9202](https://github.com/MetaMask/core/pull/9202)) diff --git a/packages/transaction-pay-controller/src/utils/provider.test.ts b/packages/transaction-pay-controller/src/utils/provider.test.ts index 95f8046d7b..8510fd9179 100644 --- a/packages/transaction-pay-controller/src/utils/provider.test.ts +++ b/packages/transaction-pay-controller/src/utils/provider.test.ts @@ -1,4 +1,5 @@ -import type { Provider } from '@metamask/network-controller'; +import type { NetworkClient, Provider } from '@metamask/network-controller'; +import { NetworkClientType } from '@metamask/network-controller'; import { RpcEndpointType } from '@metamask/network-controller'; import type { NetworkConfiguration } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; @@ -11,6 +12,31 @@ const DEFAULT_NETWORK_CLIENT_ID_MOCK = 'default-client-id'; const INFURA_NETWORK_CLIENT_ID_MOCK = 'mainnet'; const PROVIDER_MOCK = { request: jest.fn() } as unknown as Provider; +function buildNetworkClient( + provider: Provider, + networkClientId = DEFAULT_NETWORK_CLIENT_ID_MOCK, +): Pick { + return { + configuration: { + chainId: CHAIN_ID_MOCK, + type: + networkClientId === INFURA_NETWORK_CLIENT_ID_MOCK + ? NetworkClientType.Infura + : NetworkClientType.Custom, + rpcEndpoints: [ + { + networkClientId, + type: + networkClientId === INFURA_NETWORK_CLIENT_ID_MOCK + ? RpcEndpointType.Infura + : RpcEndpointType.Custom, + }, + ], + }, + provider, + }; +} + describe('provider utils', () => { const { messenger, @@ -26,9 +52,9 @@ describe('provider utils', () => { DEFAULT_NETWORK_CLIENT_ID_MOCK, ); - getNetworkClientByIdMock.mockReturnValue({ - provider: PROVIDER_MOCK, - } as never); + getNetworkClientByIdMock.mockImplementation((networkClientId) => + buildNetworkClient(PROVIDER_MOCK, networkClientId), + ); getNetworkConfigurationByChainIdMock.mockReturnValue(undefined); }); @@ -115,6 +141,16 @@ describe('provider utils', () => { it('calls provider.request with method and params', async () => { const requestMock = jest.fn().mockResolvedValue('0xabc'); getNetworkClientByIdMock.mockReturnValue({ + configuration: { + chainId: CHAIN_ID_MOCK, + type: NetworkClientType.Custom, + rpcEndpoints: [ + { + networkClientId: DEFAULT_NETWORK_CLIENT_ID_MOCK, + type: RpcEndpointType.Custom, + }, + ], + }, provider: { request: requestMock }, } as never); @@ -135,6 +171,16 @@ describe('provider utils', () => { it('calls provider.request without params when omitted', async () => { const requestMock = jest.fn().mockResolvedValue('0x10'); getNetworkClientByIdMock.mockReturnValue({ + configuration: { + chainId: CHAIN_ID_MOCK, + type: NetworkClientType.Custom, + rpcEndpoints: [ + { + networkClientId: DEFAULT_NETWORK_CLIENT_ID_MOCK, + type: RpcEndpointType.Custom, + }, + ], + }, provider: { request: requestMock }, } as never); @@ -150,10 +196,20 @@ describe('provider utils', () => { }); }); - it('propagates provider errors', async () => { + it('prefixes provider errors with custom endpoint context', async () => { const error = new Error('RPC failed'); const requestMock = jest.fn().mockRejectedValue(error); getNetworkClientByIdMock.mockReturnValue({ + configuration: { + chainId: CHAIN_ID_MOCK, + type: NetworkClientType.Custom, + rpcEndpoints: [ + { + networkClientId: DEFAULT_NETWORK_CLIENT_ID_MOCK, + type: RpcEndpointType.Custom, + }, + ], + }, provider: { request: requestMock }, } as never); @@ -164,6 +220,77 @@ describe('provider utils', () => { method: 'eth_blockNumber', }), ).rejects.toBe(error); + expect(error.message).toBe('RPC 0x1 Custom eth_blockNumber: RPC failed'); + }); + + it('prefixes provider errors with Infura endpoint context', async () => { + getNetworkConfigurationByChainIdMock.mockReturnValue({ + rpcEndpoints: [ + { + type: RpcEndpointType.Infura, + networkClientId: INFURA_NETWORK_CLIENT_ID_MOCK, + }, + ], + } as NetworkConfiguration); + + const error = new Error('Unauthorized.'); + const requestMock = jest.fn().mockRejectedValue(error); + getNetworkClientByIdMock.mockReturnValue({ + configuration: { + chainId: CHAIN_ID_MOCK, + type: NetworkClientType.Infura, + rpcEndpoints: [ + { + networkClientId: INFURA_NETWORK_CLIENT_ID_MOCK, + type: RpcEndpointType.Infura, + }, + ], + }, + provider: { request: requestMock }, + } as never); + + await expect( + rpcRequest({ + messenger, + chainId: CHAIN_ID_MOCK, + method: 'eth_getBalance', + options: { preferInfura: true }, + }), + ).rejects.toBe(error); + expect(error.message).toBe( + 'RPC 0x1 Infura eth_getBalance: Unauthorized.', + ); + }); + + it('uses nested RPC data messages when available', async () => { + const error = Object.assign(new Error('Outer message'), { + data: { message: 'Nested rpc error message' }, + }); + const requestMock = jest.fn().mockRejectedValue(error); + getNetworkClientByIdMock.mockReturnValue({ + configuration: { + chainId: CHAIN_ID_MOCK, + type: NetworkClientType.Custom, + rpcEndpoints: [ + { + networkClientId: DEFAULT_NETWORK_CLIENT_ID_MOCK, + type: RpcEndpointType.Custom, + }, + ], + }, + provider: { request: requestMock }, + } as never); + + await expect( + rpcRequest({ + messenger, + chainId: CHAIN_ID_MOCK, + method: 'eth_blockNumber', + }), + ).rejects.toBe(error); + expect(error.message).toBe( + 'RPC 0x1 Custom eth_blockNumber: Nested rpc error message', + ); }); it('uses Infura network client when preferInfura is true', async () => { @@ -178,6 +305,16 @@ describe('provider utils', () => { const requestMock = jest.fn().mockResolvedValue('0x1'); getNetworkClientByIdMock.mockReturnValue({ + configuration: { + chainId: CHAIN_ID_MOCK, + type: NetworkClientType.Infura, + rpcEndpoints: [ + { + networkClientId: INFURA_NETWORK_CLIENT_ID_MOCK, + type: RpcEndpointType.Infura, + }, + ], + }, provider: { request: requestMock }, } as never); diff --git a/packages/transaction-pay-controller/src/utils/provider.ts b/packages/transaction-pay-controller/src/utils/provider.ts index d0d86ac87d..ed793fb3fc 100644 --- a/packages/transaction-pay-controller/src/utils/provider.ts +++ b/packages/transaction-pay-controller/src/utils/provider.ts @@ -1,5 +1,12 @@ -import type { NetworkClientId, Provider } from '@metamask/network-controller'; -import { RpcEndpointType } from '@metamask/network-controller'; +import type { + NetworkClient, + NetworkClientId, + Provider, +} from '@metamask/network-controller'; +import { + NetworkClientType, + RpcEndpointType, +} from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; @@ -107,14 +114,60 @@ export async function rpcRequest({ }: RpcRequestParams): Promise { const networkClientId = getNetworkClientId(messenger, chainId, options); - const { provider } = messenger.call( - 'NetworkController:getNetworkClientById', - networkClientId, - ); + const networkClient = getNetworkClient(messenger, networkClientId); + const { provider } = networkClient; - const response = await provider.request({ method, params }); + let response: unknown; + try { + response = await provider.request({ method, params }); + } catch (error) { + throwWithRpcContext(error, { + method, + networkClient, + }); + } log(method, { chainId, networkClientId, params, response }); return response as TResponse; } + +function getNetworkClient( + messenger: TransactionPayControllerMessenger, + networkClientId: NetworkClientId, +): NetworkClient { + return messenger.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ) as NetworkClient; +} + +function throwWithRpcContext( + error: unknown, + { + method, + networkClient, + }: { + method: string; + networkClient: NetworkClient; + }, +): never { + const errorObject = error as { + data?: { message?: string }; + message?: string; + }; + const message = errorObject.data?.message ?? errorObject.message; + const prefix = `RPC ${networkClient.configuration.chainId} ${getEndpointLabel( + networkClient, + )} ${method}`; + const prefixedMessage = `${prefix}: ${message}`; + + errorObject.message = prefixedMessage; + throw error; +} + +function getEndpointLabel(networkClient: NetworkClient): 'Infura' | 'Custom' { + return networkClient.configuration.type === NetworkClientType.Infura + ? 'Infura' + : 'Custom'; +} diff --git a/packages/transaction-pay-controller/src/utils/transaction.test.ts b/packages/transaction-pay-controller/src/utils/transaction.test.ts index 6189adb600..20e4c8b566 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.test.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.test.ts @@ -1,5 +1,6 @@ import { Interface } from '@ethersproject/abi'; import { abiERC20 } from '@metamask/metamask-eth-abis'; +import { NetworkClientType } from '@metamask/network-controller'; import { TransactionStatus, TransactionType, @@ -707,6 +708,10 @@ describe('getTransferredAmountFromTxHash', () => { receiptFindNetworkMock.mockReturnValue(NETWORK_CLIENT_ID_RECEIPT_MOCK); receiptGetNetworkMock.mockReturnValue({ + configuration: { + chainId: CHAIN_ID_RECEIPT_MOCK, + type: NetworkClientType.Custom, + }, provider: PROVIDER_RECEIPT_MOCK, } as never); }); @@ -1075,11 +1080,13 @@ describe('getTransferredAmountFromTxHash', () => { tokenAddress: ERC20_ADDRESS_RECEIPT_MOCK, walletAddress: WALLET_ADDRESS_RECEIPT_MOCK, }), - ).rejects.toThrow('RPC error'); + ).rejects.toThrow('RPC 0x1 Custom eth_getTransactionReceipt: RPC error'); }); it('propagates provider errors for native when both trace and getTransaction fail', async () => { - PROVIDER_RECEIPT_MOCK.request.mockRejectedValue(new Error('RPC error')); + PROVIDER_RECEIPT_MOCK.request + .mockRejectedValueOnce(new Error('RPC error')) + .mockRejectedValueOnce(new Error('RPC error')); await expect( getTransferredAmountFromTxHash({ @@ -1089,6 +1096,6 @@ describe('getTransferredAmountFromTxHash', () => { tokenAddress: NATIVE_TOKEN_ADDRESS, walletAddress: WALLET_ADDRESS_RECEIPT_MOCK, }), - ).rejects.toThrow('RPC error'); + ).rejects.toThrow('RPC 0x1 Custom eth_getTransactionByHash: RPC error'); }); }); From 001dc20933cd839864707d090ad34528e16f7bbb Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 19 Jun 2026 11:39:02 +0100 Subject: [PATCH 5/7] fix(transaction-pay-controller): fail closed on missing submit hashes (#9201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Explanation Relay and Fiat submit paths should never silently complete without the transaction hash they are expected to produce. This PR makes those paths fail closed with explicit errors when required child transactions are not collected, a final submitted transaction has no hash, or Fiat is selected without an executable quote. The publish-hook fallback remains available for non-Fiat no-quote routes that intentionally skip Pay. Fiat is treated as an explicit Pay route, so a selected Fiat payment method with no quote now throws instead of allowing raw fallback. As supporting cleanup, submit errors now preserve the original `Error` object and stack while adding source prefixes at the Pay, Relay, Fiat, Post-Ramp, Direct mUSD, and vault batch-submission boundaries. ## References ## 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 --- > [!NOTE] > **Medium Risk** > Changes core MetaMask Pay publish and submit behavior for Relay/Fiat paths, including a new hard failure when Fiat is selected without quotes; incorrect edge-case handling could block legitimate payments. > > **Overview** > Relay and Fiat submit paths now **fail closed** when they would previously return without a required transaction hash—empty quote lists, no collected child transactions, or confirmed txs missing a hash all throw explicit errors. The publish hook still skips Pay when there are no quotes for non-Fiat flows, but **Fiat with a selected payment method and no quote** now errors (`Fiat: Missing quote`) instead of falling through. > > Submit failures use a shared **`prefixError`** helper so layered messages (`MetaMask Pay:`, `Relay:`, `Fiat:`, `Post-Ramp:`, `Direct mUSD:`, `Vault:`, `Execute:`) are applied **in place** on the original `Error` (stack preserved); `RelayStrategy` no longer adds an extra submit wrapper. Direct mUSD post-ramp submission is refactored through `submitDirectMusdAfterFiatCompletion`, and order asset validation moves to shared `validateOrderAsset` in fiat utils. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f7e9093f5d89d3c6c39da9d512c1930f25341eae. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .../transaction-pay-controller/CHANGELOG.md | 4 + .../helpers/TransactionPayPublishHook.test.ts | 34 ++++- .../src/helpers/TransactionPayPublishHook.ts | 16 ++- .../src/strategy/fiat/FiatStrategy.test.ts | 49 +++++++ .../src/strategy/fiat/FiatStrategy.ts | 15 ++- .../strategy/fiat/fiat-direct-musd.test.ts | 90 ++++++++++++- .../src/strategy/fiat/fiat-direct-musd.ts | 115 +++++++++++++---- .../strategy/fiat/fiat-submit-simple.test.ts | 2 +- .../src/strategy/fiat/fiat-submit-simple.ts | 2 +- .../fiat-submit-with-transaction-data.test.ts | 2 +- .../fiat/fiat-submit-with-transaction-data.ts | 2 +- .../src/strategy/fiat/fiat-submit.test.ts | 121 +++++++++++++++--- .../src/strategy/fiat/fiat-submit.ts | 94 ++++---------- .../src/strategy/fiat/utils.ts | 41 ++++++ .../src/strategy/relay/RelayBridge.test.ts | 2 + .../src/strategy/relay/RelayStrategy.test.ts | 25 ++-- .../src/strategy/relay/RelayStrategy.ts | 7 +- .../src/strategy/relay/relay-submit.test.ts | 80 +++++++++--- .../src/strategy/relay/relay-submit.ts | 43 ++++++- .../src/utils/error-prefix.test.ts | 29 +++++ .../src/utils/error-prefix.ts | 18 +++ 21 files changed, 617 insertions(+), 174 deletions(-) create mode 100644 packages/transaction-pay-controller/src/utils/error-prefix.test.ts create mode 100644 packages/transaction-pay-controller/src/utils/error-prefix.ts diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 23e43718c8..d0dc9a9c2a 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/ramps-controller` from `^14.2.0` to `^14.3.0` ([#9199](https://github.com/MetaMask/core/pull/9199)) - Bump `@metamask/assets-controllers` from `^109.1.0` to `^109.2.0` ([#9202](https://github.com/MetaMask/core/pull/9202)) +### Fixed + +- Preserve original error stack traces while prefixing MetaMask Pay, Relay, and Fiat submission errors, and fail closed when Fiat submission has no quote or when Relay or direct mUSD Fiat vault submissions complete without a transaction hash ([#9201](https://github.com/MetaMask/core/pull/9201)) + ## [23.12.0] ### Changed diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts index ff6a4eade9..929aa784b2 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts @@ -115,6 +115,26 @@ describe('TransactionPayPublishHook', () => { expect(executeMock).not.toHaveBeenCalled(); }); + it('throws if fiat payment is selected but no quotes are in state', async () => { + getControllerStateMock.mockReturnValue({ + transactionData: { + [TRANSACTION_META_MOCK.id]: { + fiatPayment: { + selectedPaymentMethodId: 'debit-card', + }, + isLoading: false, + tokens: [], + }, + }, + } as TransactionPayControllerState); + + await expect(runHook()).rejects.toThrow( + 'MetaMask Pay: Fiat: Missing quote', + ); + expect(executeMock).not.toHaveBeenCalled(); + expect(updateTransactionMock).not.toHaveBeenCalled(); + }); + it('sets submittedTime on the transaction before executing strategy', async () => { await runHook(); @@ -212,18 +232,20 @@ describe('TransactionPayPublishHook', () => { }); it('throws errors from submit prefixed with MetaMask Pay', async () => { - executeMock.mockRejectedValue(new Error('Test error')); + const error = new Error('Test error'); + executeMock.mockRejectedValue(error); + + const thrown = await runHook().catch((caught) => caught); - await expect(runHook()).rejects.toThrow('MetaMask Pay: Test error'); + expect(thrown).toBe(error); + expect(thrown.message).toBe('MetaMask Pay: Test error'); }); it('cascades MetaMask Pay prefix on top of strategy-level prefixes', async () => { - executeMock.mockRejectedValue( - new Error('Relay submit: Relay execute: backend boom'), - ); + executeMock.mockRejectedValue(new Error('Relay: Execute: backend boom')); await expect(runHook()).rejects.toThrow( - 'MetaMask Pay: Relay submit: Relay execute: backend boom', + 'MetaMask Pay: Relay: Execute: backend boom', ); }); diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts index 48d616fa0c..d7fe049cc6 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts @@ -10,10 +10,12 @@ import type { TransactionPayQuote, } from '../types'; import { accountSupports7702 } from '../utils/7702'; +import { prefixError } from '../utils/error-prefix'; import { getStrategyByName } from '../utils/strategy'; import { updateTransaction } from '../utils/transaction'; const log = createModuleLogger(projectLogger, 'pay-publish-hook'); +const ERROR_PREFIX = 'MetaMask Pay: '; const EMPTY_RESULT = { transactionHash: undefined, @@ -47,8 +49,7 @@ export class TransactionPayPublishHook { return await this.#publishHook(transactionMeta, _signedTx); } catch (error) { log('Error', error); - const message = error instanceof Error ? error.message : String(error); - throw new Error(`MetaMask Pay: ${message}`); + throw prefixError(error, ERROR_PREFIX); } } @@ -62,11 +63,18 @@ export class TransactionPayPublishHook { 'TransactionPayController:getState', ); + const transactionData = controllerState.transactionData?.[transactionId]; const quotes = - (controllerState.transactionData?.[transactionId] - ?.quotes as TransactionPayQuote[]) ?? []; + (transactionData?.quotes as TransactionPayQuote[]) ?? []; + const isFiatSelected = Boolean( + transactionData?.fiatPayment?.selectedPaymentMethodId, + ); if (!quotes?.length) { + if (isFiatSelected) { + throw new Error('Fiat: Missing quote'); + } + log('Skipping as no quotes found'); return EMPTY_RESULT; } diff --git a/packages/transaction-pay-controller/src/strategy/fiat/FiatStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/FiatStrategy.test.ts index 0a3382de04..b342021aeb 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/FiatStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/FiatStrategy.test.ts @@ -44,6 +44,8 @@ describe('FiatStrategy', () => { describe('execute', () => { it('delegates to submitFiatQuotes', async () => { + submitFiatQuotesMock.mockResolvedValue({ transactionHash: '0x1234' }); + await new FiatStrategy().execute({ isSmartTransaction: () => false, quotes: [QUOTE_MOCK], @@ -56,5 +58,52 @@ describe('FiatStrategy', () => { submitFiatQuotesMock.mock.calls[0][0].transaction.txParams.from, ).toBe('0x1'); }); + + it('prefixes execute errors with the Fiat prefix without replacing the Error object', async () => { + const error = new Error('Missing order ID'); + submitFiatQuotesMock.mockRejectedValue(error); + + const thrown = await new FiatStrategy() + .execute({ + isSmartTransaction: () => false, + quotes: [QUOTE_MOCK], + messenger: {} as TransactionPayControllerMessenger, + transaction: { txParams: { from: '0x1' } } as TransactionMeta, + }) + .catch((caught) => caught); + + expect(thrown).toBe(error); + expect(thrown.message).toBe('Fiat: Missing order ID'); + }); + + it('throws if fiat submission returns no transaction hash', async () => { + submitFiatQuotesMock.mockResolvedValue({ transactionHash: undefined }); + + await expect( + new FiatStrategy().execute({ + isSmartTransaction: () => false, + quotes: [QUOTE_MOCK], + messenger: {} as TransactionPayControllerMessenger, + transaction: { txParams: { from: '0x1' } } as TransactionMeta, + }), + ).rejects.toThrow('Fiat: Missing transaction hash'); + }); + + it('preserves nested Post-Ramp and Vault prefixes', async () => { + submitFiatQuotesMock.mockRejectedValue( + new Error('Post-Ramp: Direct mUSD: Vault: Missing transaction hash'), + ); + + await expect( + new FiatStrategy().execute({ + isSmartTransaction: () => false, + quotes: [QUOTE_MOCK], + messenger: {} as TransactionPayControllerMessenger, + transaction: { txParams: { from: '0x1' } } as TransactionMeta, + }), + ).rejects.toThrow( + 'Fiat: Post-Ramp: Direct mUSD: Vault: Missing transaction hash', + ); + }); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/FiatStrategy.ts b/packages/transaction-pay-controller/src/strategy/fiat/FiatStrategy.ts index ad9c823c7f..fca1490140 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/FiatStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/FiatStrategy.ts @@ -4,10 +4,13 @@ import type { PayStrategyGetQuotesRequest, TransactionPayQuote, } from '../../types'; +import { prefixError } from '../../utils/error-prefix'; import { getFiatQuotes } from './fiat-quotes'; import { submitFiatQuotes } from './fiat-submit'; import type { FiatQuote } from './types'; +const ERROR_PREFIX = 'Fiat: '; + export class FiatStrategy implements PayStrategy { async getQuotes( request: PayStrategyGetQuotesRequest, @@ -18,6 +21,16 @@ export class FiatStrategy implements PayStrategy { async execute( request: PayStrategyExecuteRequest, ): ReturnType['execute']> { - return await submitFiatQuotes(request); + try { + const result = await submitFiatQuotes(request); + + if (result.transactionHash === undefined) { + throw new Error('Missing transaction hash'); + } + + return result; + } catch (error) { + throw prefixError(error, ERROR_PREFIX); + } } } diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.test.ts index d6000c2e2d..68e08b5dc0 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.test.ts @@ -435,9 +435,7 @@ describe('fiat-direct-musd', () => { sourceAmountRaw: '5000000', transaction: TRANSACTION_MOCK, }), - ).rejects.toThrow( - 'getAmountData returned no updates for direct mUSD submit', - ); + ).rejects.toThrow('No amount updates'); }); it('throws when nested transactions are missing', async () => { @@ -463,7 +461,89 @@ describe('fiat-direct-musd', () => { sourceAmountRaw: '5000000', transaction, }), - ).rejects.toThrow('Missing nested transactions for direct mUSD submit'); + ).rejects.toThrow('Missing nested transactions'); + }); + + it('prefixes addTransactionBatch errors with Vault', async () => { + const callMock = jest.fn((action: string) => { + if (action === 'TransactionPayController:getAmountData') { + return Promise.resolve({ + updates: [{ data: '0xnewApprove', nestedTransactionIndex: 0 }], + }); + } + + if (action === 'TransactionController:addTransactionBatch') { + throw new Error('batch failed'); + } + + throw new Error(`Unexpected action: ${action}`); + }); + + await expect( + submitDirectMusdVaultDeposit({ + request: getExecuteRequest({ callMock }), + sourceAmountRaw: '5000000', + transaction: TRANSACTION_MOCK, + }), + ).rejects.toThrow('Vault: batch failed'); + }); + + it('throws when no vault transactions are collected', async () => { + const callMock = jest.fn((action: string) => { + if (action === 'TransactionPayController:getAmountData') { + return Promise.resolve({ + updates: [{ data: '0xnewApprove', nestedTransactionIndex: 0 }], + }); + } + + if (action === 'TransactionController:addTransactionBatch') { + return Promise.resolve({ batchId: 'batch-id' }); + } + + throw new Error(`Unexpected action: ${action}`); + }); + + collectTransactionIdsMock.mockReturnValue({ end: jest.fn() }); + + await expect( + submitDirectMusdVaultDeposit({ + request: getExecuteRequest({ callMock }), + sourceAmountRaw: '5000000', + transaction: TRANSACTION_MOCK, + }), + ).rejects.toThrow('No transactions submitted'); + }); + + it('throws when the confirmed vault transaction has no hash', async () => { + const callMock = jest.fn((action: string) => { + if (action === 'TransactionPayController:getAmountData') { + return Promise.resolve({ + updates: [{ data: '0xnewApprove', nestedTransactionIndex: 0 }], + }); + } + + if (action === 'TransactionController:addTransactionBatch') { + return Promise.resolve({ batchId: 'batch-id' }); + } + + throw new Error(`Unexpected action: ${action}`); + }); + + getTransactionMock.mockImplementation((transactionId) => { + if (transactionId === TRANSACTION_ID_MOCK) { + return TRANSACTION_MOCK; + } + + return undefined; + }); + + await expect( + submitDirectMusdVaultDeposit({ + request: getExecuteRequest({ callMock }), + sourceAmountRaw: '5000000', + transaction: TRANSACTION_MOCK, + }), + ).rejects.toThrow('Missing transaction hash'); }); it('skips the vault batch when vaultDisabled is enabled', async () => { @@ -495,7 +575,7 @@ describe('fiat-direct-musd', () => { sourceAmountRaw: '5000000', transaction, }), - ).rejects.toThrow('Missing Money Account address for direct mUSD submit'); + ).rejects.toThrow('Missing Money Account address'); }); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.ts index b6602031a4..9fedef9202 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.ts @@ -1,5 +1,8 @@ import { ORIGIN_METAMASK } from '@metamask/controller-utils'; -import type { Quote as RampsQuote } from '@metamask/ramps-controller'; +import type { + Quote as RampsQuote, + RampsOrder, +} from '@metamask/ramps-controller'; import { TransactionType } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; @@ -14,6 +17,7 @@ import type { TransactionPayRequiredToken, TransactionPayQuote, } from '../../types'; +import { prefixError } from '../../utils/error-prefix'; import { getFiatVaultDisabled } from '../../utils/feature-flags'; import { getNetworkClientId } from '../../utils/provider'; import { buildCaipAssetType, getTokenInfo } from '../../utils/token'; @@ -28,9 +32,13 @@ import type { FiatQuote } from './types'; import { getRampsQuote, getRawSourceAmountFromOrderCryptoAmount, + resolveSourceAmountRaw, + validateOrderAsset, } from './utils'; const log = createModuleLogger(projectLogger, 'fiat-direct-musd'); +const DIRECT_MUSD_ERROR_PREFIX = 'Direct mUSD: '; +const VAULT_ERROR_PREFIX = 'Vault: '; /** * Returns a direct mUSD fiat quote to the Money Account. @@ -117,6 +125,47 @@ export function isDirectMusdMoneyAccountQuote( return quote?.request.isDirectMusdMoneyAccount === true; } +/** + * Submits the direct mUSD post-Ramp path after fiat settlement. + * + * @param options - Submit options. + * @param options.order - Completed fiat order. + * @param options.request - Strategy execute request. + * @returns Hash of the submitted direct mUSD transaction, if available. + */ +export async function submitDirectMusdAfterFiatCompletion({ + order, + request, +}: { + order: RampsOrder; + request: PayStrategyExecuteRequest; +}): Promise<{ transactionHash?: Hex }> { + const { messenger, transaction } = request; + + try { + validateOrderAsset({ + expectedAsset: MUSD_MONAD_FIAT_ASSET, + orderCrypto: order.cryptoCurrency, + transactionId: transaction.id, + }); + + const sourceAmountRaw = await resolveSourceAmountRaw({ + messenger, + order, + fiatAsset: MUSD_MONAD_FIAT_ASSET, + walletAddress: transaction.txParams.from as Hex, + }); + + return await submitDirectMusdVaultDeposit({ + request, + sourceAmountRaw, + transaction, + }); + } catch (error) { + throw prefixError(error, DIRECT_MUSD_ERROR_PREFIX); + } +} + /** * Submits the direct mUSD Money Account vault batch after fiat settlement. * @@ -140,7 +189,7 @@ export async function submitDirectMusdVaultDeposit({ const moneyAccountAddress = transaction.txParams.from as Hex | undefined; if (!moneyAccountAddress) { - throw new Error('Missing Money Account address for direct mUSD submit'); + throw new Error('Missing Money Account address'); } if (getFiatVaultDisabled(messenger)) { @@ -164,7 +213,7 @@ export async function submitDirectMusdVaultDeposit({ ); if (!updates.length) { - throw new Error('getAmountData returned no updates for direct mUSD submit'); + throw new Error('No amount updates'); } const nestedTransactions = updatedTransaction.nestedTransactions?.map( @@ -172,7 +221,7 @@ export async function submitDirectMusdVaultDeposit({ ); if (!nestedTransactions?.length) { - throw new Error('Missing nested transactions for direct mUSD submit'); + throw new Error('Missing nested transactions'); } for (const { nestedTransactionIndex, data } of updates) { @@ -182,7 +231,11 @@ export async function submitDirectMusdVaultDeposit({ } updateTransaction( - { transactionId, messenger, note: 'Direct mUSD fiat: update vault amount' }, + { + transactionId, + messenger, + note: 'Direct mUSD fiat: update vault amount', + }, (tx) => { for (const { nestedTransactionIndex, data } of updates) { if (tx.nestedTransactions?.[nestedTransactionIndex]) { @@ -231,25 +284,29 @@ export async function submitDirectMusdVaultDeposit({ transactionId, }); - await messenger.call('TransactionController:addTransactionBatch', { - from: moneyAccountAddress, - isGasFeeSponsored: true, - isInternal: true, - networkClientId, - origin: ORIGIN_METAMASK, - requireApproval: false, - transactions: nestedTransactions.map((nestedTransaction, index) => ({ - params: { - data: nestedTransaction.data, - to: nestedTransaction.to, - value: nestedTransaction.value ?? '0x0', - }, - type: - index === 0 - ? (nestedTransaction.type ?? TransactionType.tokenMethodApprove) - : TransactionType.contractInteraction, - })), - }); + try { + await messenger.call('TransactionController:addTransactionBatch', { + from: moneyAccountAddress, + isGasFeeSponsored: true, + isInternal: true, + networkClientId, + origin: ORIGIN_METAMASK, + requireApproval: false, + transactions: nestedTransactions.map((nestedTransaction, index) => ({ + params: { + data: nestedTransaction.data, + to: nestedTransaction.to, + value: nestedTransaction.value ?? '0x0', + }, + type: + index === 0 + ? (nestedTransaction.type ?? TransactionType.tokenMethodApprove) + : TransactionType.contractInteraction, + })), + }); + } catch (error) { + throw prefixError(error, VAULT_ERROR_PREFIX); + } end(); @@ -262,12 +319,20 @@ export async function submitDirectMusdVaultDeposit({ transactionIds, }); + if (!transactionIds.length) { + throw new Error('No transactions submitted'); + } + await Promise.all( transactionIds.map((id) => waitForTransactionConfirmed(id, messenger)), ); const hash = getTransaction(transactionIds.slice(-1)[0], messenger)?.hash; + if (!hash) { + throw new Error('Missing transaction hash'); + } + log('Confirmed direct mUSD vault deposit', { hash, moneyAccountAddress, @@ -278,7 +343,7 @@ export async function submitDirectMusdVaultDeposit({ transactionIds, }); - return { transactionHash: hash as Hex | undefined }; + return { transactionHash: hash as Hex }; } function combineDirectMusdFiatQuote({ diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-simple.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-simple.test.ts index bd3c0f43b1..6168dd9e87 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-simple.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-simple.test.ts @@ -172,7 +172,7 @@ describe('submitSimpleRelay', () => { sourceAmountRaw: '1000000000000000000', transaction: TRANSACTION_MOCK, }), - ).rejects.toThrow('Missing Relay quote for fiat submission'); + ).rejects.toThrow('Missing Relay quote'); }); it('throws when rate drift exceeds configured threshold', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-simple.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-simple.ts index 3a90627344..be1aca5e7f 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-simple.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-simple.ts @@ -39,7 +39,7 @@ export async function submitSimpleRelay({ const originalRelayQuote = request.quotes[0].original.relayQuote; if (!originalRelayQuote) { - throw new Error('Missing Relay quote for fiat submission'); + throw new Error('Missing Relay quote'); } const relayRequest: QuoteRequest = { diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-with-transaction-data.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-with-transaction-data.test.ts index 325f77bf0c..99a744eff5 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-with-transaction-data.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-with-transaction-data.test.ts @@ -409,7 +409,7 @@ describe('submitWithCalldataReEncoding', () => { sourceAmountRaw: '1000000000000000000', transaction: TRANSACTION_MOCK, }), - ).rejects.toThrow('Missing Relay quote for fiat submission'); + ).rejects.toThrow('Missing Relay quote'); }); it('falls back to original transaction when getTransaction returns undefined', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-with-transaction-data.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-with-transaction-data.ts index 14c95dfcda..03f975e116 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-with-transaction-data.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit-with-transaction-data.ts @@ -48,7 +48,7 @@ export async function submitWithTransactionData({ const originalRelayQuote = request.quotes[0].original.relayQuote; if (!originalRelayQuote) { - throw new Error('Missing Relay quote for fiat submission'); + throw new Error('Missing Relay quote'); } const feeReserveRaw = calculateFeeReserve({ diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts index c8bb4a6fa3..b1227b0a07 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts @@ -563,16 +563,14 @@ describe('submitFiatQuotes', () => { }); await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing wallet address for fiat submission', + 'Missing wallet address', ); }); it('throws if order ID is missing', async () => { const { request } = getRequest({ orderId: '' }); - await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing order ID for fiat submission', - ); + await expect(submitFiatQuotes(request)).rejects.toThrow('Missing order ID'); }); it('throws if ramps quote is missing from fiat payment state', async () => { @@ -606,7 +604,7 @@ describe('submitFiatQuotes', () => { }; await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing provider code for fiat submission', + 'Missing provider code', ); }); @@ -616,7 +614,7 @@ describe('submitFiatQuotes', () => { }); await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing provider code for fiat submission', + 'Missing provider code', ); }); @@ -626,7 +624,7 @@ describe('submitFiatQuotes', () => { }); await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing provider code for fiat submission', + 'Missing provider code', ); }); @@ -883,7 +881,7 @@ describe('submitFiatQuotes', () => { const { request } = getRequest(); await expect(submitFiatQuotes(request)).rejects.toThrow( - `Unable to resolve token info for fiat asset ${FIAT_ASSET_MOCK.address} on chain ${FIAT_ASSET_MOCK.chainId}`, + `Post-Ramp: Unable to resolve token info for fiat asset ${FIAT_ASSET_MOCK.address} on chain ${FIAT_ASSET_MOCK.chainId}`, ); }); @@ -898,7 +896,7 @@ describe('submitFiatQuotes', () => { }); await expect(submitFiatQuotes(request)).rejects.toThrow( - `Fiat order asset mismatch for transaction ${TRANSACTION_ID_MOCK}: expected ${FIAT_ASSET_CAIP_ID_MOCK.toLowerCase()}, got eip155:137/slip44:60`, + `Post-Ramp: Order asset mismatch for transaction ${TRANSACTION_ID_MOCK}: expected ${FIAT_ASSET_CAIP_ID_MOCK.toLowerCase()}, got eip155:137/slip44:60`, ); }); @@ -913,7 +911,7 @@ describe('submitFiatQuotes', () => { }); await expect(submitFiatQuotes(request)).rejects.toThrow( - `Fiat order chain mismatch for transaction ${TRANSACTION_ID_MOCK}: expected eip155:137, got eip155:1`, + `Post-Ramp: Order chain mismatch for transaction ${TRANSACTION_ID_MOCK}: expected eip155:137, got eip155:1`, ); }); @@ -924,7 +922,7 @@ describe('submitFiatQuotes', () => { const { request } = getRequest(); await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Invalid fiat order crypto amount: 0', + 'Post-Ramp: Invalid fiat order crypto amount: 0', ); }); @@ -932,9 +930,7 @@ describe('submitFiatQuotes', () => { const { request } = getRequest(); request.quotes = []; - await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing fiat quote for relay submission', - ); + await expect(submitFiatQuotes(request)).rejects.toThrow('Missing quote'); }); it('throws if request has multiple fiat quotes', async () => { @@ -942,7 +938,7 @@ describe('submitFiatQuotes', () => { request.quotes = [getFiatQuoteMock(), getFiatQuoteMock()]; await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Multiple fiat quotes are not supported for submission', + 'Post-Ramp: Multiple fiat quotes are not supported for submission', ); }); @@ -952,7 +948,7 @@ describe('submitFiatQuotes', () => { }); await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing Relay quote for fiat submission', + 'Post-Ramp: Missing Relay quote', ); }); @@ -963,7 +959,16 @@ describe('submitFiatQuotes', () => { const { request } = getRequest(); await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Computed fiat order source amount is not positive', + 'Post-Ramp: Computed fiat order source amount is not positive', + ); + }); + + it('throws if post-Ramp submission returns no transaction hash', async () => { + submitRelayQuotesMock.mockResolvedValue({ transactionHash: undefined }); + const { request } = getRequest(); + + await expect(submitFiatQuotes(request)).rejects.toThrow( + 'Post-Ramp: Missing transaction hash', ); }); @@ -1116,6 +1121,88 @@ describe('submitFiatQuotes', () => { ).toBe(false); }); + it('prefixes direct mUSD vault errors', async () => { + getTransactionMock.mockImplementation((transactionId) => + transactionId === TRANSACTION_ID_MOCK + ? MUSD_TRANSACTION_MOCK + : undefined, + ); + const { request } = getRequest({ + quotes: [ + getFiatQuoteMock({ + includeRelayQuote: false, + request: MUSD_QUOTE_REQUEST, + }), + ], + transaction: MUSD_TRANSACTION_MOCK, + }); + + await expect(submitFiatQuotes(request)).rejects.toThrow( + 'Post-Ramp: Direct mUSD: Missing transaction hash', + ); + }); + + it('prefixes direct mUSD addTransactionBatch errors as Vault errors', async () => { + const { callMock, request } = getRequest({ + quotes: [ + getFiatQuoteMock({ + includeRelayQuote: false, + request: MUSD_QUOTE_REQUEST, + }), + ], + transaction: MUSD_TRANSACTION_MOCK, + }); + + callMock.mockImplementation((action: string) => { + if (action === 'TransactionPayController:getState') { + return { + transactionData: { + [MUSD_TRANSACTION_MOCK.id]: { + fiatPayment: { + orderId: ORDER_ID_MOCK, + rampsQuote: RAMPS_QUOTE_MOCK, + }, + isLoading: false, + tokens: [], + }, + }, + }; + } + + if (action === 'TransactionPayController:getFiatOptions') { + return undefined; + } + + if (action === 'RampsController:getOrder') { + return getFiatOrderMock(); + } + + if (action === 'TransactionPayController:getAmountData') { + return Promise.resolve({ + updates: [{ nestedTransactionIndex: 0, data: '0xapprove' }], + }); + } + + if (action === 'NetworkController:findNetworkClientIdByChainId') { + return 'network-client-id-mock'; + } + + if (action === 'TransactionController:addTransactionBatch') { + throw new Error('batch failed'); + } + + if (action === 'RemoteFeatureFlagController:getState') { + return { remoteFeatureFlags: {} }; + } + + throw new Error(`Unexpected action: ${action}`); + }); + + await expect(submitFiatQuotes(request)).rejects.toThrow( + 'Post-Ramp: Direct mUSD: Vault: batch failed', + ); + }); + it('skips the vault batch and returns an empty hash when vaultDisabled is enabled', async () => { const { callMock, request } = getRequest({ quotes: [ diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts index 1607cf18aa..24f505f29e 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts @@ -1,7 +1,4 @@ -import type { - RampsOrder, - RampsOrderCryptoCurrency, -} from '@metamask/ramps-controller'; +import type { RampsOrder } from '@metamask/ramps-controller'; import { RampsOrderStatus } from '@metamask/ramps-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; @@ -13,17 +10,15 @@ import type { TransactionPayFiatOptions, TransactionPayControllerMessenger, } from '../../types'; +import { prefixError } from '../../utils/error-prefix'; import { getFiatOrderPollIntervalMs, getFiatOrderPollTimeoutMs, } from '../../utils/feature-flags'; -import { buildCaipAssetType } from '../../utils/token'; import { updateTransaction } from '../../utils/transaction'; -import type { TransactionPayFiatAsset } from './constants'; -import { MUSD_MONAD_FIAT_ASSET } from './constants'; import { isDirectMusdMoneyAccountQuote, - submitDirectMusdVaultDeposit, + submitDirectMusdAfterFiatCompletion, } from './fiat-direct-musd'; import { submitSimpleRelay } from './fiat-submit-simple'; import { submitWithTransactionData } from './fiat-submit-with-transaction-data'; @@ -33,9 +28,11 @@ import { deriveFiatAssetForFiatPayment, extractProviderCode, resolveSourceAmountRaw, + validateOrderAsset, } from './utils'; const log = createModuleLogger(projectLogger, 'fiat-submit'); +const POST_RAMP_ERROR_PREFIX = 'Post-Ramp: '; const TERMINAL_FAILURE_STATUSES: RampsOrderStatus[] = [ RampsOrderStatus.Cancelled, @@ -72,13 +69,13 @@ export async function submitFiatQuotes( const orderId = fiatPayment?.orderId; if (!orderId) { - throw new Error('Missing order ID for fiat submission'); + throw new Error('Missing order ID'); } const providerCode = extractProviderCode(fiatPayment?.rampsQuote?.provider); if (!providerCode) { - throw new Error('Missing provider code for fiat submission'); + throw new Error('Missing provider code'); } updateTransaction( @@ -102,7 +99,7 @@ export async function submitFiatQuotes( const fiatQuote = request.quotes[0]; if (!fiatQuote) { - throw new Error('Missing fiat quote for relay submission'); + throw new Error('Missing quote'); } const fiatOptions = getFiatOptions(messenger); @@ -128,7 +125,17 @@ export async function submitFiatQuotes( transactionId, }); - return await submitRelayAfterFiatCompletion({ order, request }); + try { + const result = await submitRelayAfterFiatCompletion({ order, request }); + + if (result.transactionHash === undefined) { + throw new Error('Missing transaction hash'); + } + + return result; + } catch (error) { + throw prefixError(error, POST_RAMP_ERROR_PREFIX); + } } function getFiatOptions( @@ -142,46 +149,6 @@ function getFiatOptions( } } -/** - * Validates that the completed order's crypto asset matches the expected fiat asset. - * - * @param options - The validation options. - * @param options.expectedAsset - The expected fiat asset derived from the transaction type. - * @param options.orderCrypto - The crypto currency information from the completed order. - * @param options.transactionId - Transaction ID for error reporting. - */ -function validateOrderAsset({ - expectedAsset, - orderCrypto, - transactionId, -}: { - expectedAsset: TransactionPayFiatAsset; - orderCrypto: RampsOrderCryptoCurrency | undefined; - transactionId: string; -}): void { - const orderAssetId = orderCrypto?.assetId?.toLowerCase(); - const expectedAssetId = buildCaipAssetType( - expectedAsset.chainId, - expectedAsset.address, - ).toLowerCase(); - const expectedChainId = expectedAssetId.split('/')[0]; - const orderChainId = orderCrypto?.chainId?.toLowerCase(); - - if (orderAssetId && orderAssetId !== expectedAssetId) { - throw new Error( - `Fiat order asset mismatch for transaction ${transactionId}: ` + - `expected ${expectedAssetId}, got ${orderAssetId}`, - ); - } - - if (orderChainId && orderChainId !== expectedChainId) { - throw new Error( - `Fiat order chain mismatch for transaction ${transactionId}: ` + - `expected ${expectedChainId}, got ${orderChainId}`, - ); - } -} - /** * Polls the on-ramp order until it reaches a terminal status. * @@ -279,24 +246,7 @@ async function submitRelayAfterFiatCompletion({ const isDirectMusd = isDirectMusdMoneyAccountQuote(fiatQuote); if (isDirectMusd) { - validateOrderAsset({ - expectedAsset: MUSD_MONAD_FIAT_ASSET, - orderCrypto: order.cryptoCurrency, - transactionId, - }); - - const sourceAmountRaw = await resolveSourceAmountRaw({ - messenger, - order, - fiatAsset: MUSD_MONAD_FIAT_ASSET, - walletAddress: transaction.txParams.from as Hex, - }); - - return await submitDirectMusdVaultDeposit({ - request, - sourceAmountRaw, - transaction, - }); + return await submitDirectMusdAfterFiatCompletion({ order, request }); } const fiatAsset = deriveFiatAssetForFiatPayment(transaction, messenger); @@ -317,7 +267,7 @@ async function submitRelayAfterFiatCompletion({ }); if (!fiatQuote.original.relayQuote) { - throw new Error('Missing Relay quote for fiat submission'); + throw new Error('Missing Relay quote'); } const hasNestedCalldata = (transaction.nestedTransactions?.length ?? 0) >= 2; @@ -358,7 +308,7 @@ function getWalletAddress({ : (accountOverride ?? transaction.txParams.from); if (!address) { - throw new Error('Missing wallet address for fiat submission'); + throw new Error('Missing wallet address'); } return address as Hex; diff --git a/packages/transaction-pay-controller/src/strategy/fiat/utils.ts b/packages/transaction-pay-controller/src/strategy/fiat/utils.ts index 34f3b475c8..54c7d6844d 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/utils.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/utils.ts @@ -1,6 +1,7 @@ import type { Quote as RampsQuote, RampsOrder, + RampsOrderCryptoCurrency, } from '@metamask/ramps-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionType } from '@metamask/transaction-controller'; @@ -129,6 +130,46 @@ export async function getRampsQuote({ return quote; } +/** + * Validates that a completed order's crypto asset matches the expected fiat asset. + * + * @param options - The validation options. + * @param options.expectedAsset - The expected fiat asset derived from the transaction type. + * @param options.orderCrypto - The crypto currency information from the completed order. + * @param options.transactionId - Transaction ID for error reporting. + */ +export function validateOrderAsset({ + expectedAsset, + orderCrypto, + transactionId, +}: { + expectedAsset: TransactionPayFiatAsset; + orderCrypto: RampsOrderCryptoCurrency | undefined; + transactionId: string; +}): void { + const orderAssetId = orderCrypto?.assetId?.toLowerCase(); + const expectedAssetId = buildCaipAssetType( + expectedAsset.chainId, + expectedAsset.address, + ).toLowerCase(); + const expectedChainId = expectedAssetId.split('/')[0]; + const orderChainId = orderCrypto?.chainId?.toLowerCase(); + + if (orderAssetId && orderAssetId !== expectedAssetId) { + throw new Error( + `Order asset mismatch for transaction ${transactionId}: ` + + `expected ${expectedAssetId}, got ${orderAssetId}`, + ); + } + + if (orderChainId && orderChainId !== expectedChainId) { + throw new Error( + `Order chain mismatch for transaction ${transactionId}: ` + + `expected ${expectedChainId}, got ${orderChainId}`, + ); + } +} + /** * Resolves the raw source amount for a completed fiat order. * diff --git a/packages/transaction-pay-controller/src/strategy/relay/RelayBridge.test.ts b/packages/transaction-pay-controller/src/strategy/relay/RelayBridge.test.ts index 98f6507c7e..0f90f51893 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/RelayBridge.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/RelayBridge.test.ts @@ -37,6 +37,8 @@ describe('RelayStrategy', () => { describe('execute', () => { it('calls util', async () => { + submitRelayQuotesMock.mockResolvedValue({ transactionHash: '0x1234' }); + await new RelayStrategy().execute({ isSmartTransaction: () => false, quotes: [QUOTE_MOCK], diff --git a/packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.test.ts index 0d44f891d8..947a0553fe 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.test.ts @@ -109,25 +109,27 @@ describe('RelayStrategy', () => { expect(submitRelayQuotesMock).toHaveBeenCalledWith(executeRequest); }); - it('wraps execute errors with the Relay submit prefix', async () => { + it('propagates execute errors without replacing the Error object', async () => { const executeRequest = { messenger, quotes: [], transaction: request.transaction, isSmartTransaction: jest.fn(), } as PayStrategyExecuteRequest; + const error = new Error('Insufficient liquidity'); - submitRelayQuotesMock.mockRejectedValue( - new Error('Relay execute: 422 - Insufficient liquidity'), - ); + submitRelayQuotesMock.mockRejectedValue(error); const strategy = new RelayStrategy(); - await expect(strategy.execute(executeRequest)).rejects.toThrow( - 'Relay submit: Relay execute: 422 - Insufficient liquidity', - ); + const thrown = await strategy + .execute(executeRequest) + .catch((caught) => caught); + + expect(thrown).toBe(error); + expect(thrown.message).toBe('Insufficient liquidity'); }); - it('wraps non-Error throws with the Relay submit prefix', async () => { + it('propagates Relay-prefixed execute errors from submitRelayQuotes', async () => { const executeRequest = { messenger, quotes: [], @@ -135,11 +137,14 @@ describe('RelayStrategy', () => { isSmartTransaction: jest.fn(), } as PayStrategyExecuteRequest; - submitRelayQuotesMock.mockRejectedValue('boom'); + submitRelayQuotesMock.mockRejectedValue( + new Error('Relay: Execute: 422 - Insufficient liquidity'), + ); const strategy = new RelayStrategy(); + await expect(strategy.execute(executeRequest)).rejects.toThrow( - 'Relay submit: boom', + 'Relay: Execute: 422 - Insufficient liquidity', ); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.ts b/packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.ts index 8ed8d23c54..22f6d9c31f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.ts @@ -24,11 +24,6 @@ export class RelayStrategy implements PayStrategy { async execute( request: PayStrategyExecuteRequest, ): ReturnType['execute']> { - try { - return await submitRelayQuotes(request); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Relay submit: ${message}`); - } + return await submitRelayQuotes(request); } } diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index c43dfb22e6..cc0ad27ec8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -206,6 +206,14 @@ describe('Relay Submit Utils', () => { }); describe('submitRelayQuotes', () => { + it('throws if there are no Relay quotes to submit', async () => { + request.quotes = []; + + await expect(submitRelayQuotes(request)).rejects.toThrow( + 'Relay: No quotes to submit', + ); + }); + it('adds transaction', async () => { await submitRelayQuotes(request); @@ -601,7 +609,7 @@ describe('Relay Submit Utils', () => { request.quotes[0].original.steps[0].kind = 'unsupported' as never; await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Unsupported step kind: unsupported', + 'Relay: Unsupported step kind: unsupported', ); }); @@ -625,11 +633,33 @@ describe('Relay Submit Utils', () => { it('does not wait for relay status if same chain', async () => { request.quotes[0].original.details.currencyOut.currency.chainId = 1; - await submitRelayQuotes(request); + const result = await submitRelayQuotes(request); + expect(result.transactionHash).toBe('0x0'); expect(successfulFetchMock).toHaveBeenCalledTimes(0); }); + it('returns fallback hash if same-chain polling returns no target hash', async () => { + request.quotes[0].original.details.currencyOut.currency.chainId = 1; + request.quotes[0].original.steps = [ + { + ...request.quotes[0].original.steps[0], + id: 'deposit', + }, + ]; + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => ({ + ...STATUS_RESPONSE_MOCK, + txHashes: [], + }), + } as Response); + + const result = await submitRelayQuotes(request); + + expect(result.transactionHash).toBe('0x0'); + }); + it('waits for relay status if same chain with single deposit step', async () => { request.quotes[0].original.details.currencyOut.currency.chainId = 1; request.quotes[0].original.steps = [ @@ -656,7 +686,7 @@ describe('Relay Submit Utils', () => { ); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Transaction failed', + 'Relay: Transaction failed', ); }); @@ -666,7 +696,23 @@ describe('Relay Submit Utils', () => { ); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'addTransaction boom', + 'Relay: addTransaction boom', + ); + }); + + it('throws if no Relay child transactions are collected', async () => { + collectTransactionIdsMock.mockReturnValue({ end: jest.fn() }); + + await expect(submitRelayQuotes(request)).rejects.toThrow( + 'Relay: No transactions submitted', + ); + }); + + it('throws if the confirmed Relay child transaction has no hash', async () => { + getTransactionMock.mockReturnValue({} as TransactionMeta); + + await expect(submitRelayQuotes(request)).rejects.toThrow( + 'Relay: Missing transaction hash', ); }); @@ -679,7 +725,7 @@ describe('Relay Submit Utils', () => { } as Response); await expect(submitRelayQuotes(request)).rejects.toThrow( - `Relay request failed with status: ${status}`, + `Relay: Request failed with status: ${status}`, ); }, ); @@ -691,7 +737,7 @@ describe('Relay Submit Utils', () => { } as Response); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Relay returned unrecognized status: unknown_status', + 'Relay: Unrecognized status: unknown_status', ); }); @@ -746,7 +792,7 @@ describe('Relay Submit Utils', () => { }); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Relay polling timed out', + 'Relay: Polling timed out', ); }); @@ -768,7 +814,7 @@ describe('Relay Submit Utils', () => { }); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Relay polling timed out (last status: pending)', + 'Relay: Polling timed out (last status: pending)', ); }); @@ -1369,7 +1415,7 @@ describe('Relay Submit Utils', () => { getLiveTokenBalanceMock.mockResolvedValue('500000'); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Insufficient source token balance for relay deposit. Required: 1000000, Available: 500000', + 'Relay: Insufficient source token balance for relay deposit. Required: 1000000, Available: 500000', ); expect(addTransactionMock).not.toHaveBeenCalled(); @@ -1383,7 +1429,7 @@ describe('Relay Submit Utils', () => { getLiveTokenBalanceMock.mockResolvedValue('500000'); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Insufficient source token balance for relay deposit. Required: 1000000, Available: 500000', + 'Relay: Insufficient source token balance for relay deposit. Required: 1000000, Available: 500000', ); expect(addTransactionBatchMock).not.toHaveBeenCalled(); @@ -1393,7 +1439,7 @@ describe('Relay Submit Utils', () => { getLiveTokenBalanceMock.mockResolvedValue('0'); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Insufficient source token balance for relay deposit. Required: 1000000, Available: 0', + 'Relay: Insufficient source token balance for relay deposit. Required: 1000000, Available: 0', ); expect(addTransactionMock).not.toHaveBeenCalled(); @@ -1419,7 +1465,7 @@ describe('Relay Submit Utils', () => { getLiveTokenBalanceMock.mockRejectedValue(new Error('RPC timeout')); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Cannot validate payment token balance - RPC timeout', + 'Relay: Cannot validate payment token balance - RPC timeout', ); }); @@ -1562,7 +1608,7 @@ describe('Relay Submit Utils', () => { } as Response); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Relay request failed with status: refund', + 'Relay: Request failed with status: refund', ); expect(sweepPolymarketDepositWallet).toHaveBeenCalledWith( FROM_MOCK, @@ -1584,7 +1630,7 @@ describe('Relay Submit Utils', () => { } as Response); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Relay request failed with status: refunded', + 'Relay: Request failed with status: refunded', ); expect(sweepPolymarketDepositWallet).toHaveBeenCalledWith( FROM_MOCK, @@ -1602,7 +1648,7 @@ describe('Relay Submit Utils', () => { } as Response); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Relay request failed with status: timeout', + 'Relay: Request failed with status: timeout', ); expect(sweepPolymarketDepositWallet).toHaveBeenCalledWith( FROM_MOCK, @@ -1759,7 +1805,7 @@ describe('Relay Submit Utils', () => { } as Response); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Relay execute: 422 - failed to decode param in array[0] invalid JSON input', + 'Relay: Execute: 422 - failed to decode param in array[0] invalid JSON input', ); }); @@ -1768,7 +1814,7 @@ describe('Relay Submit Utils', () => { successfulFetchMock.mockRejectedValueOnce('network down'); await expect(submitRelayQuotes(request)).rejects.toThrow( - 'Relay execute: network down', + 'Relay: Execute: network down', ); }); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 29b1c26271..1faf269885 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -15,6 +15,7 @@ import type { TransactionPayControllerMessenger, TransactionPayQuote, } from '../../types'; +import { prefixError } from '../../utils/error-prefix'; import { getFeatureFlags, getRelayPollingInterval, @@ -54,6 +55,8 @@ import type { const FALLBACK_HASH = '0x0' as Hex; const log = createModuleLogger(projectLogger, 'relay-strategy'); +const RELAY_ERROR_PREFIX = 'Relay: '; +const RELAY_EXECUTE_ERROR_PREFIX = 'Execute: '; /** * Submits Relay quotes. @@ -63,11 +66,25 @@ const log = createModuleLogger(projectLogger, 'relay-strategy'); */ export async function submitRelayQuotes( request: PayStrategyExecuteRequest, +): Promise<{ transactionHash?: Hex }> { + try { + return await submitRelayQuotesInternal(request); + } catch (error) { + throw prefixError(error, RELAY_ERROR_PREFIX); + } +} + +async function submitRelayQuotesInternal( + request: PayStrategyExecuteRequest, ): Promise<{ transactionHash?: Hex }> { log('Executing quotes', request); const { quotes, messenger, transaction } = request; + if (!quotes.length) { + throw new Error('No quotes to submit'); + } + let transactionHash: Hex | undefined; for (const quote of quotes) { @@ -78,6 +95,11 @@ export async function submitRelayQuotes( )); } + /* istanbul ignore if: concrete Relay submit paths return a hash/fallback or throw. */ + if (transactionHash === undefined) { + throw new Error('Missing transaction hash'); + } + return { transactionHash }; } @@ -139,7 +161,7 @@ async function executeSingleQuote( }); if (completion.status !== 'success') { - throw new Error(`Relay request failed with status: ${completion.status}`); + throw new Error(`Request failed with status: ${completion.status}`); } } @@ -234,7 +256,7 @@ async function waitForRelayCompletion( if (status.status === 'success') { const targetHash = - (status.txHashes?.slice(-1)[0] as Hex) ?? FALLBACK_HASH; + (status.txHashes?.slice(-1)[0] as Hex | undefined) ?? FALLBACK_HASH; return { status: 'success', targetHash }; } @@ -244,9 +266,9 @@ async function waitForRelayCompletion( log('Relay ended in failure status (tolerated)', status.status); return { status: status.status }; } - throw new Error(`Relay request failed with status: ${status.status}`); + throw new Error(`Request failed with status: ${status.status}`); } - throw new Error(`Relay returned unrecognized status: ${status.status}`); + throw new Error(`Unrecognized status: ${status.status}`); } } @@ -256,7 +278,7 @@ async function waitForRelayCompletion( log('Relay polling timed out (tolerated)', statusDetail); return { status: 'timeout' }; } - throw new Error(`Relay polling timed out${statusDetail}`); + throw new Error(`Polling timed out${statusDetail}`); } await new Promise((resolve) => setTimeout(resolve, pollingInterval)); @@ -575,8 +597,7 @@ async function submitViaRelayExecute( try { result = await submitRelayExecute(messenger, executeBody); } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Relay execute: ${message}`); + throw prefixError(error, RELAY_EXECUTE_ERROR_PREFIX); } log('Relay execute response', result); @@ -734,6 +755,10 @@ async function submitViaTransactionController( log('Added transactions', transactionIds); + if (!transactionIds.length) { + throw new Error('No transactions submitted'); + } + if (result) { const txHash = await result.result; log('Submitted transaction', txHash); @@ -747,6 +772,10 @@ async function submitViaTransactionController( const hash = getTransaction(transactionIds.slice(-1)[0], messenger)?.hash; + if (!hash) { + throw new Error('Missing transaction hash'); + } + return hash as Hex; } diff --git a/packages/transaction-pay-controller/src/utils/error-prefix.test.ts b/packages/transaction-pay-controller/src/utils/error-prefix.test.ts new file mode 100644 index 0000000000..1488a7af95 --- /dev/null +++ b/packages/transaction-pay-controller/src/utils/error-prefix.test.ts @@ -0,0 +1,29 @@ +import { prefixError } from './error-prefix'; + +describe('prefixError', () => { + it('prefixes Error messages without replacing the Error object', () => { + const error = new Error('boom'); + const { stack } = error; + + const result = prefixError(error, 'Test: '); + + expect(result).toBe(error); + expect(result.message).toBe('Test: boom'); + expect(result.stack).toBe(stack); + }); + + it('does not duplicate prefixes', () => { + const error = new Error('Test: boom'); + + const result = prefixError(error, 'Test: '); + + expect(result.message).toBe('Test: boom'); + }); + + it('converts non-Error throws to prefixed Errors', () => { + const result = prefixError('boom', 'Test: '); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Test: boom'); + }); +}); diff --git a/packages/transaction-pay-controller/src/utils/error-prefix.ts b/packages/transaction-pay-controller/src/utils/error-prefix.ts new file mode 100644 index 0000000000..5367493ff3 --- /dev/null +++ b/packages/transaction-pay-controller/src/utils/error-prefix.ts @@ -0,0 +1,18 @@ +/** + * Prefix an error message while preserving the original Error object and stack. + * + * @param error - Error or thrown value to prefix. + * @param prefix - Prefix to prepend when missing. + * @returns The prefixed Error object. + */ +export function prefixError(error: unknown, prefix: string): Error { + if (error instanceof Error) { + if (!error.message.startsWith(prefix)) { + error.message = `${prefix}${error.message}`; + } + + return error; + } + + return new Error(`${prefix}${String(error)}`); +} From 88638bee6b188053c43f9a289969ab1cd6504226 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 19 Jun 2026 12:31:09 +0100 Subject: [PATCH 6/7] Release 1057.0.0 (#9203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release of: - `@metamask/transaction-controller` (minor): 68.0.1 → 68.1.0 - `@metamask/transaction-pay-controller` (minor): 23.12.0 → 23.13.0 --- > [!NOTE] > **Medium Risk** > Wide bump of transaction-controller across payment, bridge, and signing-adjacent packages; 68.1.0 changes error shaping on the tx/RPC path, which is critical even if submission logic is unchanged. > > **Overview** > **Monorepo release 1057.0.0** cuts new versions of `@metamask/transaction-controller` (**68.0.1 → 68.1.0**) and `@metamask/transaction-pay-controller` (**23.12.0 → 23.13.0**), with changelog links and `yarn.lock` updated across the tree. > > The shipped behavior in this release (documented in those changelogs, not in this diff) is **richer provider errors**: RPC method, chain ID, and endpoint type are attached to transaction and MetaMask Pay provider failures, including raw tx submission errors ([#9144](https://github.com/MetaMask/core/pull/9144)). Pay’s **23.13.0** entry also records that alignment plus routine dependency bumps (e.g. `transaction-controller`, `assets-controllers`, `ramps-controller`). > > This PR **propagates** `@metamask/transaction-controller` to **^68.1.0** in many workspace packages (bridge, assets, EIP-5792 middleware, phishing, shield, smart-transactions, subscription, user-operation, etc.) and updates their **Unreleased** changelog lines to cite [#9203](https://github.com/MetaMask/core/pull/9203). > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4d6e6027cf9398aeddcef7c9b0bb877ae7a279d8. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- package.json | 2 +- packages/assets-controller/CHANGELOG.md | 2 +- packages/assets-controller/package.json | 2 +- packages/assets-controllers/CHANGELOG.md | 4 +++ packages/assets-controllers/package.json | 2 +- packages/bridge-controller/CHANGELOG.md | 2 +- packages/bridge-controller/package.json | 2 +- .../bridge-status-controller/CHANGELOG.md | 2 +- .../bridge-status-controller/package.json | 2 +- packages/earn-controller/package.json | 2 +- packages/eip-5792-middleware/CHANGELOG.md | 2 +- packages/eip-5792-middleware/package.json | 2 +- .../gator-permissions-controller/CHANGELOG.md | 2 +- .../gator-permissions-controller/package.json | 2 +- .../CHANGELOG.md | 2 +- .../package.json | 2 +- packages/perps-controller/package.json | 2 +- packages/phishing-controller/CHANGELOG.md | 2 +- packages/phishing-controller/package.json | 2 +- .../profile-metrics-controller/CHANGELOG.md | 2 +- .../profile-metrics-controller/package.json | 2 +- packages/shield-controller/CHANGELOG.md | 2 +- packages/shield-controller/package.json | 2 +- .../CHANGELOG.md | 4 +++ .../package.json | 2 +- packages/subscription-controller/CHANGELOG.md | 2 +- packages/subscription-controller/package.json | 2 +- packages/transaction-controller/CHANGELOG.md | 5 ++- packages/transaction-controller/package.json | 2 +- .../transaction-pay-controller/CHANGELOG.md | 6 +++- .../transaction-pay-controller/package.json | 4 +-- .../user-operation-controller/CHANGELOG.md | 2 +- .../user-operation-controller/package.json | 2 +- yarn.lock | 34 +++++++++---------- 34 files changed, 64 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 29eaed4d97..321546c6d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "1056.0.0", + "version": "1057.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 abfd9c06e0..10c4f7edb6 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/assets-controllers` from `^109.0.0` to `^109.2.0` ([#9110](https://github.com/MetaMask/core/pull/9110), [#9202](https://github.com/MetaMask/core/pull/9202)) - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) -- Bump `@metamask/transaction-controller` from `^67.1.0` to `^68.0.1` ([#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^67.1.0` to `^68.1.0` ([#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) - Bump `@metamask/keyring-controller` from `^27.0.0` to `^27.1.0` ([#9129](https://github.com/MetaMask/core/pull/9129)) ### Fixed diff --git a/packages/assets-controller/package.json b/packages/assets-controller/package.json index 9eedc9b4b2..36f5bddb52 100644 --- a/packages/assets-controller/package.json +++ b/packages/assets-controller/package.json @@ -76,7 +76,7 @@ "@metamask/preferences-controller": "^23.1.0", "@metamask/snaps-controllers": "^19.0.0", "@metamask/snaps-utils": "^12.1.2", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "async-mutex": "^0.5.0", "bignumber.js": "^9.1.2", diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 601b3c529c..137dd64c3a 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@metamask/transaction-controller` from `^68.0.1` to `^68.1.0` ([#9203](https://github.com/MetaMask/core/pull/9203)) + ## [109.2.0] ### Added diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 58076a70c6..466ce30706 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -85,7 +85,7 @@ "@metamask/snaps-sdk": "^11.0.0", "@metamask/snaps-utils": "^12.1.2", "@metamask/storage-service": "^1.0.2", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "@tanstack/query-core": "^5.62.16", "@types/bn.js": "^5.1.5", diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 3da70d90ff..eabf76d55f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/assets-controller` from `^9.0.0` to `^9.0.1` ([#9083](https://github.com/MetaMask/core/pull/9083)) - Bump `@metamask/controller-utils` from `^12.1.1` to `^12.2.0` ([#9083](https://github.com/MetaMask/core/pull/9083)) -- Bump `@metamask/transaction-controller` from `^67.1.0` to `^68.0.1` ([#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^67.1.0` to `^68.1.0` ([#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) - Bump `@metamask/profile-sync-controller` from `^28.1.1` to `^28.2.0` ([#9119](https://github.com/MetaMask/core/pull/9119)) ## [75.1.1] diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 6dbebfdbc0..c3ace53127 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -72,7 +72,7 @@ "@metamask/profile-sync-controller": "^28.2.0", "@metamask/remote-feature-flag-controller": "^4.2.2", "@metamask/snaps-controllers": "^19.0.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "bignumber.js": "^9.1.2", "reselect": "^5.1.1", diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 58dc7c29e2..a9a5be65ef 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/controller-utils` from `^12.1.1` to `^12.2.0` ([#9083](https://github.com/MetaMask/core/pull/9083)) - Bump `@metamask/bridge-controller` from `^75.0.0` to `^75.1.1` ([#9078](https://github.com/MetaMask/core/pull/9078)) -- Bump `@metamask/transaction-controller` from `^67.1.0` to `^68.0.1` ([#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^67.1.0` to `^68.1.0` ([#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) - Bump `@metamask/profile-sync-controller` from `^28.1.1` to `^28.2.0` ([#9119](https://github.com/MetaMask/core/pull/9119)) - Bump `@metamask/keyring-controller` from `^27.0.0` to `^27.1.0` ([#9129](https://github.com/MetaMask/core/pull/9129)) diff --git a/packages/bridge-status-controller/package.json b/packages/bridge-status-controller/package.json index eaaf94a876..8b5f0488ff 100644 --- a/packages/bridge-status-controller/package.json +++ b/packages/bridge-status-controller/package.json @@ -64,7 +64,7 @@ "@metamask/profile-sync-controller": "^28.2.0", "@metamask/snaps-controllers": "^19.0.0", "@metamask/superstruct": "^3.1.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "bignumber.js": "^9.1.2", "uuid": "^8.3.2" diff --git a/packages/earn-controller/package.json b/packages/earn-controller/package.json index 248681b05b..6d2d438c8a 100644 --- a/packages/earn-controller/package.json +++ b/packages/earn-controller/package.json @@ -66,7 +66,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^6.1.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "deepmerge": "^4.2.2", diff --git a/packages/eip-5792-middleware/CHANGELOG.md b/packages/eip-5792-middleware/CHANGELOG.md index 1763c034f0..2e816c2883 100644 --- a/packages/eip-5792-middleware/CHANGELOG.md +++ b/packages/eip-5792-middleware/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) -- Bump `@metamask/transaction-controller` from `^65.4.0` to `^68.0.1` ([#8848](https://github.com/MetaMask/core/pull/8848), [#8999](https://github.com/MetaMask/core/pull/8999), [#9021](https://github.com/MetaMask/core/pull/9021), [#9027](https://github.com/MetaMask/core/pull/9027), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^65.4.0` to `^68.1.0` ([#8848](https://github.com/MetaMask/core/pull/8848), [#8999](https://github.com/MetaMask/core/pull/8999), [#9021](https://github.com/MetaMask/core/pull/9021), [#9027](https://github.com/MetaMask/core/pull/9027), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) ## [3.0.4] diff --git a/packages/eip-5792-middleware/package.json b/packages/eip-5792-middleware/package.json index 35e61ed09b..0c78b76cf5 100644 --- a/packages/eip-5792-middleware/package.json +++ b/packages/eip-5792-middleware/package.json @@ -53,7 +53,7 @@ "dependencies": { "@metamask/messenger": "^1.2.0", "@metamask/superstruct": "^3.1.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "lodash": "^4.17.21", "uuid": "^8.3.2" diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index c1e8a4ace8..80f4d00659 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) -- Bump `@metamask/transaction-controller` from `^66.0.0` to `^68.0.1` ([#8999](https://github.com/MetaMask/core/pull/8999), [#9021](https://github.com/MetaMask/core/pull/9021), [#9027](https://github.com/MetaMask/core/pull/9027), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^66.0.0` to `^68.1.0` ([#8999](https://github.com/MetaMask/core/pull/8999), [#9021](https://github.com/MetaMask/core/pull/9021), [#9027](https://github.com/MetaMask/core/pull/9027), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) ## [4.2.0] diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index 01ee1d383c..30c7fe974d 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -63,7 +63,7 @@ "@metamask/snaps-controllers": "^19.0.0", "@metamask/snaps-sdk": "^11.0.0", "@metamask/snaps-utils": "^12.1.2", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0" }, "devDependencies": { diff --git a/packages/network-enablement-controller/CHANGELOG.md b/packages/network-enablement-controller/CHANGELOG.md index 990aec509b..8f9ad2a3f5 100644 --- a/packages/network-enablement-controller/CHANGELOG.md +++ b/packages/network-enablement-controller/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/controller-utils` from `^12.1.0` to `^12.2.0` ([#9058](https://github.com/MetaMask/core/pull/9058), [#9083](https://github.com/MetaMask/core/pull/9083)) -- Bump `@metamask/transaction-controller` from `^66.0.1` to `^68.0.1` ([#9021](https://github.com/MetaMask/core/pull/9021), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^66.0.1` to `^68.1.0` ([#9021](https://github.com/MetaMask/core/pull/9021), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) ## [5.3.0] diff --git a/packages/network-enablement-controller/package.json b/packages/network-enablement-controller/package.json index 128085f2a2..5dccdd0de0 100644 --- a/packages/network-enablement-controller/package.json +++ b/packages/network-enablement-controller/package.json @@ -60,7 +60,7 @@ "@metamask/multichain-network-controller": "^3.1.3", "@metamask/network-controller": "^32.0.0", "@metamask/slip44": "^4.3.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "reselect": "^5.1.1" }, diff --git a/packages/perps-controller/package.json b/packages/perps-controller/package.json index c94b8fdc2b..84b70d47f2 100644 --- a/packages/perps-controller/package.json +++ b/packages/perps-controller/package.json @@ -115,7 +115,7 @@ "@metamask/network-controller": "^32.0.0", "@metamask/profile-sync-controller": "^28.2.0", "@metamask/remote-feature-flag-controller": "^4.2.2", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "@types/uuid": "^8.3.0", diff --git a/packages/phishing-controller/CHANGELOG.md b/packages/phishing-controller/CHANGELOG.md index 76d147adf6..a9e131dcd0 100644 --- a/packages/phishing-controller/CHANGELOG.md +++ b/packages/phishing-controller/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/controller-utils` from `^12.1.0` to `^12.2.0` ([#9058](https://github.com/MetaMask/core/pull/9058), [#9083](https://github.com/MetaMask/core/pull/9083)) -- Bump `@metamask/transaction-controller` from `^65.4.0` to `^68.0.1` ([#8848](https://github.com/MetaMask/core/pull/8848), [#8999](https://github.com/MetaMask/core/pull/8999), [#9021](https://github.com/MetaMask/core/pull/9021), [#9027](https://github.com/MetaMask/core/pull/9027), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^65.4.0` to `^68.1.0` ([#8848](https://github.com/MetaMask/core/pull/8848), [#8999](https://github.com/MetaMask/core/pull/8999), [#9021](https://github.com/MetaMask/core/pull/9021), [#9027](https://github.com/MetaMask/core/pull/9027), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) ## [17.2.0] diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json index 296f250f80..ae11b37089 100644 --- a/packages/phishing-controller/package.json +++ b/packages/phishing-controller/package.json @@ -57,7 +57,7 @@ "@metamask/base-controller": "^9.1.0", "@metamask/controller-utils": "^12.2.0", "@metamask/messenger": "^1.2.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@noble/hashes": "^1.8.0", "@types/punycode": "^2.1.0", "ethereum-cryptography": "^2.1.2", diff --git a/packages/profile-metrics-controller/CHANGELOG.md b/packages/profile-metrics-controller/CHANGELOG.md index 77b3fe6415..a09825939b 100644 --- a/packages/profile-metrics-controller/CHANGELOG.md +++ b/packages/profile-metrics-controller/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/controller-utils` from `^12.1.1` to `^12.2.0` ([#9083](https://github.com/MetaMask/core/pull/9083)) -- Bump `@metamask/transaction-controller` from `^67.0.0` to `^68.0.1` ([#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^67.0.0` to `^68.1.0` ([#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) - Bump `@metamask/profile-sync-controller` from `^28.1.1` to `^28.2.0` ([#9119](https://github.com/MetaMask/core/pull/9119)) - Bump `@metamask/keyring-controller` from `^27.0.0` to `^27.1.0` ([#9129](https://github.com/MetaMask/core/pull/9129)) diff --git a/packages/profile-metrics-controller/package.json b/packages/profile-metrics-controller/package.json index 30489e1219..13314bd255 100644 --- a/packages/profile-metrics-controller/package.json +++ b/packages/profile-metrics-controller/package.json @@ -64,7 +64,7 @@ "@metamask/snaps-sdk": "^11.0.0", "@metamask/snaps-utils": "^12.1.2", "@metamask/superstruct": "^3.1.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "async-mutex": "^0.5.0", "uuid": "^8.3.2" diff --git a/packages/shield-controller/CHANGELOG.md b/packages/shield-controller/CHANGELOG.md index ad606b1372..723a7c4c9c 100644 --- a/packages/shield-controller/CHANGELOG.md +++ b/packages/shield-controller/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.2.0` ([#8774](https://github.com/MetaMask/core/pull/8774), [#9058](https://github.com/MetaMask/core/pull/9058), [#9083](https://github.com/MetaMask/core/pull/9083)) - Bump `@metamask/signature-controller` from `^39.2.1` to `^39.2.5` ([#8774](https://github.com/MetaMask/core/pull/8774), [#8912](https://github.com/MetaMask/core/pull/8912), [#8999](https://github.com/MetaMask/core/pull/8999), [#9058](https://github.com/MetaMask/core/pull/9058)) -- Bump `@metamask/transaction-controller` from `^65.3.0` to `^68.0.1` ([#8796](https://github.com/MetaMask/core/pull/8796), [#8848](https://github.com/MetaMask/core/pull/8848), [#8999](https://github.com/MetaMask/core/pull/8999), [#9021](https://github.com/MetaMask/core/pull/9021), [#9027](https://github.com/MetaMask/core/pull/9027), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^65.3.0` to `^68.1.0` ([#8796](https://github.com/MetaMask/core/pull/8796), [#8848](https://github.com/MetaMask/core/pull/8848), [#8999](https://github.com/MetaMask/core/pull/8999), [#9021](https://github.com/MetaMask/core/pull/9021), [#9027](https://github.com/MetaMask/core/pull/9027), [#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) ## [5.1.2] diff --git a/packages/shield-controller/package.json b/packages/shield-controller/package.json index 9a141efa65..9c26010269 100644 --- a/packages/shield-controller/package.json +++ b/packages/shield-controller/package.json @@ -57,7 +57,7 @@ "@metamask/controller-utils": "^12.2.0", "@metamask/messenger": "^1.2.0", "@metamask/signature-controller": "^39.2.5", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "cockatiel": "^3.1.2" }, diff --git a/packages/smart-transactions-controller/CHANGELOG.md b/packages/smart-transactions-controller/CHANGELOG.md index 09dca8d9b3..5bd3551e5c 100644 --- a/packages/smart-transactions-controller/CHANGELOG.md +++ b/packages/smart-transactions-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/transaction-controller` from `^68.0.1` to `^68.1.0` ([#9203](https://github.com/MetaMask/core/pull/9203)) + ## [24.2.2] ### Changed diff --git a/packages/smart-transactions-controller/package.json b/packages/smart-transactions-controller/package.json index af1773cade..95d8635057 100644 --- a/packages/smart-transactions-controller/package.json +++ b/packages/smart-transactions-controller/package.json @@ -67,7 +67,7 @@ "@metamask/profile-sync-controller": "^28.2.0", "@metamask/remote-feature-flag-controller": "^4.2.2", "@metamask/superstruct": "^3.1.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "bignumber.js": "^9.1.2", "lodash": "^4.17.21", diff --git a/packages/subscription-controller/CHANGELOG.md b/packages/subscription-controller/CHANGELOG.md index bda89802b8..bad6572504 100644 --- a/packages/subscription-controller/CHANGELOG.md +++ b/packages/subscription-controller/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/profile-sync-controller` from `^28.1.1` to `^28.2.0` ([#9119](https://github.com/MetaMask/core/pull/9119)) -- Bump `@metamask/transaction-controller` from `^68.0.0` to `^68.0.1` ([#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^68.0.0` to `^68.1.0` ([#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) ## [6.2.0] diff --git a/packages/subscription-controller/package.json b/packages/subscription-controller/package.json index 4e7daee9ee..ef3dba00e7 100644 --- a/packages/subscription-controller/package.json +++ b/packages/subscription-controller/package.json @@ -58,7 +58,7 @@ "@metamask/messenger": "^1.2.0", "@metamask/polling-controller": "^16.0.6", "@metamask/profile-sync-controller": "^28.2.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "bignumber.js": "^9.1.2" }, diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 4a653b0df5..0b1a60cde3 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [68.1.0] + ### Changed - Add RPC method, chain ID, and endpoint type context to transaction provider errors, including raw transaction submission failures ([#9144](https://github.com/MetaMask/core/pull/9144)) @@ -2510,7 +2512,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@68.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@68.1.0...HEAD +[68.1.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@68.0.1...@metamask/transaction-controller@68.1.0 [68.0.1]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@68.0.0...@metamask/transaction-controller@68.0.1 [68.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@67.1.0...@metamask/transaction-controller@68.0.0 [67.1.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@67.0.0...@metamask/transaction-controller@67.1.0 diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index fc1e51b511..8be90e4000 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/transaction-controller", - "version": "68.0.1", + "version": "68.1.0", "description": "Stores transactions alongside their periodically updated statuses and manages interactions such as approval and cancellation", "keywords": [ "Ethereum", diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index d0dc9a9c2a..5454b81a38 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,11 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [23.13.0] + ### Changed - Add RPC method, chain ID, and endpoint type context to transaction pay provider errors ([#9144](https://github.com/MetaMask/core/pull/9144)) - Bump `@metamask/ramps-controller` from `^14.2.0` to `^14.3.0` ([#9199](https://github.com/MetaMask/core/pull/9199)) - Bump `@metamask/assets-controllers` from `^109.1.0` to `^109.2.0` ([#9202](https://github.com/MetaMask/core/pull/9202)) +- Bump `@metamask/transaction-controller` from `^68.0.1` to `^68.1.0` ([#9203](https://github.com/MetaMask/core/pull/9203)) ### Fixed @@ -1127,7 +1130,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@23.12.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@23.13.0...HEAD +[23.13.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@23.12.0...@metamask/transaction-pay-controller@23.13.0 [23.12.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@23.11.0...@metamask/transaction-pay-controller@23.12.0 [23.11.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@23.10.0...@metamask/transaction-pay-controller@23.11.0 [23.10.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@23.9.0...@metamask/transaction-pay-controller@23.10.0 diff --git a/packages/transaction-pay-controller/package.json b/packages/transaction-pay-controller/package.json index f0bf80c771..604853019f 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": "23.12.0", + "version": "23.13.0", "description": "Manages alternate payment strategies to provide required funds for transactions in MetaMask", "keywords": [ "Ethereum", @@ -70,7 +70,7 @@ "@metamask/network-controller": "^32.0.0", "@metamask/ramps-controller": "^14.3.0", "@metamask/remote-feature-flag-controller": "^4.2.2", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "bignumber.js": "^9.1.2", "bn.js": "^5.2.1", diff --git a/packages/user-operation-controller/CHANGELOG.md b/packages/user-operation-controller/CHANGELOG.md index 866543d707..a180dc5af9 100644 --- a/packages/user-operation-controller/CHANGELOG.md +++ b/packages/user-operation-controller/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/controller-utils` from `^12.1.1` to `^12.2.0` ([#9083](https://github.com/MetaMask/core/pull/9083)) -- Bump `@metamask/transaction-controller` from `^67.0.0` to `^68.0.1` ([#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177)) +- Bump `@metamask/transaction-controller` from `^67.0.0` to `^68.1.0` ([#9066](https://github.com/MetaMask/core/pull/9066), [#9089](https://github.com/MetaMask/core/pull/9089), [#9177](https://github.com/MetaMask/core/pull/9177), [#9203](https://github.com/MetaMask/core/pull/9203)) - Bump `@metamask/keyring-controller` from `^27.0.0` to `^27.1.0` ([#9129](https://github.com/MetaMask/core/pull/9129)) ## [41.2.4] diff --git a/packages/user-operation-controller/package.json b/packages/user-operation-controller/package.json index 981299f49a..15d0ae0847 100644 --- a/packages/user-operation-controller/package.json +++ b/packages/user-operation-controller/package.json @@ -65,7 +65,7 @@ "@metamask/polling-controller": "^16.0.6", "@metamask/rpc-errors": "^7.0.2", "@metamask/superstruct": "^3.1.0", - "@metamask/transaction-controller": "^68.0.1", + "@metamask/transaction-controller": "^68.1.0", "@metamask/utils": "^11.11.0", "bn.js": "^5.2.1", "immer": "^9.0.6", diff --git a/yarn.lock b/yarn.lock index 1df6fe25f7..cbcb4ad276 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5669,7 +5669,7 @@ __metadata: "@metamask/preferences-controller": "npm:^23.1.0" "@metamask/snaps-controllers": "npm:^19.0.0" "@metamask/snaps-utils": "npm:^12.1.2" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -5731,7 +5731,7 @@ __metadata: "@metamask/snaps-sdk": "npm:^11.0.0" "@metamask/snaps-utils": "npm:^12.1.2" "@metamask/storage-service": "npm:^1.0.2" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@tanstack/query-core": "npm:^5.62.16" "@ts-bridge/cli": "npm:^0.6.4" @@ -5914,7 +5914,7 @@ __metadata: "@metamask/remote-feature-flag-controller": "npm:^4.2.2" "@metamask/snaps-controllers": "npm:^19.0.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -5951,7 +5951,7 @@ __metadata: "@metamask/profile-sync-controller": "npm:^28.2.0" "@metamask/snaps-controllers": "npm:^19.0.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -6408,7 +6408,7 @@ __metadata: "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^32.0.0" "@metamask/stake-sdk": "npm:^3.2.1" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" @@ -6431,7 +6431,7 @@ __metadata: "@metamask/messenger": "npm:^1.2.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -7008,7 +7008,7 @@ __metadata: "@metamask/snaps-controllers": "npm:^19.0.0" "@metamask/snaps-sdk": "npm:^11.0.0" "@metamask/snaps-utils": "npm:^12.1.2" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -7658,7 +7658,7 @@ __metadata: "@metamask/multichain-network-controller": "npm:^3.1.3" "@metamask/network-controller": "npm:^32.0.0" "@metamask/slip44": "npm:^4.3.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -7855,7 +7855,7 @@ __metadata: "@metamask/network-controller": "npm:^32.0.0" "@metamask/profile-sync-controller": "npm:^28.2.0" "@metamask/remote-feature-flag-controller": "npm:^4.2.2" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@myx-trade/sdk": "npm:^0.1.265" "@nktkas/hyperliquid": "npm:^0.32.2" @@ -7887,7 +7887,7 @@ __metadata: "@metamask/base-controller": "npm:^9.1.0" "@metamask/controller-utils": "npm:^12.2.0" "@metamask/messenger": "npm:^1.2.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@noble/hashes": "npm:^1.8.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -7976,7 +7976,7 @@ __metadata: "@metamask/snaps-sdk": "npm:^11.0.0" "@metamask/snaps-utils": "npm:^12.1.2" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -8274,7 +8274,7 @@ __metadata: "@metamask/controller-utils": "npm:^12.2.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/signature-controller": "npm:^39.2.5" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -8358,7 +8358,7 @@ __metadata: "@metamask/profile-sync-controller": "npm:^28.2.0" "@metamask/remote-feature-flag-controller": "npm:^4.2.2" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" bignumber.js: "npm:^9.1.2" @@ -8580,7 +8580,7 @@ __metadata: "@metamask/messenger": "npm:^1.2.0" "@metamask/polling-controller": "npm:^16.0.6" "@metamask/profile-sync-controller": "npm:^28.2.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -8627,7 +8627,7 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^68.0.1, @metamask/transaction-controller@workspace:packages/transaction-controller": +"@metamask/transaction-controller@npm:^68.1.0, @metamask/transaction-controller@workspace:packages/transaction-controller": version: 0.0.0-use.local resolution: "@metamask/transaction-controller@workspace:packages/transaction-controller" dependencies: @@ -8705,7 +8705,7 @@ __metadata: "@metamask/network-controller": "npm:^32.0.0" "@metamask/ramps-controller": "npm:^14.3.0" "@metamask/remote-feature-flag-controller": "npm:^4.2.2" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -8740,7 +8740,7 @@ __metadata: "@metamask/polling-controller": "npm:^16.0.6" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^68.0.1" + "@metamask/transaction-controller": "npm:^68.1.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" From d562139595d96b536b8aaa4b5e5ecc64c18a0c46 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 19 Jun 2026 14:47:46 +0100 Subject: [PATCH 7/7] feat: expose more info about chomp upgrade step failures (#9204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Explanation When the upgrade process throws an error, include the step where the error occurred in what is thrown. This will allow client applications to report errors more effectively. ## References ## 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 --- > [!NOTE] > **Low Risk** > Only changes error propagation on the failure path; upgrade sequencing and success behavior are unchanged. > > **Overview** > **`upgradeAccount`** now catches failures from any upgrade step and re-throws them as **`MoneyAccountUpgradeStepError`**, with **`step`** set to that step’s `name` and the original value kept on **`cause`**. The public message still includes the underlying text (from `Error.message` or `String(cause)`). > > The package adds **`MoneyAccountUpgradeStepError`**, **`isMoneyAccountUpgradeStepError`** (structural guard for bundled apps), unit/integration tests, changelog notes, and updated JSDoc for the messenger action type. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d66b594240adb21d2e573a27117bda292a3738f0. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .../CHANGELOG.md | 4 + ...ntUpgradeController-method-action-types.ts | 4 +- .../src/MoneyAccountUpgradeController.test.ts | 75 ++++++++++++++++++- .../src/MoneyAccountUpgradeController.ts | 19 +++-- .../src/errors.test.ts | 65 ++++++++++++++++ .../src/errors.ts | 45 +++++++++++ .../src/index.ts | 4 + 7 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 packages/money-account-upgrade-controller/src/errors.test.ts create mode 100644 packages/money-account-upgrade-controller/src/errors.ts diff --git a/packages/money-account-upgrade-controller/CHANGELOG.md b/packages/money-account-upgrade-controller/CHANGELOG.md index 1fe0c0ece4..bb62faae04 100644 --- a/packages/money-account-upgrade-controller/CHANGELOG.md +++ b/packages/money-account-upgrade-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `MoneyAccountUpgradeStepError` (and the `isMoneyAccountUpgradeStepError` type guard). `upgradeAccount` now wraps any error thrown by a step in this error, exposing the failing step's `name` as `step` and preserving the original error as `cause`, so consumers can attribute failures to a specific step when reporting to Sentry). ([#9204](https://github.com/MetaMask/core/pull/9204)) + ### Changed - Bump `@metamask/keyring-controller` from `^27.0.0` to `^27.1.0` ([#9129](https://github.com/MetaMask/core/pull/9129)) diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts index ab45da1779..23d5c72362 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts @@ -9,7 +9,9 @@ import type { MoneyAccountUpgradeController } from './MoneyAccountUpgradeControl * Runs each step in the upgrade sequence in order. A step that reports * `'already-done'` is skipped without performing any action; a step that * reports `'completed'` has performed its action. An error thrown by any - * step propagates and halts the sequence. + * step halts the sequence and is re-thrown wrapped in a + * {@link MoneyAccountUpgradeStepError} that records which step failed (the + * original error is preserved as `cause`). * * @param address - The Money Account address to upgrade. */ diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index 6ce78bc3c6..b617238242 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -8,8 +8,14 @@ import type { import { hexToNumber } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; -import type { MoneyAccountUpgradeControllerMessenger } from '.'; -import { MoneyAccountUpgradeController } from '.'; +import type { + MoneyAccountUpgradeControllerMessenger, + MoneyAccountUpgradeStepError, +} from '.'; +import { + MoneyAccountUpgradeController, + isMoneyAccountUpgradeStepError, +} from '.'; const MOCK_CHAIN_ID = '0x1' as Hex; // mainnet, supported in delegation-deployments@1.3.0 const UNSUPPORTED_CHAIN_ID = '0x539' as Hex; // 1337 — local dev, not in registry @@ -438,5 +444,70 @@ describe('MoneyAccountUpgradeController', () => { controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS), ).rejects.toThrow('signing failed'); }); + + it('wraps a step failure in a MoneyAccountUpgradeStepError that records the step and cause', async () => { + const { controller, mocks } = setup(); + await controller.init({ + chainId: MOCK_CHAIN_ID, + boringVaultAddress: MOCK_BORING_VAULT_ADDRESS, + }); + const cause = new Error('signing failed'); + // The associate-address step (first in the sequence) signs a personal + // message before calling CHOMP, so failing this surfaces that step. + mocks.signPersonalMessage.mockRejectedValue(cause); + + const error = await controller + .upgradeAccount(MOCK_ACCOUNT_ADDRESS) + .catch((thrown: unknown) => thrown); + + expect(isMoneyAccountUpgradeStepError(error)).toBe(true); + expect(error).toMatchObject({ + step: 'associate-address', + cause, + }); + expect((error as MoneyAccountUpgradeStepError).message).toBe( + 'Money Account upgrade failed at step "associate-address": signing failed', + ); + }); + + it('records the name of the specific step that failed', async () => { + const { controller, mocks } = setup(); + await controller.init({ + chainId: MOCK_CHAIN_ID, + boringVaultAddress: MOCK_BORING_VAULT_ADDRESS, + }); + // The first step (associate-address) passes; fail at the second step + // (eip-7702-authorization), which signs the authorization. + mocks.signEip7702Authorization.mockRejectedValue( + new Error('authorization rejected'), + ); + + const error = await controller + .upgradeAccount(MOCK_ACCOUNT_ADDRESS) + .catch((thrown: unknown) => thrown); + + expect(error).toMatchObject({ step: 'eip-7702-authorization' }); + }); + + it('wraps a non-Error thrown by a step, stringifying it as the cause message', async () => { + const { controller, mocks } = setup(); + await controller.init({ + chainId: MOCK_CHAIN_ID, + boringVaultAddress: MOCK_BORING_VAULT_ADDRESS, + }); + mocks.signPersonalMessage.mockRejectedValue('plain string failure'); + + const error = await controller + .upgradeAccount(MOCK_ACCOUNT_ADDRESS) + .catch((thrown: unknown) => thrown); + + expect(error).toMatchObject({ + step: 'associate-address', + cause: 'plain string failure', + }); + expect((error as MoneyAccountUpgradeStepError).message).toBe( + 'Money Account upgrade failed at step "associate-address": plain string failure', + ); + }); }); }); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index 574a78f400..7d0556e5ec 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -30,6 +30,7 @@ import type { import { hexToNumber } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; +import { MoneyAccountUpgradeStepError } from './errors'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; import { associateAddressStep } from './steps/associate-address'; import { buildDelegationStep } from './steps/build-delegations'; @@ -203,7 +204,9 @@ export class MoneyAccountUpgradeController extends BaseController< * Runs each step in the upgrade sequence in order. A step that reports * `'already-done'` is skipped without performing any action; a step that * reports `'completed'` has performed its action. An error thrown by any - * step propagates and halts the sequence. + * step halts the sequence and is re-thrown wrapped in a + * {@link MoneyAccountUpgradeStepError} that records which step failed (the + * original error is preserved as `cause`). * * @param address - The Money Account address to upgrade. */ @@ -215,11 +218,15 @@ export class MoneyAccountUpgradeController extends BaseController< } for (const step of this.#steps) { - await step.run({ - messenger: this.messenger, - address, - ...this.#config, - }); + try { + await step.run({ + messenger: this.messenger, + address, + ...this.#config, + }); + } catch (error) { + throw new MoneyAccountUpgradeStepError(step.name, error); + } } } } diff --git a/packages/money-account-upgrade-controller/src/errors.test.ts b/packages/money-account-upgrade-controller/src/errors.test.ts new file mode 100644 index 0000000000..53b3f1a885 --- /dev/null +++ b/packages/money-account-upgrade-controller/src/errors.test.ts @@ -0,0 +1,65 @@ +import { + MoneyAccountUpgradeStepError, + isMoneyAccountUpgradeStepError, +} from './errors'; + +describe('MoneyAccountUpgradeStepError', () => { + it('records the step name and preserves an Error cause', () => { + const cause = new Error('boom'); + + const error = new MoneyAccountUpgradeStepError('build-delegation', cause); + + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe('MoneyAccountUpgradeStepError'); + expect(error.step).toBe('build-delegation'); + expect(error.cause).toBe(cause); + expect(error.message).toBe( + 'Money Account upgrade failed at step "build-delegation": boom', + ); + }); + + it('stringifies a non-Error cause in the message', () => { + const error = new MoneyAccountUpgradeStepError('register-intents', 42); + + expect(error.cause).toBe(42); + expect(error.message).toBe( + 'Money Account upgrade failed at step "register-intents": 42', + ); + }); +}); + +describe('isMoneyAccountUpgradeStepError', () => { + it('returns true for a MoneyAccountUpgradeStepError', () => { + expect( + isMoneyAccountUpgradeStepError( + new MoneyAccountUpgradeStepError('associate-address', new Error('x')), + ), + ).toBe(true); + }); + + it('returns true for a structurally-equivalent error from another realm', () => { + const lookalike = new Error('whatever'); + lookalike.name = 'MoneyAccountUpgradeStepError'; + (lookalike as unknown as { step: string }).step = 'associate-address'; + + expect(isMoneyAccountUpgradeStepError(lookalike)).toBe(true); + }); + + it('returns false for a plain Error', () => { + expect(isMoneyAccountUpgradeStepError(new Error('nope'))).toBe(false); + }); + + it('returns false for an error with the right name but no step', () => { + const error = new Error('nope'); + error.name = 'MoneyAccountUpgradeStepError'; + + expect(isMoneyAccountUpgradeStepError(error)).toBe(false); + }); + + it('returns false for non-error values', () => { + expect(isMoneyAccountUpgradeStepError(undefined)).toBe(false); + expect(isMoneyAccountUpgradeStepError(null)).toBe(false); + expect(isMoneyAccountUpgradeStepError('error')).toBe(false); + expect(isMoneyAccountUpgradeStepError({ step: 'x' })).toBe(false); + }); +}); diff --git a/packages/money-account-upgrade-controller/src/errors.ts b/packages/money-account-upgrade-controller/src/errors.ts new file mode 100644 index 0000000000..0fc386d60c --- /dev/null +++ b/packages/money-account-upgrade-controller/src/errors.ts @@ -0,0 +1,45 @@ +/** + * Error thrown by `MoneyAccountUpgradeController.upgradeAccount` when one of + * the upgrade steps fails. + * + * Wraps the underlying error (preserved as `cause`) and records the name of + * the step that was running, so consumers can attribute the failure to a + * specific step — e.g. when tagging an error report — without parsing the + * message. + */ +export class MoneyAccountUpgradeStepError extends Error { + /** The name of the step that threw. */ + readonly step: string; + + /** The underlying error thrown by the step. */ + readonly cause: unknown; + + constructor(step: string, cause: unknown) { + const causeMessage = cause instanceof Error ? cause.message : String(cause); + super(`Money Account upgrade failed at step "${step}": ${causeMessage}`); + + this.name = 'MoneyAccountUpgradeStepError'; + this.step = step; + this.cause = cause; + } +} + +/** + * Type guard for {@link MoneyAccountUpgradeStepError}. + * + * Uses a structural check rather than `instanceof` so it holds across module + * realm boundaries — e.g. when the controller is consumed from a bundled host + * app where a duplicate copy of this class may exist. + * + * @param error - The value to test. + * @returns Whether `error` is a `MoneyAccountUpgradeStepError`. + */ +export function isMoneyAccountUpgradeStepError( + error: unknown, +): error is MoneyAccountUpgradeStepError { + return ( + error instanceof Error && + error.name === 'MoneyAccountUpgradeStepError' && + typeof (error as { step?: unknown }).step === 'string' + ); +} diff --git a/packages/money-account-upgrade-controller/src/index.ts b/packages/money-account-upgrade-controller/src/index.ts index 26d32b8711..784e62cfd5 100644 --- a/packages/money-account-upgrade-controller/src/index.ts +++ b/packages/money-account-upgrade-controller/src/index.ts @@ -1,4 +1,8 @@ export type { UpgradeConfig } from './types'; +export { + MoneyAccountUpgradeStepError, + isMoneyAccountUpgradeStepError, +} from './errors'; export { MoneyAccountUpgradeController } from './MoneyAccountUpgradeController'; export type { MoneyAccountUpgradeControllerState,