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/package.json b/package.json index 350be0b05a..321546c6d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "1055.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 fe3376bd7f..10c4f7edb6 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -9,9 +9,9 @@ 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/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 69e535aa3a..36f5bddb52 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", @@ -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 edef6dc8c5..137dd64c3a 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,8 +7,16 @@ 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 +- 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. @@ -3226,7 +3234,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..466ce30706 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", @@ -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/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 = {}; }); diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 4f1c18a1cd..eabf76d55f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -13,12 +13,12 @@ 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)) - 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 2089624e90..c3ace53127 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", @@ -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/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, 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 24443eb4cb..0b1a60cde3 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,12 @@ 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)) + ## [68.0.1] ### Fixed @@ -2506,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-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 0a26f749b7..5454b81a38 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,9 +7,18 @@ 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 + +- 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] @@ -1121,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 84f04b95ee..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", @@ -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", @@ -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/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)}`); +} 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'); }); }); 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/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", diff --git a/yarn.lock b/yarn.lock index f68fd7e449..cbcb4ad276 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" @@ -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" @@ -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: @@ -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" @@ -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" @@ -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: @@ -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" @@ -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"