diff --git a/.github/workflows/add-prs-to-project.yml b/.github/workflows/add-prs-to-project.yml new file mode 100644 index 0000000000..f930f9a370 --- /dev/null +++ b/.github/workflows/add-prs-to-project.yml @@ -0,0 +1,24 @@ +name: 'Add PR to Project Board - Wallet Framework Team' + +on: + workflow_dispatch: + pull_request: + types: [opened, labeled, review_requested] + +jobs: + add-to-project: + name: Add PR to Project Board + runs-on: ubuntu-latest + env: + TEAM_NAME: 'wallet-framework-engineers' + TEAM_LABEL: 'team-wallet-framework' + + steps: + - name: Add PR to project board + uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e + if: | + github.event.requested_team.name == env.TEAM_NAME || + contains(github.event.pull_request.labels.*.name, env.TEAM_LABEL) + with: + project-url: https://github.com/orgs/MetaMask/projects/113 + github-token: ${{ secrets.CORE_ADD_PRS_TO_PROJECT }} diff --git a/docs/package-migration-process-guide.md b/docs/package-migration-process-guide.md index a7fc711c80..cdaf15c997 100644 --- a/docs/package-migration-process-guide.md +++ b/docs/package-migration-process-guide.md @@ -12,15 +12,9 @@ This document outlines the process for migrating a MetaMask library into the cor - [Example PR](https://github.com/MetaMask/eth-json-rpc-provider/pull/38) -### 2. Disable `dependabot` dependency version updates and security alerts +### 2. Add the source repo to the ZenHub workspace repo filter so that its issues/PRs show up on the board -- [Disable dependabot alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts/configuring-dependabot-alerts#enabling-or-disabling-dependabot-alerts-for-a-repository) for the repo. -- [Disable dependabot dependency version updates](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates#disabling-dependabot-version-updates) from settings, or delete the source repo's `.github/dependabot.yml` file. -- Contact a [**maintainer**](https://github.com/orgs/MetaMask/teams/engineering?query=role%3Amaintainer) to perform this step. - -### 3. Add the source repo to the ZenHub workspace repo filter so that its issues/PRs show up on the board - -### **[PR#2]** 4. Align dependency versions and TypeScript, ESLint, Prettier configurations with the core monorepo +### **[PR#2]** 3. Align dependency versions and TypeScript, ESLint, Prettier configurations with the core monorepo - If the dependency versions of the migration target are ahead of core, consider updating the core dependencies first. - Apply the configurations of the core monorepo to the source repo files. @@ -28,17 +22,17 @@ This document outlines the process for migrating a MetaMask library into the cor - Resolve any errors or issues resulting from these changes. - [Example PR](https://github.com/MetaMask/eth-json-rpc-provider/pull/28) -### **[PR#3]** 5. Review the `metamask-module-template`, and add any missing files or elements (e.g. LICENSE) +### **[PR#3]** 4. Review the `metamask-module-template`, and add any missing files or elements (e.g. LICENSE) - [Example PR](https://github.com/MetaMask/eth-json-rpc-provider/pull/24) -### **[PR#4]** 6. Rename the migration target package so that it is prepended by the `@metamask/` namespace (skip if not applicable) +### **[PR#4]** 5. Rename the migration target package so that it is prepended by the `@metamask/` namespace (skip if not applicable) - Modify the "name" field in `package.json`. - Update the title of the README.md. - Add a CHANGELOG entry for the rename. -### **[PR#5]** 7. Create a new release of the migration target from the source repo +### **[PR#5]** 6. Create a new release of the migration target from the source repo - All subsequent releases of the migration target will be made from the core monorepo. - [Example PR](https://github.com/MetaMask/eth-json-rpc-provider/pull/29) diff --git a/examples/example-controllers/package.json b/examples/example-controllers/package.json index 0f9f2f0d26..e3a596bf51 100644 --- a/examples/example-controllers/package.json +++ b/examples/example-controllers/package.json @@ -52,7 +52,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/package.json b/package.json index 20d2bf7657..0779428d8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "239.0.0", + "version": "245.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index f617adb7cc..ce886e69d2 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [19.0.0] + +### Changed + +- **BREAKING:** Bump peer dependency `@metamask/keyring-controller` from `^17.0.0` to `^18.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) + ## [18.2.3] ### Changed @@ -337,7 +343,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#1637](https://github.com/MetaMask/core/pull/1637)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@18.2.3...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@19.0.0...HEAD +[19.0.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@18.2.3...@metamask/accounts-controller@19.0.0 [18.2.3]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@18.2.2...@metamask/accounts-controller@18.2.3 [18.2.2]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@18.2.1...@metamask/accounts-controller@18.2.2 [18.2.1]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@18.2.0...@metamask/accounts-controller@18.2.1 diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 8732b1f425..ceaa7e999c 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/accounts-controller", - "version": "18.2.3", + "version": "19.0.0", "description": "Manages internal accounts", "keywords": [ "MetaMask", @@ -61,7 +61,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^17.3.1", + "@metamask/keyring-controller": "^18.0.0", "@metamask/snaps-controllers": "^9.7.0", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", @@ -72,7 +72,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/keyring-controller": "^17.0.0", + "@metamask/keyring-controller": "^18.0.0", "@metamask/snaps-controllers": "^9.7.0" }, "engines": { diff --git a/packages/address-book-controller/package.json b/packages/address-book-controller/package.json index bd8f3b37bb..e8ec8d94af 100644 --- a/packages/address-book-controller/package.json +++ b/packages/address-book-controller/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/utils": "^10.0.0" }, "devDependencies": { diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index e21843a335..923f49ed9b 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [43.0.0] + +### Added + +- `AccountTrackerController` now tracks balances of staked ETH for each account, under the state property `stakedBalance`. ([#4879](https://github.com/MetaMask/core/pull/4879)) + +### Changed + +- **BREAKING**: The polling input for`TokenListController` is now `{chainId: Hex}` instead of `{networkClientId: NetworkClientId}`. ([#4878](https://github.com/MetaMask/core/pull/4878)) +- **BREAKING**: The polling input for`TokenDetectionController` is now `{ chainIds: Hex[]; address: string; }` instead of `{ networkClientId: NetworkClientId; address: string; }`. ([#4894](https://github.com/MetaMask/core/pull/4894)) +- **BREAKING:** Bump `@metamask/keyring-controller` peer dependency from `^17.0.0` to `^18.0.0` ([#4195](https://github.com/MetaMask/core/pull/4195)) +- **BREAKING:** Bump `@metamask/preferences-controller` peer dependency from `^13.2.0` to `^14.0.0` ([#4909](https://github.com/MetaMask/core/pull/4909), [#4915](https://github.com/MetaMask/core/pull/4915)) +- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency from `^18.0.0` to `^19.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) +- Bump `@metamask/controller-utils` from `^11.4.2` to `^11.4.3` ([#4195](https://github.com/MetaMask/core/pull/4195)) + ## [42.0.0] ### Added @@ -1189,7 +1204,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@42.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@43.0.0...HEAD +[43.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@42.0.0...@metamask/assets-controllers@43.0.0 [42.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@41.0.0...@metamask/assets-controllers@42.0.0 [41.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@40.0.0...@metamask/assets-controllers@41.0.0 [40.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@39.0.0...@metamask/assets-controllers@40.0.0 diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index dbf711b72e..61b73e747d 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/assets-controllers", - "version": "42.0.0", + "version": "43.0.0", "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", "keywords": [ "MetaMask", @@ -55,7 +55,7 @@ "@metamask/abi-utils": "^2.0.3", "@metamask/base-controller": "^7.0.2", "@metamask/contract-metadata": "^2.4.0", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/eth-query": "^4.0.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/polling-controller": "^12.0.1", @@ -73,14 +73,14 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/accounts-controller": "^18.2.3", + "@metamask/accounts-controller": "^19.0.0", "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", "@metamask/keyring-api": "^8.1.3", - "@metamask/keyring-controller": "^17.3.1", - "@metamask/network-controller": "^22.0.1", - "@metamask/preferences-controller": "^13.2.0", + "@metamask/keyring-controller": "^18.0.0", + "@metamask/network-controller": "^22.0.2", + "@metamask/preferences-controller": "^14.0.0", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.191", "@types/node": "^16.18.54", @@ -95,11 +95,11 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^18.0.0", + "@metamask/accounts-controller": "^19.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/keyring-controller": "^17.0.0", + "@metamask/keyring-controller": "^18.0.0", "@metamask/network-controller": "^22.0.0", - "@metamask/preferences-controller": "^13.0.0" + "@metamask/preferences-controller": "^14.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index 7413ad9ff2..31c82807bb 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -49,6 +49,7 @@ import { STATIC_MAINNET_TOKEN_LIST, TokenDetectionController, controllerName, + mapChainIdWithTokenListMap, } from './TokenDetectionController'; import { getDefaultTokenListState, @@ -329,6 +330,7 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: defaultSelectedAccount, @@ -363,13 +365,16 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getAccount: selectedAccount, getSelectedAccount: selectedAccount, }, }, + async ({ controller, mockTokenListGetState, callActionSpy }) => { + mockMultiChainAccountsService(); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -404,6 +409,80 @@ describe('TokenDetectionController', () => { ); }); + it('should not call add tokens if balance is not available on account api', async () => { + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ + [sampleTokenA.address]: new BN(1), + }); + + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + await withController( + { + options: { + getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API + }, + mocks: { + getAccount: selectedAccount, + getSelectedAccount: selectedAccount, + }, + }, + + async ({ controller, mockTokenListGetState, callActionSpy }) => { + mockMultiChainAccountsService(); + + const mockAPI = mockMultiChainAccountsService(); + mockAPI.mockFetchMultiChainBalances.mockResolvedValue({ + count: 0, + balances: [ + { + object: 'token', + address: '0xaddress', + name: 'Mock Token', + symbol: 'MOCK', + decimals: 18, + balance: '10.18', + chainId: 2, + }, + ], + unprocessedNetworks: [], + }); + + mockTokenListGetState({ + ...getDefaultTokenListState(), + tokensChainsCache: { + '0x1': { + timestamp: 0, + data: { + test: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: 'test', + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + }, + }, + }); + + await controller.start(); + + expect(callActionSpy).not.toHaveBeenCalledWith( + 'TokensController:addDetectedTokens', + [sampleTokenA], + { + chainId: ChainId.mainnet, + selectedAddress: selectedAccount.address, + }, + ); + }, + ); + }); + it('should detect tokens correctly on the Polygon network', async () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), @@ -415,6 +494,7 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getAccount: selectedAccount, @@ -428,6 +508,7 @@ describe('TokenDetectionController', () => { mockGetNetworkClientById, callActionSpy, }) => { + mockMultiChainAccountsService(); mockNetworkState({ ...getDefaultNetworkControllerState(), selectedNetworkClientId: 'polygon', @@ -494,6 +575,7 @@ describe('TokenDetectionController', () => { }, }, async ({ controller, mockTokenListGetState, callActionSpy }) => { + mockMultiChainAccountsService(); const tokenListState = { ...getDefaultTokenListState(), tokensChainsCache: { @@ -551,6 +633,7 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getAccount: selectedAccount, @@ -563,6 +646,7 @@ describe('TokenDetectionController', () => { mockTokenListGetState, callActionSpy, }) => { + mockMultiChainAccountsService(); mockTokensGetState({ ...getDefaultTokensState(), ignoredTokens: [sampleTokenA.address], @@ -604,12 +688,14 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: defaultSelectedAccount, }, }, async ({ controller, mockTokenListGetState, callActionSpy }) => { + mockMultiChainAccountsService(); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -666,6 +752,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: firstSelectedAccount, @@ -677,6 +764,7 @@ describe('TokenDetectionController', () => { triggerSelectedAccountChange, callActionSpy, }) => { + mockMultiChainAccountsService(); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -725,6 +813,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -735,6 +824,7 @@ describe('TokenDetectionController', () => { triggerSelectedAccountChange, callActionSpy, }) => { + mockMultiChainAccountsService(); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -844,6 +934,7 @@ describe('TokenDetectionController', () => { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: firstSelectedAccount, @@ -914,6 +1005,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: firstSelectedAccount, @@ -926,6 +1018,7 @@ describe('TokenDetectionController', () => { triggerSelectedAccountChange, callActionSpy, }) => { + mockMultiChainAccountsService(); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -978,6 +1071,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -989,6 +1083,7 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange, callActionSpy, }) => { + mockMultiChainAccountsService(); mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), @@ -1049,6 +1144,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: firstSelectedAccount, @@ -1061,6 +1157,7 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange, callActionSpy, }) => { + mockMultiChainAccountsService(); mockGetAccount(firstSelectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), @@ -1422,67 +1519,6 @@ describe('TokenDetectionController', () => { }); describe('when "disabled" is false', () => { - it('should detect new tokens after switching network client id', async () => { - const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ - [sampleTokenA.address]: new BN(1), - }); - const selectedAccount = createMockInternalAccount({ - address: '0x0000000000000000000000000000000000000001', - }); - await withController( - { - options: { - disabled: false, - getBalancesInSingleCall: mockGetBalancesInSingleCall, - }, - mocks: { - getAccount: selectedAccount, - getSelectedAccount: selectedAccount, - }, - }, - async ({ - mockTokenListGetState, - callActionSpy, - triggerNetworkDidChange, - }) => { - mockTokenListGetState({ - ...getDefaultTokenListState(), - tokensChainsCache: { - '0x89': { - timestamp: 0, - data: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, - }, - }, - }, - }, - }); - - triggerNetworkDidChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'polygon', - }); - await advanceTime({ clock, duration: 1 }); - - expect(callActionSpy).toHaveBeenCalledWith( - 'TokensController:addDetectedTokens', - [sampleTokenA], - { - chainId: '0x89', - selectedAddress: selectedAccount.address, - }, - ); - }, - ); - }); - it('should not detect new tokens after switching to a chain that does not support token detection', async () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), @@ -1737,6 +1773,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -1748,6 +1785,7 @@ describe('TokenDetectionController', () => { callActionSpy, triggerTokenListStateChange, }) => { + mockMultiChainAccountsService(); const tokenList = { [sampleTokenA.address]: { name: sampleTokenA.name, @@ -1951,6 +1989,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -1962,6 +2001,7 @@ describe('TokenDetectionController', () => { triggerTokenListStateChange, controller, }) => { + mockMultiChainAccountsService(); const tokenListState = { ...getDefaultTokenListState(), tokensChainsCache: { @@ -2010,6 +2050,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -2021,6 +2062,7 @@ describe('TokenDetectionController', () => { triggerTokenListStateChange, controller, }) => { + mockMultiChainAccountsService(); const tokenListState = { ...getDefaultTokenListState(), tokensChainsCache: { @@ -2087,6 +2129,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -2098,6 +2141,7 @@ describe('TokenDetectionController', () => { triggerTokenListStateChange, controller, }) => { + mockMultiChainAccountsService(); const tokenListState = { ...getDefaultTokenListState(), tokensChainsCache: { @@ -2208,33 +2252,33 @@ describe('TokenDetectionController', () => { }); controller.startPolling({ - networkClientId: 'mainnet', + chainIds: ['0x1'], address: '0x1', }); controller.startPolling({ - networkClientId: 'sepolia', + chainIds: ['0xaa36a7'], address: '0xdeadbeef', }); controller.startPolling({ - networkClientId: 'goerli', + chainIds: ['0x5'], address: '0x3', }); await advanceTime({ clock, duration: 0 }); expect(spy.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet', selectedAddress: '0x1' }], - [{ networkClientId: 'sepolia', selectedAddress: '0xdeadbeef' }], - [{ networkClientId: 'goerli', selectedAddress: '0x3' }], + [{ chainIds: ['0x1'], selectedAddress: '0x1' }], + [{ chainIds: ['0xaa36a7'], selectedAddress: '0xdeadbeef' }], + [{ chainIds: ['0x5'], selectedAddress: '0x3' }], ]); await advanceTime({ clock, duration: DEFAULT_INTERVAL }); expect(spy.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet', selectedAddress: '0x1' }], - [{ networkClientId: 'sepolia', selectedAddress: '0xdeadbeef' }], - [{ networkClientId: 'goerli', selectedAddress: '0x3' }], - [{ networkClientId: 'mainnet', selectedAddress: '0x1' }], - [{ networkClientId: 'sepolia', selectedAddress: '0xdeadbeef' }], - [{ networkClientId: 'goerli', selectedAddress: '0x3' }], + [{ chainIds: ['0x1'], selectedAddress: '0x1' }], + [{ chainIds: ['0xaa36a7'], selectedAddress: '0xdeadbeef' }], + [{ chainIds: ['0x5'], selectedAddress: '0x3' }], + [{ chainIds: ['0x1'], selectedAddress: '0x1' }], + [{ chainIds: ['0xaa36a7'], selectedAddress: '0xdeadbeef' }], + [{ chainIds: ['0x5'], selectedAddress: '0x3' }], ]); }, ); @@ -2254,6 +2298,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -2266,6 +2311,7 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange, callActionSpy, }) => { + mockMultiChainAccountsService(); mockNetworkState({ ...getDefaultNetworkControllerState(), selectedNetworkClientId: NetworkType.goerli, @@ -2275,7 +2321,7 @@ describe('TokenDetectionController', () => { useTokenDetection: false, }); await controller.detectTokens({ - networkClientId: NetworkType.goerli, + chainIds: ['0x5'], selectedAddress: selectedAccount.address, }); expect(callActionSpy).not.toHaveBeenCalledWith( @@ -2314,12 +2360,13 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange, callActionSpy, }) => { + mockMultiChainAccountsService(); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), useTokenDetection: false, }); await controller.detectTokens({ - networkClientId: NetworkType.mainnet, + chainIds: ['0x1'], selectedAddress: selectedAccount.address, }); expect(callActionSpy).toHaveBeenLastCalledWith( @@ -2353,6 +2400,7 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -2360,6 +2408,7 @@ describe('TokenDetectionController', () => { }, }, async ({ controller, mockTokenListGetState, callActionSpy }) => { + mockMultiChainAccountsService(); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -2381,7 +2430,7 @@ describe('TokenDetectionController', () => { }); await controller.detectTokens({ - networkClientId: NetworkType.mainnet, + chainIds: ['0x1'], selectedAddress: selectedAccount.address, }); @@ -2412,6 +2461,7 @@ describe('TokenDetectionController', () => { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, trackMetaMetricsEvent: mockTrackMetaMetricsEvent, + useAccountsAPI: true, // USING ACCOUNTS API }, mocks: { getSelectedAccount: selectedAccount, @@ -2419,6 +2469,7 @@ describe('TokenDetectionController', () => { }, }, async ({ controller, mockTokenListGetState }) => { + mockMultiChainAccountsService(); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -2440,7 +2491,7 @@ describe('TokenDetectionController', () => { }); await controller.detectTokens({ - networkClientId: NetworkType.mainnet, + chainIds: ['0x1'], selectedAddress: selectedAccount.address, }); @@ -2474,6 +2525,7 @@ describe('TokenDetectionController', () => { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, trackMetaMetricsEvent: mockTrackMetaMetricsEvent, + useAccountsAPI: true, // USING ACCOUNTS API }, }, async ({ @@ -2482,6 +2534,7 @@ describe('TokenDetectionController', () => { mockTokenListGetState, callActionSpy, }) => { + mockMultiChainAccountsService(); // @ts-expect-error forcing an undefined value mockGetAccount(undefined); mockTokenListGetState({ @@ -2505,7 +2558,7 @@ describe('TokenDetectionController', () => { }); await controller.detectTokens({ - networkClientId: NetworkType.mainnet, + chainIds: ['0x1'], }); expect(callActionSpy).toHaveBeenLastCalledWith( @@ -2540,6 +2593,54 @@ describe('TokenDetectionController', () => { ); }); + it('should fallback to rpc call', async () => { + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ + [sampleTokenA.address]: new BN(1), + }); + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + await withController( + { + options: { + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API + }, + mocks: { + getSelectedAccount: selectedAccount, + getAccount: selectedAccount, + }, + }, + async ({ + controller, + mockNetworkState, + triggerPreferencesStateChange, + callActionSpy, + }) => { + const mockAPI = mockMultiChainAccountsService(); + mockAPI.mockFetchMultiChainBalances.mockRejectedValue( + new Error('Mock Error'), + ); + mockNetworkState({ + ...getDefaultNetworkControllerState(), + selectedNetworkClientId: 'polygon', + }); + triggerPreferencesStateChange({ + ...getDefaultPreferencesState(), + useTokenDetection: false, + }); + await controller.detectTokens({ + chainIds: ['0x5'], + selectedAddress: selectedAccount.address, + }); + expect(callActionSpy).not.toHaveBeenCalledWith( + 'TokensController:addDetectedTokens', + ); + }, + ); + }); + /** * Test Utility - Arrange and Act `detectTokens()` with the Accounts API feature * RPC flow will return `sampleTokenA` and the Accounts API flow will use `sampleTokenB` @@ -2634,7 +2735,7 @@ describe('TokenDetectionController', () => { // Act await controller.detectTokens({ - networkClientId: NetworkType.mainnet, + chainIds: ['0x1'], selectedAddress: selectedAccount.address, }); @@ -2752,6 +2853,57 @@ describe('TokenDetectionController', () => { assertTokensNeverAdded(); }); }); + + describe('mapChainIdWithTokenListMap', () => { + it('should return an empty object when given an empty input', () => { + const tokensChainsCache = {}; + const result = mapChainIdWithTokenListMap(tokensChainsCache); + expect(result).toStrictEqual({}); + }); + + it('should return the same structure when there is no "data" property in the object', () => { + const tokensChainsCache = { + chain1: { info: 'no data property' }, + }; + const result = mapChainIdWithTokenListMap(tokensChainsCache); + expect(result).toStrictEqual(tokensChainsCache); // Expect unchanged structure + }); + + it('should map "data" property if present in the object', () => { + const tokensChainsCache = { + chain1: { data: 'someData' }, + }; + const result = mapChainIdWithTokenListMap(tokensChainsCache); + expect(result).toStrictEqual({ chain1: 'someData' }); + }); + + it('should handle multiple chains with mixed "data" properties', () => { + const tokensChainsCache = { + chain1: { data: 'someData1' }, + chain2: { info: 'no data property' }, + chain3: { data: 'someData3' }, + }; + const result = mapChainIdWithTokenListMap(tokensChainsCache); + + expect(result).toStrictEqual({ + chain1: 'someData1', + chain2: { info: 'no data property' }, + chain3: 'someData3', + }); + }); + + it('should handle nested object with "data" property correctly', () => { + const tokensChainsCache = { + chain1: { + data: { + nested: 'nestedData', + }, + }, + }; + const result = mapChainIdWithTokenListMap(tokensChainsCache); + expect(result).toStrictEqual({ chain1: { nested: 'nestedData' } }); + }); + }); }); /** diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 52a84bfc01..a2d9a744c1 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -70,6 +70,11 @@ type TokenDetectionMap = { [P in keyof TokenListMap]: Omit; }; +type NetworkClient = { + chainId: Hex; + networkClientId: string; +}; + export const STATIC_MAINNET_TOKEN_LIST = Object.entries( contractMap, ).reduce((acc, [base, contract]) => { @@ -90,7 +95,9 @@ export const STATIC_MAINNET_TOKEN_LIST = Object.entries( * @param tokensChainsCache - TokensChainsCache input object * @returns returns the map of chainId with TokenListMap */ -function mapChainIdWithTokenListMap(tokensChainsCache: TokensChainsCache) { +export function mapChainIdWithTokenListMap( + tokensChainsCache: TokensChainsCache, +) { return mapValues(tokensChainsCache, (value) => { if (isObject(value) && 'data' in value) { return get(value, ['data']); @@ -147,7 +154,7 @@ export type TokenDetectionControllerMessenger = RestrictedControllerMessenger< /** The input to start polling for the {@link TokenDetectionController} */ type TokenDetectionPollingInput = { - networkClientId: NetworkClientId; + chainIds: Hex[]; address: string; }; @@ -219,25 +226,27 @@ export class TokenDetectionController extends StaticIntervalPollingController hexToNumber(chainId)); - if (!supportedNetworks || !supportedNetworks.includes(chainIdNumber)) { + if ( + !supportedNetworks || + !chainIdNumbers.every((id) => supportedNetworks.includes(id)) + ) { const supportedNetworksErrStr = (supportedNetworks ?? []).toString(); throw new Error( - `Unsupported Network: supported networks ${supportedNetworksErrStr}, network: ${chainIdNumber}`, + `Unsupported Network: supported networks ${supportedNetworksErrStr}, requested networks: ${chainIdNumbers.toString()}`, ); } const result = await fetchMultiChainBalances( address, { - networks: [chainIdNumber], + networks: chainIdNumbers, }, this.platform, ); @@ -306,6 +315,7 @@ export class TokenDetectionController extends StaticIntervalPollingController { - const isNetworkClientIdChanged = - this.#networkClientId !== selectedNetworkClientId; - - const { chainId: newChainId } = - this.#getCorrectChainIdAndNetworkClientId(selectedNetworkClientId); - this.#isDetectionEnabledForNetwork = - isTokenDetectionSupportedForNetwork(newChainId); - - if (isNetworkClientIdChanged && this.#isDetectionEnabledForNetwork) { - this.#networkClientId = selectedNetworkClientId; - await this.#restartTokenDetection({ - networkClientId: this.#networkClientId, - }); - } - }, - ); } /** @@ -501,22 +489,38 @@ export class TokenDetectionController extends StaticIntervalPollingController { + const configuration = networkConfigurationsByChainId[chainId]; + return { + chainId, + networkClientId: + configuration.rpcEndpoints[configuration.defaultRpcEndpointIndex] + .networkClientId, + }; + }); + } + + #getCorrectChainIdAndNetworkClientId() { const { selectedNetworkClientId } = this.messagingSystem.call( 'NetworkController:getState', ); @@ -533,14 +537,14 @@ export class TokenDetectionController extends StaticIntervalPollingController { if (!this.isActive) { return; } await this.detectTokens({ - networkClientId, + chainIds, selectedAddress: address, }); } @@ -551,93 +555,178 @@ export class TokenDetectionController extends StaticIntervalPollingController { await this.detectTokens({ - networkClientId, + chainIds, selectedAddress, }); this.setIntervalLength(DEFAULT_INTERVAL); } + #getChainsToDetect( + clientNetworks: NetworkClient[], + supportedNetworks: number[] | null | undefined, + ) { + const chainsToDetectUsingAccountAPI: Hex[] = []; + const chainsToDetectUsingRpc: NetworkClient[] = []; + + clientNetworks.forEach(({ chainId, networkClientId }) => { + if (supportedNetworks?.includes(hexToNumber(chainId))) { + chainsToDetectUsingAccountAPI.push(chainId); + } else { + chainsToDetectUsingRpc.push({ chainId, networkClientId }); + } + }); + + return { chainsToDetectUsingRpc, chainsToDetectUsingAccountAPI }; + } + + async #attemptAccountAPIDetection( + chainsToDetectUsingAccountAPI: Hex[], + addressToDetect: string, + supportedNetworks: number[] | null, + ) { + return await this.#addDetectedTokensViaAPI({ + chainIds: chainsToDetectUsingAccountAPI, + selectedAddress: addressToDetect, + supportedNetworks, + }); + } + + #addChainsToRpcDetection( + chainsToDetectUsingRpc: NetworkClient[], + chainsToDetectUsingAccountAPI: Hex[], + clientNetworks: NetworkClient[], + ): void { + chainsToDetectUsingAccountAPI.forEach((chainId) => { + const networkEntry = clientNetworks.find( + (network) => network.chainId === chainId, + ); + if (networkEntry) { + chainsToDetectUsingRpc.push({ + chainId: networkEntry.chainId, + networkClientId: networkEntry.networkClientId, + }); + } + }); + } + + #shouldDetectTokens(chainId: Hex): boolean { + if (!isTokenDetectionSupportedForNetwork(chainId)) { + return false; + } + if ( + !this.#isDetectionEnabledFromPreferences && + chainId !== ChainId.mainnet + ) { + return false; + } + + const isMainnetDetectionInactive = + !this.#isDetectionEnabledFromPreferences && chainId === ChainId.mainnet; + if (isMainnetDetectionInactive) { + this.#tokensChainsCache = this.#getConvertedStaticMainnetTokenList(); + } else { + const { tokensChainsCache } = this.messagingSystem.call( + 'TokenListController:getState', + ); + this.#tokensChainsCache = tokensChainsCache ?? {}; + } + + return true; + } + + async #detectTokensUsingRpc( + chainsToDetectUsingRpc: NetworkClient[], + addressToDetect: string, + ): Promise { + for (const { chainId, networkClientId } of chainsToDetectUsingRpc) { + if (!this.#shouldDetectTokens(chainId)) { + continue; + } + + const tokenCandidateSlices = this.#getSlicesOfTokensToDetect({ + chainId, + selectedAddress: addressToDetect, + }); + const tokenDetectionPromises = tokenCandidateSlices.map((tokensSlice) => + this.#addDetectedTokens({ + tokensSlice, + selectedAddress: addressToDetect, + networkClientId, + chainId, + }), + ); + + await Promise.all(tokenDetectionPromises); + } + } + /** * For each token in the token list provided by the TokenListController, checks the token's balance for the selected account address on the active network. * On mainnet, if token detection is disabled in preferences, ERC20 token auto detection will be triggered for each contract address in the legacy token list from the @metamask/contract-metadata repo. * * @param options - Options for token detection. - * @param options.networkClientId - The ID of the network client to use. + * @param options.chainIds - The chain IDs of the network client to use. * @param options.selectedAddress - the selectedAddress against which to detect for token balances. */ async detectTokens({ - networkClientId, + chainIds, selectedAddress, }: { - networkClientId?: NetworkClientId; + chainIds?: Hex[]; selectedAddress?: string; } = {}): Promise { if (!this.isActive) { return; } - const addressAgainstWhichToDetect = - selectedAddress ?? this.#getSelectedAddress(); - const { chainId, networkClientId: selectedNetworkClientId } = - this.#getCorrectChainIdAndNetworkClientId(networkClientId); - const chainIdAgainstWhichToDetect = chainId; - const networkClientIdAgainstWhichToDetect = selectedNetworkClientId; + const addressToDetect = selectedAddress ?? this.#getSelectedAddress(); + const clientNetworks = this.#getCorrectNetworkClientIdByChainId(chainIds); - if (!isTokenDetectionSupportedForNetwork(chainIdAgainstWhichToDetect)) { - return; + let supportedNetworks; + if (this.#accountsAPI.isAccountsAPIEnabled) { + supportedNetworks = await this.#accountsAPI.getSupportedNetworks(); } - if ( - !this.#isDetectionEnabledFromPreferences && - chainIdAgainstWhichToDetect !== ChainId.mainnet - ) { - return; - } - const isTokenDetectionInactiveInMainnet = - !this.#isDetectionEnabledFromPreferences && - chainIdAgainstWhichToDetect === ChainId.mainnet; - const { tokensChainsCache } = this.messagingSystem.call( - 'TokenListController:getState', - ); - this.#tokensChainsCache = isTokenDetectionInactiveInMainnet - ? this.#getConvertedStaticMainnetTokenList() - : tokensChainsCache ?? {}; + const { chainsToDetectUsingRpc, chainsToDetectUsingAccountAPI } = + this.#getChainsToDetect(clientNetworks, supportedNetworks); + + // Try detecting tokens via Account API first if conditions allow + if (supportedNetworks && chainsToDetectUsingAccountAPI.length > 0) { + const apiResult = await this.#attemptAccountAPIDetection( + chainsToDetectUsingAccountAPI, + addressToDetect, + supportedNetworks, + ); - const tokenCandidateSlices = this.#getSlicesOfTokensToDetect({ - chainId: chainIdAgainstWhichToDetect, - selectedAddress: addressAgainstWhichToDetect, - }); + // If API succeeds and no chains are left for RPC detection, we can return early + if ( + apiResult?.result === 'success' && + chainsToDetectUsingRpc.length === 0 + ) { + return; + } - // Attempt Accounts API Detection - const accountAPIResult = await this.#addDetectedTokensViaAPI({ - chainId: chainIdAgainstWhichToDetect, - selectedAddress: addressAgainstWhichToDetect, - tokenCandidateSlices, - }); - if (accountAPIResult?.result === 'success') { - return; + // If API fails or chainsToDetectUsingRpc still has items, add chains to RPC detection + this.#addChainsToRpcDetection( + chainsToDetectUsingRpc, + chainsToDetectUsingAccountAPI, + clientNetworks, + ); } - // Attempt RPC Detection - const tokenDetectionPromises = tokenCandidateSlices.map((tokensSlice) => - this.#addDetectedTokens({ - tokensSlice, - selectedAddress: addressAgainstWhichToDetect, - networkClientId: networkClientIdAgainstWhichToDetect, - chainId: chainIdAgainstWhichToDetect, - }), - ); - - await Promise.all(tokenDetectionPromises); + // Proceed with RPC detection if there are chains remaining in chainsToDetectUsingRpc + if (chainsToDetectUsingRpc.length > 0) { + await this.#detectTokensUsingRpc(chainsToDetectUsingRpc, addressToDetect); + } } #getSlicesOfTokensToDetect({ @@ -714,91 +803,160 @@ export class TokenDetectionController extends StaticIntervalPollingController { - const tokenBalances = await this.#accountsAPI - .getMultiChainBalances(selectedAddress, chainId) + // Fetch balances for multiple chain IDs at once + const tokenBalancesByChain = await this.#accountsAPI + .getMultiNetworksBalances(selectedAddress, chainIds, supportedNetworks) .catch(() => null); - if (!tokenBalances || tokenBalances.length === 0) { + if ( + !tokenBalancesByChain || + Object.keys(tokenBalancesByChain).length === 0 + ) { return { result: 'failed' } as const; } - const tokensWithBalance: Token[] = []; - const eventTokensDetails: string[] = []; - - const tokenCandidateSet = new Set(tokenCandidateSlices.flat()); + // Process each chain ID individually + for (const chainId of chainIds) { + const isTokenDetectionInactiveInMainnet = + !this.#isDetectionEnabledFromPreferences && + chainId === ChainId.mainnet; + const { tokensChainsCache } = this.messagingSystem.call( + 'TokenListController:getState', + ); + this.#tokensChainsCache = isTokenDetectionInactiveInMainnet + ? this.#getConvertedStaticMainnetTokenList() + : tokensChainsCache ?? {}; + + // Generate token candidates based on chainId and selectedAddress + const tokenCandidateSlices = this.#getSlicesOfTokensToDetect({ + chainId, + selectedAddress, + }); - tokenBalances.forEach((token) => { - const tokenAddress = token.address; + // Filter balances for the current chainId + const tokenBalances = tokenBalancesByChain.filter( + (balance) => balance.chainId === hexToNumber(chainId), + ); - // Make sure that the token to add is in our candidate list - // Ensures we don't add tokens we already own - if (!tokenCandidateSet.has(token.address)) { - return; + if (!tokenBalances || tokenBalances.length === 0) { + continue; } - // We need specific data from tokensChainsCache to correctly create a token - // So even if we have a token that was detected correctly by the API, if its missing data we cannot safely add it. - if (!this.#tokensChainsCache[chainId].data[token.address]) { - return; + // Use helper function to filter tokens with balance for this chainId + const { tokensWithBalance, eventTokensDetails } = + this.#filterAndBuildTokensWithBalance( + tokenCandidateSlices, + tokenBalances, + chainId, + ); + + if (tokensWithBalance.length) { + this.#trackMetaMetricsEvent({ + event: 'Token Detected', + category: 'Wallet', + properties: { + tokens: eventTokensDetails, + // TODO: Either fix this lint violation or explain why it's necessary to ignore. + // eslint-disable-next-line @typescript-eslint/naming-convention + token_standard: ERC20, + // TODO: Either fix this lint violation or explain why it's necessary to ignore. + // eslint-disable-next-line @typescript-eslint/naming-convention + asset_type: ASSET_TYPES.TOKEN, + }, + }); + + await this.messagingSystem.call( + 'TokensController:addDetectedTokens', + tokensWithBalance, + { + selectedAddress, + chainId, + }, + ); } + } - const { decimals, symbol, aggregators, iconUrl, name } = - this.#tokensChainsCache[chainId].data[token.address]; - eventTokensDetails.push(`${symbol} - ${tokenAddress}`); - tokensWithBalance.push({ - address: tokenAddress, - decimals, - symbol, - aggregators, - image: iconUrl, - isERC721: false, - name, - }); - }); + return { result: 'success' } as const; + }); + } - if (tokensWithBalance.length) { - this.#trackMetaMetricsEvent({ - event: 'Token Detected', - category: 'Wallet', - properties: { - tokens: eventTokensDetails, - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention - token_standard: ERC20, - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention - asset_type: ASSET_TYPES.TOKEN, - }, - }); + /** + * Helper function to filter and build token data for detected tokens + * @param options.tokenCandidateSlices - these are tokens we know a user does not have (by checking the tokens controller). + * We will use these these token candidates to determine if a token found from the API is valid to be added on the users wallet. + * It will also prevent us to adding tokens a user already has + * @param tokenBalances - Tokens balances fetched from API + * @param chainId - The chain ID being processed + * @returns an object containing tokensWithBalance and eventTokensDetails arrays + */ - await this.messagingSystem.call( - 'TokensController:addDetectedTokens', - tokensWithBalance, - { - selectedAddress, - chainId, - }, - ); + #filterAndBuildTokensWithBalance( + tokenCandidateSlices: string[][], + tokenBalances: + | { + object: string; + type?: string; + timestamp?: string; + address: string; + symbol: string; + name: string; + decimals: number; + chainId: number; + balance: string; + }[] + | null, + chainId: Hex, + ) { + const tokensWithBalance: Token[] = []; + const eventTokensDetails: string[] = []; + + const tokenCandidateSet = new Set(tokenCandidateSlices.flat()); + + tokenBalances?.forEach((token) => { + const tokenAddress = token.address; + + // Make sure the token to add is in our candidate list + if (!tokenCandidateSet.has(tokenAddress)) { + return; } - return { result: 'success' } as const; + // Retrieve token data from cache to safely add it + const tokenData = this.#tokensChainsCache[chainId]?.data[tokenAddress]; + + // We need specific data from tokensChainsCache to correctly create a token + // So even if we have a token that was detected correctly by the API, if its missing data we cannot safely add it. + if (!tokenData) { + return; + } + + const { decimals, symbol, aggregators, iconUrl, name } = tokenData; + eventTokensDetails.push(`${symbol} - ${tokenAddress}`); + tokensWithBalance.push({ + address: tokenAddress, + decimals, + symbol, + aggregators, + image: iconUrl, + isERC721: false, + name, + }); }); + + return { tokensWithBalance, eventTokensDetails }; } async #addDetectedTokens({ diff --git a/packages/assets-controllers/src/TokenListController.test.ts b/packages/assets-controllers/src/TokenListController.test.ts index 317fc16657..bd338d3560 100644 --- a/packages/assets-controllers/src/TokenListController.test.ts +++ b/packages/assets-controllers/src/TokenListController.test.ts @@ -854,18 +854,18 @@ describe('TokenListController', () => { preventPollingOnNetworkRestart: false, messenger, interval: 100, + state: existingState, }); - await controller.start(); - expect(controller.state.tokenList).toStrictEqual({}); + expect(controller.state.tokenList).toStrictEqual(existingState.tokenList); + const pollingToken = controller.startPolling({ chainId: ChainId.mainnet }); await new Promise((resolve) => setTimeout(() => resolve(), 150)); expect(controller.state.tokenList).toStrictEqual( sampleSingleChainState.tokenList, ); - expect(controller.state.tokensChainsCache[toHex(1)].data).toStrictEqual( sampleSingleChainState.tokensChainsCache[toHex(1)].data, ); - controller.destroy(); + controller.stopPollingByPollingToken(pollingToken); }); it('should update token list from cache before reaching the threshold time', async () => { @@ -1116,45 +1116,6 @@ describe('TokenListController', () => { tokensChainsCache: {}, preventPollingOnNetworkRestart: false, }); - - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await new Promise((resolve: any) => { - messenger.subscribe('TokenListController:stateChange', (_, patch) => { - const tokenListChanged = patch.find( - (p) => Object.keys(p.value.tokenList).length !== 0, - ); - if (!tokenListChanged) { - return; - } - - expect(controller.state.tokenList).toStrictEqual( - sampleTwoChainState.tokenList, - ); - - expect( - controller.state.tokensChainsCache[toHex(56)].data, - ).toStrictEqual(sampleTwoChainState.tokensChainsCache[toHex(56)].data); - messenger.clearEventSubscriptions('TokenListController:stateChange'); - controller.destroy(); - controllerMessenger.clearEventSubscriptions( - 'NetworkController:stateChange', - ); - resolve(); - }); - - controllerMessenger.publish( - 'NetworkController:stateChange', - { - selectedNetworkClientId: selectedCustomNetworkClientId, - networkConfigurationsByChainId: {}, - networksMetadata: {}, - // @ts-expect-error This property isn't used and will get removed later. - providerConfig: {}, - }, - [], - ); - }); }); describe('startPolling', () => { @@ -1200,7 +1161,7 @@ describe('TokenListController', () => { expiredCacheExistingState.tokenList, ); - controller.startPolling({ networkClientId: 'sepolia' }); + controller.startPolling({ chainId: ChainId.sepolia }); await advanceTime({ clock, duration: 0 }); expect(fetchTokenListByChainIdSpy.mock.calls[0]).toStrictEqual( @@ -1208,49 +1169,6 @@ describe('TokenListController', () => { ); }); - it('should start polling against the token list API at the interval passed to the constructor', async () => { - const fetchTokenListByChainIdSpy = jest.spyOn( - tokenService, - 'fetchTokenListByChainId', - ); - - const controllerMessenger = getControllerMessenger(); - controllerMessenger.registerActionHandler( - 'NetworkController:getNetworkClientById', - jest.fn().mockReturnValue({ - configuration: { - type: NetworkType.goerli, - chainId: ChainId.goerli, - }, - }), - ); - const messenger = getRestrictedMessenger(controllerMessenger); - const controller = new TokenListController({ - chainId: ChainId.mainnet, - preventPollingOnNetworkRestart: false, - messenger, - state: expiredCacheExistingState, - interval: pollingIntervalTime, - }); - expect(controller.state.tokenList).toStrictEqual( - expiredCacheExistingState.tokenList, - ); - - controller.startPolling({ networkClientId: 'goerli' }); - await advanceTime({ clock, duration: 0 }); - - expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1); - await advanceTime({ clock, duration: pollingIntervalTime / 2 }); - - expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1); - await advanceTime({ clock, duration: pollingIntervalTime / 2 }); - - expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(2); - await advanceTime({ clock, duration: pollingIntervalTime }); - - expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(3); - }); - it('should update tokenList state and tokensChainsCache', async () => { const startingState: TokenListState = { tokenList: {}, @@ -1270,6 +1188,7 @@ describe('TokenListController', () => { throw new Error('Invalid chainId'); } }); + const controllerMessenger = getControllerMessenger(); controllerMessenger.registerActionHandler( 'NetworkController:getNetworkClientById', @@ -1296,7 +1215,7 @@ describe('TokenListController', () => { ); const messenger = getRestrictedMessenger(controllerMessenger); const controller = new TokenListController({ - chainId: ChainId.mainnet, + chainId: ChainId.sepolia, preventPollingOnNetworkRestart: false, messenger, state: startingState, @@ -1307,13 +1226,14 @@ describe('TokenListController', () => { // start polling for sepolia const pollingToken = controller.startPolling({ - networkClientId: 'sepolia', + chainId: ChainId.sepolia, }); + // wait a polling interval await advanceTime({ clock, duration: pollingIntervalTime }); expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1); - // expect the state to be updated with the sepolia token list + expect(controller.state.tokenList).toStrictEqual( sampleSepoliaTokensChainCache, ); @@ -1327,7 +1247,7 @@ describe('TokenListController', () => { // start polling for binance controller.startPolling({ - networkClientId: 'binance-network-client-id', + chainId: '0x38', }); await advanceTime({ clock, duration: pollingIntervalTime }); @@ -1335,10 +1255,10 @@ describe('TokenListController', () => { // because the cache for the recently fetched sepolia token list is still valid expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(2); - // expect tokenList to be updated with the binance token list + // expect tokenList to be not be updated with the binance token list, because sepolia is still this.chainId // and the cache to now contain both the binance token list and the sepolia token list expect(controller.state.tokenList).toStrictEqual( - sampleBinanceTokensChainsCache, + sampleSepoliaTokensChainCache, ); // once we adopt this polling pattern we should no longer access the root tokenList state // but rather access from the cache with a chainId selector. diff --git a/packages/assets-controllers/src/TokenListController.ts b/packages/assets-controllers/src/TokenListController.ts index ab4b709843..7f5e373777 100644 --- a/packages/assets-controllers/src/TokenListController.ts +++ b/packages/assets-controllers/src/TokenListController.ts @@ -5,7 +5,6 @@ import type { } from '@metamask/base-controller'; import { safelyExecute } from '@metamask/controller-utils'; import type { - NetworkClientId, NetworkControllerStateChangeEvent, NetworkState, NetworkControllerGetNetworkClientByIdAction, @@ -94,7 +93,7 @@ export const getDefaultTokenListState = (): TokenListState => { /** The input to start polling for the {@link TokenListController} */ type TokenListPollingInput = { - networkClientId: NetworkClientId; + chainId: Hex; }; /** @@ -155,6 +154,7 @@ export class TokenListController extends StaticIntervalPollingController { - await safelyExecute(() => this.fetchTokenList()); + async #startDeprecatedPolling(): Promise { + // renaming this to avoid collision with base class + await safelyExecute(() => this.fetchTokenList(this.chainId)); // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises this.intervalId = setInterval(async () => { - await safelyExecute(() => this.fetchTokenList()); + await safelyExecute(() => this.fetchTokenList(this.chainId)); }, this.intervalDelay); } /** - * Fetching token list from the Token Service API. + * This starts a new polling loop for any given chain. Under the hood it is deduping polls * * @private * @param input - The input for the poll. - * @param input.networkClientId - The ID of the network client triggering the fetch. + * @param input.chainId - The chainId of the chain to trigger the fetch. * @returns A promise that resolves when this operation completes. */ - async _executePoll({ - networkClientId, - }: TokenListPollingInput): Promise { - return this.fetchTokenList(networkClientId); + async _executePoll({ chainId }: TokenListPollingInput): Promise { + return this.fetchTokenList(chainId); } /** - * Fetching token list from the Token Service API. + * Fetching token list from the Token Service API. This will fetch tokens across chains. It will update tokensChainsCache (scoped across chains), and also the tokenList (scoped for the selected chain) * - * @param networkClientId - The ID of the network client triggering the fetch. + * @param chainId - The chainId of the current chain triggering the fetch. */ - async fetchTokenList(networkClientId?: NetworkClientId): Promise { + async fetchTokenList(chainId: Hex): Promise { const releaseLock = await this.mutex.acquire(); - let networkClient; - if (networkClientId) { - networkClient = this.messagingSystem.call( - 'NetworkController:getNetworkClientById', - networkClientId, - ); - } - const chainId = networkClient?.configuration.chainId ?? this.chainId; try { const { tokensChainsCache } = this.state; let tokenList: TokenListMap = {}; + // Attempt to fetch cached tokens const cachedTokens = await safelyExecute(() => this.#fetchFromCache(chainId), ); @@ -301,7 +307,7 @@ export class TokenListController extends StaticIntervalPollingController fetchTokenListByChainId( @@ -310,42 +316,38 @@ export class TokenListController extends StaticIntervalPollingController, ); - if (!tokensFromAPI) { + if (tokensFromAPI) { + // Format tokens from API (HTTP) and update tokenList + tokenList = {}; + for (const token of tokensFromAPI) { + tokenList[token.address] = { + ...token, + aggregators: formatAggregatorNames(token.aggregators), + iconUrl: formatIconUrlWithProxy({ + chainId, + tokenAddress: token.address, + }), + }; + } + } else { // Fallback to expired cached tokens tokenList = { ...(tokensChainsCache[chainId]?.data || {}) }; - this.update(() => { - return { - ...this.state, - tokenList, - tokensChainsCache, - }; - }); - return; - } - for (const token of tokensFromAPI) { - const formattedToken: TokenListToken = { - ...token, - aggregators: formatAggregatorNames(token.aggregators), - iconUrl: formatIconUrlWithProxy({ - chainId, - tokenAddress: token.address, - }), - }; - tokenList[token.address] = formattedToken; } } - const updatedTokensChainsCache: TokensChainsCache = { - ...tokensChainsCache, - [chainId]: { - timestamp: Date.now(), - data: tokenList, - }, - }; + + // Update the state with a single update for both tokenList and tokenChainsCache this.update(() => { return { ...this.state, - tokenList, - tokensChainsCache: updatedTokensChainsCache, + tokenList: + this.chainId === chainId ? tokenList : this.state.tokenList, + tokensChainsCache: { + ...tokensChainsCache, + [chainId]: { + timestamp: Date.now(), + data: tokenList, + }, + }, }; }); } finally { diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index eb12ef587d..22f7c60a1a 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -198,6 +198,84 @@ describe('TokensController', () => { }); }); + it('should add tokens and update existing ones and detected tokens', async () => { + const selectedAddress = '0x0001'; + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, + }); + await withController( + { + mockNetworkClientConfigurationsByNetworkClientId: { + networkClientId1: buildCustomNetworkClientConfiguration({ + chainId: '0x1', + }), + }, + mocks: { + getSelectedAccount: selectedAccount, + getAccount: selectedAccount, + }, + }, + async ({ controller }) => { + await controller.addDetectedTokens( + [ + { + address: '0x01', + symbol: 'barA', + decimals: 2, + }, + ], + { + selectedAddress: '0x0001', + chainId: '0x1', + }, + ); + + await controller.addTokens( + [ + { + address: '0x01', + symbol: 'barA', + decimals: 2, + aggregators: [], + name: 'Token1', + }, + { + address: '0x02', + symbol: 'barB', + decimals: 2, + aggregators: [], + name: 'Token2', + }, + ], + 'networkClientId1', + ); + + expect(controller.state.allTokens).toStrictEqual({ + '0x1': { + '0x0001': [ + { + address: '0x01', + symbol: 'barA', + decimals: 2, + aggregators: [], + name: 'Token1', + image: undefined, + }, + { + address: '0x02', + symbol: 'barB', + decimals: 2, + aggregators: [], + name: 'Token2', + image: undefined, + }, + ], + }, + }); + }, + ); + }); + it('should add detected tokens', async () => { await withController(async ({ controller }) => { await controller.addDetectedTokens([ @@ -2142,6 +2220,66 @@ describe('TokensController', () => { }, ); }); + + it('should clear allDetectedTokens under chain ID and selected address when a detected token is added to tokens list', async () => { + const selectedAddress = '0x1'; + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, + }); + const tokenAddress = '0x01'; + const dummyDetectedTokens = [ + { + address: tokenAddress, + symbol: 'barA', + decimals: 2, + aggregators: [], + isERC721: undefined, + name: undefined, + image: undefined, + }, + ]; + const dummyTokens = [ + { + address: tokenAddress, + symbol: 'barA', + decimals: 2, + aggregators: [], + isERC721: undefined, + name: undefined, + image: undefined, + }, + ]; + + await withController( + { + options: { + chainId: ChainId.mainnet, + }, + mocks: { + getSelectedAccount: selectedAccount, + }, + }, + async ({ controller }) => { + // First, add detected tokens + await controller.addDetectedTokens(dummyDetectedTokens); + expect( + controller.state.allDetectedTokens[ChainId.mainnet][ + selectedAddress + ], + ).toStrictEqual(dummyDetectedTokens); + + // Now, add the same token to the tokens list + await controller.addTokens(dummyTokens); + + // Check that allDetectedTokens for the selected address is cleared + expect( + controller.state.allDetectedTokens[ChainId.mainnet][ + selectedAddress + ], + ).toStrictEqual([]); + }, + ); + }); }); describe('when TokenListController:stateChange is published', () => { diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index fe680cdd0d..ba94bb2fb3 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -462,13 +462,16 @@ export class TokensController extends BaseController< */ async addTokens(tokensToImport: Token[], networkClientId?: NetworkClientId) { const releaseLock = await this.#mutex.acquire(); - const { tokens, detectedTokens, ignoredTokens } = this.state; + const { ignoredTokens, allDetectedTokens } = this.state; const importedTokensMap: { [key: string]: true } = {}; // Used later to dedupe imported tokens - const newTokensMap = tokens.reduce((output, current) => { - output[current.address] = current; - return output; - }, {} as { [address: string]: Token }); + const newTokensMap = Object.values(tokensToImport).reduce( + (output, token) => { + output[token.address] = token; + return output; + }, + {} as { [address: string]: Token }, + ); try { tokensToImport.forEach((tokenToAdd) => { const { address, symbol, decimals, image, aggregators, name } = @@ -488,9 +491,6 @@ export class TokensController extends BaseController< }); const newTokens = Object.values(newTokensMap); - const newDetectedTokens = detectedTokens.filter( - (token) => !importedTokensMap[token.address.toLowerCase()], - ); const newIgnoredTokens = ignoredTokens.filter( (tokenAddress) => !newTokensMap[tokenAddress.toLowerCase()], ); @@ -503,6 +503,14 @@ export class TokensController extends BaseController< ).configuration.chainId; } + const detectedTokensForGivenChain = interactingChainId + ? allDetectedTokens?.[interactingChainId]?.[this.#getSelectedAddress()] + : []; + + const newDetectedTokens = detectedTokensForGivenChain?.filter( + (t) => !importedTokensMap[t.address.toLowerCase()], + ); + const { newAllTokens, newAllDetectedTokens, newAllIgnoredTokens } = this.#getNewAllTokensState({ newTokens, diff --git a/packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-balances.ts b/packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-balances.ts index 08b0b98a44..3ac5f71697 100644 --- a/packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-balances.ts +++ b/packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-balances.ts @@ -61,6 +61,24 @@ export const MOCK_GET_BALANCES_RESPONSE: GetBalancesResponse = { balance: '100.000000000000000000', chainId: 59144, }, + { + object: 'token', + address: '0x514910771AF9Ca656af840dff83E8264EcF986CA', + name: 'Chainlink', + symbol: 'LINK', + decimals: 18, + balance: '10', + chainId: 1, + }, + { + object: 'token', + address: '0x514910771AF9Ca656af840dff83E8264EcF986CA', + name: 'Chainlink', + symbol: 'LINK', + decimals: 18, + balance: '10', + chainId: 137, + }, ], unprocessedNetworks: [], }; diff --git a/packages/controller-utils/CHANGELOG.md b/packages/controller-utils/CHANGELOG.md index 7715645282..97f34968c4 100644 --- a/packages/controller-utils/CHANGELOG.md +++ b/packages/controller-utils/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.4.3] + +### Changed + +- The `NetworkNickname` for mainnet is now `Ethereum Mainnet` instead of `Mainnet`. And the display name for Linea is now `Linea` instead of `Linea Mainnet`. ([#4865](https://github.com/MetaMask/core/pull/4865)) + ## [11.4.2] ### Changed @@ -418,7 +424,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/controller-utils@11.4.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.3...HEAD +[11.4.3]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.2...@metamask/controller-utils@11.4.3 [11.4.2]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.1...@metamask/controller-utils@11.4.2 [11.4.1]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.0...@metamask/controller-utils@11.4.1 [11.4.0]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.3.0...@metamask/controller-utils@11.4.0 diff --git a/packages/controller-utils/package.json b/packages/controller-utils/package.json index 8c4d021605..43c5aa5fb0 100644 --- a/packages/controller-utils/package.json +++ b/packages/controller-utils/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/controller-utils", - "version": "11.4.2", + "version": "11.4.3", "description": "Data and convenience functions shared by multiple packages", "keywords": [ "MetaMask", diff --git a/packages/ens-controller/package.json b/packages/ens-controller/package.json index aac3c26f59..c39d70b606 100644 --- a/packages/ens-controller/package.json +++ b/packages/ens-controller/package.json @@ -49,13 +49,13 @@ "dependencies": { "@ethersproject/providers": "^5.7.0", "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/utils": "^10.0.0", "punycode": "^2.1.1" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.1", + "@metamask/network-controller": "^22.0.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/gas-fee-controller/CHANGELOG.md b/packages/gas-fee-controller/CHANGELOG.md index 7e1cb1d9fe..01b03342fd 100644 --- a/packages/gas-fee-controller/CHANGELOG.md +++ b/packages/gas-fee-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [22.0.1] + +### Changed + +- Bump `@metamask/polling-controller` from `^12.0.0` to `^12.0.1` ([#4870](https://github.com/MetaMask/core/pull/4870)) +- Bump `@metamask/base-controller` from `^7.0.1` to `^7.0.2` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/controller-utils` from `^11.4.0` to `^11.4.3` ([#4862](https://github.com/MetaMask/core/pull/4862), [#4870](https://github.com/MetaMask/core/pull/4870), [#4195](https://github.com/MetaMask/core/pull/4195)) + ## [22.0.0] ### Changed @@ -367,7 +375,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/gas-fee-controller@22.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@22.0.1...HEAD +[22.0.1]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@22.0.0...@metamask/gas-fee-controller@22.0.1 [22.0.0]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@21.0.0...@metamask/gas-fee-controller@22.0.0 [21.0.0]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@20.0.1...@metamask/gas-fee-controller@21.0.0 [20.0.1]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@20.0.0...@metamask/gas-fee-controller@20.0.1 diff --git a/packages/gas-fee-controller/package.json b/packages/gas-fee-controller/package.json index 573ca69228..57b4ca8ee2 100644 --- a/packages/gas-fee-controller/package.json +++ b/packages/gas-fee-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/gas-fee-controller", - "version": "22.0.0", + "version": "22.0.1", "description": "Periodically calculates gas fee estimates based on various gas limits as well as other data displayed on transaction confirm screens", "keywords": [ "MetaMask", @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/eth-query": "^4.0.0", "@metamask/ethjs-unit": "^0.3.0", "@metamask/polling-controller": "^12.0.1", @@ -60,7 +60,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.1", + "@metamask/network-controller": "^22.0.2", "@types/jest": "^27.4.1", "@types/jest-when": "^2.7.3", "deepmerge": "^4.2.2", diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 8e9a19a7f6..ee4831644e 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [18.0.0] + ### Removed - **BREAKING** Remove `addNewAccountWithoutUpdate` method ([#4845](https://github.com/MetaMask/core/pull/4845)) @@ -579,7 +581,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/keyring-controller@17.3.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@18.0.0...HEAD +[18.0.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@17.3.1...@metamask/keyring-controller@18.0.0 [17.3.1]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@17.3.0...@metamask/keyring-controller@17.3.1 [17.3.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@17.2.2...@metamask/keyring-controller@17.3.0 [17.2.2]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@17.2.1...@metamask/keyring-controller@17.2.2 diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index dbb01e8dba..6bcca26f1e 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/keyring-controller", - "version": "17.3.1", + "version": "18.0.0", "description": "Stores identities seen in the wallet and manages interactions such as signing", "keywords": [ "MetaMask", diff --git a/packages/logging-controller/CHANGELOG.md b/packages/logging-controller/CHANGELOG.md index 82cca1e14f..9fbc59972f 100644 --- a/packages/logging-controller/CHANGELOG.md +++ b/packages/logging-controller/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [6.0.2] + +### Changed + +- Bump `@metamask/controller-utils` from `^11.3.0` to `^11.4.3` ([#4870](https://github.com/MetaMask/core/pull/4870), [#4862](https://github.com/MetaMask/core/pull/4862), [#4834](https://github.com/MetaMask/core/pull/4834), [#4915](https://github.com/MetaMask/core/pull/4915)) +- Bump `@metamask/base-controller` from `^7.0.1` to `^^7.0.2` ([#4862](https://github.com/MetaMask/core/pull/4862)) + ## [6.0.1] ### Fixed @@ -138,7 +145,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial Release - Add logging controller ([#1089](https://github.com/MetaMask/core.git/pull/1089)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@6.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@6.0.2...HEAD +[6.0.2]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@6.0.1...@metamask/logging-controller@6.0.2 [6.0.1]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@6.0.0...@metamask/logging-controller@6.0.1 [6.0.0]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@5.0.0...@metamask/logging-controller@6.0.0 [5.0.0]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@4.0.0...@metamask/logging-controller@5.0.0 diff --git a/packages/logging-controller/package.json b/packages/logging-controller/package.json index 6c375c895d..1a2a9eb419 100644 --- a/packages/logging-controller/package.json +++ b/packages/logging-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/logging-controller", - "version": "6.0.1", + "version": "6.0.2", "description": "Manages logging data to assist users and support staff", "keywords": [ "MetaMask", @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/packages/message-manager/package.json b/packages/message-manager/package.json index 407eb2693e..e7e4a26433 100644 --- a/packages/message-manager/package.json +++ b/packages/message-manager/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/eth-sig-util": "^8.0.0", "@metamask/utils": "^10.0.0", "@types/uuid": "^8.3.0", diff --git a/packages/name-controller/package.json b/packages/name-controller/package.json index e604ea12fe..3d6df7e7e3 100644 --- a/packages/name-controller/package.json +++ b/packages/name-controller/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/utils": "^10.0.0", "async-mutex": "^0.5.0" }, diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index f547dc5046..9321068fff 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [22.0.2] + +### Changed + +- `getDefaultNetworkConfigurationsByChainId` returns the updated display names for mainnet and linea. `Ethereum Mainnet` instead of `Mainnet`, and `Linea` instead of `Linea Mainnet`. ([#4865](https://github.com/MetaMask/core/pull/4865)) +- Bump `@metamask/controller-utils` from `^11.4.2` to `^11.4.3` ([#4915](https://github.com/MetaMask/core/pull/4915)) + ## [22.0.1] ### Changed @@ -653,7 +660,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/network-controller@22.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.0.2...HEAD +[22.0.2]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.0.1...@metamask/network-controller@22.0.2 [22.0.1]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.0.0...@metamask/network-controller@22.0.1 [22.0.0]: https://github.com/MetaMask/core/compare/@metamask/network-controller@21.1.0...@metamask/network-controller@22.0.0 [21.1.0]: https://github.com/MetaMask/core/compare/@metamask/network-controller@21.0.1...@metamask/network-controller@21.1.0 diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index d6eea69342..d1196dca8e 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/network-controller", - "version": "22.0.1", + "version": "22.0.2", "description": "Provides an interface to the currently selected network via a MetaMask-compatible provider object", "keywords": [ "MetaMask", @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/eth-block-tracker": "^11.0.2", "@metamask/eth-json-rpc-infura": "^10.0.0", "@metamask/eth-json-rpc-middleware": "^15.0.0", diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 4dfb2700b4..5e2aa5da30 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.0] + +### Changed + +- **BREAKING:** Bump `@metamask/keyring-controller` peer dependency from `^17.0.0` to `^18.0.0` ([#4195](https://github.com/MetaMask/core/pull/4195)) +- **BREAKING:** Bump `@metamask/profile-sync-controller` peer dependency from `^0.9.7` to `^1.0.0` ([#4902](https://github.com/MetaMask/core/pull/4902)) +- Bump `@metamask/controller-utils` from `^11.4.2` to `^11.4.3` ([#4195](https://github.com/MetaMask/core/pull/4195)) + ## [0.12.1] ### Uncategorized @@ -239,7 +247,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.12.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.13.0...HEAD +[0.13.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.12.1...@metamask/notification-services-controller@0.13.0 [0.12.1]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.12.0...@metamask/notification-services-controller@0.12.1 [0.12.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.11.0...@metamask/notification-services-controller@0.12.0 [0.11.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.10.0...@metamask/notification-services-controller@0.11.0 diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index f92865eec0..f80f8d0a84 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/notification-services-controller", - "version": "0.12.1", + "version": "0.13.0", "description": "Manages New MetaMask decentralized Notification system", "keywords": [ "MetaMask", @@ -101,7 +101,7 @@ "dependencies": { "@contentful/rich-text-html-renderer": "^16.5.2", "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/utils": "^10.0.0", "bignumber.js": "^9.1.2", "firebase": "^10.11.0", @@ -111,8 +111,8 @@ "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^17.3.1", - "@metamask/profile-sync-controller": "^0.9.7", + "@metamask/keyring-controller": "^18.0.0", + "@metamask/profile-sync-controller": "^1.0.0", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", "contentful": "^10.15.0", @@ -126,8 +126,8 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/keyring-controller": "^17.0.0", - "@metamask/profile-sync-controller": "^0.0.0" + "@metamask/keyring-controller": "^18.0.0", + "@metamask/profile-sync-controller": "^1.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/permission-controller/package.json b/packages/permission-controller/package.json index 332b543889..f3280d47db 100644 --- a/packages/permission-controller/package.json +++ b/packages/permission-controller/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/json-rpc-engine": "^10.0.1", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json index f34e9ec725..916eb88dc8 100644 --- a/packages/phishing-controller/package.json +++ b/packages/phishing-controller/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@noble/hashes": "^1.4.0", "@types/punycode": "^2.1.0", "ethereum-cryptography": "^2.1.2", diff --git a/packages/polling-controller/package.json b/packages/polling-controller/package.json index aff01c539b..c053be2e34 100644 --- a/packages/polling-controller/package.json +++ b/packages/polling-controller/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/utils": "^10.0.0", "@types/uuid": "^8.3.0", "fast-json-stable-stringify": "^2.1.0", @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.1", + "@metamask/network-controller": "^22.0.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/preferences-controller/CHANGELOG.md b/packages/preferences-controller/CHANGELOG.md index 5f61f71608..c1b301a341 100644 --- a/packages/preferences-controller/CHANGELOG.md +++ b/packages/preferences-controller/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [14.0.0] + +### Changed + +- **BREAKING:** Bump `@metamask/keyring-controller` peer dependency from `^17.0.0` to `^18.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) +- Bump `@metamask/controller-utils` from `^11.4.2` to `^11.4.3` ([#4915](https://github.com/MetaMask/core/pull/4915)) + +## [13.3.0] + +### Changed + +- Enable smart transactions by default for new users ([#4885](https://github.com/MetaMask/core/pull/4885)) + ## [13.2.0] ### Added @@ -308,7 +321,9 @@ 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/preferences-controller@13.2.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@14.0.0...HEAD +[14.0.0]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@13.3.0...@metamask/preferences-controller@14.0.0 +[13.3.0]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@13.2.0...@metamask/preferences-controller@13.3.0 [13.2.0]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@13.1.0...@metamask/preferences-controller@13.2.0 [13.1.0]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@13.0.3...@metamask/preferences-controller@13.1.0 [13.0.3]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@13.0.2...@metamask/preferences-controller@13.0.3 diff --git a/packages/preferences-controller/package.json b/packages/preferences-controller/package.json index e64da21e79..bcebd59c43 100644 --- a/packages/preferences-controller/package.json +++ b/packages/preferences-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/preferences-controller", - "version": "13.2.0", + "version": "14.0.0", "description": "Manages user-configurable settings for MetaMask", "keywords": [ "MetaMask", @@ -48,11 +48,11 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2" + "@metamask/controller-utils": "^11.4.3" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^17.3.1", + "@metamask/keyring-controller": "^18.0.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -63,7 +63,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/keyring-controller": "^17.0.0" + "@metamask/keyring-controller": "^18.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/preferences-controller/src/PreferencesController.test.ts b/packages/preferences-controller/src/PreferencesController.test.ts index 03d9332cc8..e36a3eeb8b 100644 --- a/packages/preferences-controller/src/PreferencesController.test.ts +++ b/packages/preferences-controller/src/PreferencesController.test.ts @@ -35,7 +35,7 @@ describe('PreferencesController', () => { acc[curr] = true; return acc; }, {} as { [chainId in EtherscanSupportedHexChainId]: boolean }), - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, useSafeChainsListValidation: true, tokenSortConfig: { key: 'tokenFiatAmount', @@ -425,6 +425,8 @@ describe('PreferencesController', () => { it('should set smartTransactionsOptInStatus', () => { const controller = setupPreferencesController(); + controller.setSmartTransactionsOptInStatus(false); + expect(controller.state.smartTransactionsOptInStatus).toBe(false); controller.setSmartTransactionsOptInStatus(true); expect(controller.state.smartTransactionsOptInStatus).toBe(true); }); diff --git a/packages/preferences-controller/src/PreferencesController.ts b/packages/preferences-controller/src/PreferencesController.ts index e67450caed..28af915a87 100644 --- a/packages/preferences-controller/src/PreferencesController.ts +++ b/packages/preferences-controller/src/PreferencesController.ts @@ -224,7 +224,7 @@ export function getDefaultPreferencesState(): PreferencesState { useNftDetection: false, useTokenDetection: true, useMultiRpcMigration: true, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, useTransactionSimulations: true, useSafeChainsListValidation: true, tokenSortConfig: { diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index d1fce99dff..c20e028436 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,10 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] + +### Changed + +- **BREAKING:** Bump `@metamask/keyring-controller` peer dependency from `^17.2.0` to `^18.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) +- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency from `^18.1.1` to `^19.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) + +## [0.9.8] + ### Changed - **BREAKING:** Bump `@metamask/network-controller` peer dependency to `^22.0.0` ([#4841](https://github.com/MetaMask/core/pull/4841)) +### Fixed + +- prevent multiple parallel account syncs by checking the value of `isAccountSyncingInProgress` before dispatching account syncing ([#4901](https://github.com/MetaMask/core/pull/4901)) + ## [0.9.7] ### Added @@ -282,7 +295,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.7...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@1.0.0...HEAD +[1.0.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.8...@metamask/profile-sync-controller@1.0.0 +[0.9.8]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.7...@metamask/profile-sync-controller@0.9.8 [0.9.7]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.6...@metamask/profile-sync-controller@0.9.7 [0.9.6]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.5...@metamask/profile-sync-controller@0.9.6 [0.9.5]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.4...@metamask/profile-sync-controller@0.9.5 diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index 5d2d6223db..dceafce99a 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/profile-sync-controller", - "version": "0.9.7", + "version": "1.0.0", "description": "The profile sync helps developers synchronize data across multiple clients and devices in a privacy-preserving way. All data saved in the user storage database is encrypted client-side to preserve privacy. The user storage provides a modular design, giving developers the flexibility to construct and manage their storage spaces in a way that best suits their needs", "keywords": [ "MetaMask", @@ -102,8 +102,8 @@ "dependencies": { "@metamask/base-controller": "^7.0.2", "@metamask/keyring-api": "^8.1.3", - "@metamask/keyring-controller": "^17.3.1", - "@metamask/network-controller": "^22.0.1", + "@metamask/keyring-controller": "^18.0.0", + "@metamask/network-controller": "^22.0.2", "@metamask/snaps-sdk": "^6.5.0", "@metamask/snaps-utils": "^8.1.1", "@noble/ciphers": "^0.5.2", @@ -114,7 +114,7 @@ }, "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", - "@metamask/accounts-controller": "^18.2.3", + "@metamask/accounts-controller": "^19.0.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/snaps-controllers": "^9.7.0", "@types/jest": "^27.4.1", @@ -129,8 +129,8 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^18.1.1", - "@metamask/keyring-controller": "^17.2.0", + "@metamask/accounts-controller": "^19.0.0", + "@metamask/keyring-controller": "^18.0.0", "@metamask/network-controller": "^22.0.0", "@metamask/snaps-controllers": "^9.7.0" }, diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index 9020e24cca..b35f400ca9 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -118,6 +118,7 @@ const metadata: StateMetadata = { type ControllerConfig = { accountSyncing?: { + maxNumberOfAccountsToAdd?: number; /** * Callback that fires when account sync adds an account. * This is used for analytics. @@ -274,17 +275,26 @@ export default class UserStorageController extends BaseController< #accounts = { // This is replaced with the actual value in the constructor - // We will remove this once the feature will be released isAccountSyncingEnabled: false, isAccountSyncingInProgress: false, - addedAccountsCount: 0, + maxNumberOfAccountsToAdd: 0, canSync: () => { try { this.#assertProfileSyncingEnabled(); - return ( - this.#accounts.isAccountSyncingEnabled && this.#auth.isAuthEnabled() - ); + if (this.#accounts.isAccountSyncingInProgress) { + return false; + } + + if (!this.#accounts.isAccountSyncingEnabled) { + return false; + } + + if (!this.#auth.isAuthEnabled()) { + return false; + } + + return true; } catch { return false; } @@ -294,8 +304,7 @@ export default class UserStorageController extends BaseController< 'AccountsController:accountAdded', // eslint-disable-next-line @typescript-eslint/no-misused-promises async (account) => { - if (this.#accounts.isAccountSyncingInProgress) { - this.#accounts.addedAccountsCount += 1; + if (!this.#accounts.canSync()) { return; } @@ -307,7 +316,7 @@ export default class UserStorageController extends BaseController< 'AccountsController:accountRenamed', // eslint-disable-next-line @typescript-eslint/no-misused-promises async (account) => { - if (this.#accounts.isAccountSyncingInProgress) { + if (!this.#accounts.canSync()) { return; } await this.saveInternalAccountToUserStorage(account); @@ -440,6 +449,9 @@ export default class UserStorageController extends BaseController< env?.isAccountSyncingEnabled, ); + this.#accounts.maxNumberOfAccountsToAdd = + config?.accountSyncing?.maxNumberOfAccountsToAdd ?? 100; + this.getMetaMetricsState = getMetaMetricsState; this.#keyringController.setupLockedStateSubscriptions(); this.#registerMessageHandlers(); @@ -819,7 +831,6 @@ export default class UserStorageController extends BaseController< try { this.#accounts.isAccountSyncingInProgress = true; - this.#accounts.addedAccountsCount = 0; const profileId = await this.#auth.getProfileId(); @@ -849,7 +860,10 @@ export default class UserStorageController extends BaseController< // so we only add new accounts if the user has more accounts than the internal accounts list if (!hasMoreInternalAccountsThanUserStorageAccounts) { const numberOfAccountsToAdd = - userStorageAccountsList.length - internalAccountsList.length; + Math.min( + userStorageAccountsList.length, + this.#accounts.maxNumberOfAccountsToAdd, + ) - internalAccountsList.length; // Create new accounts to match the user storage accounts list diff --git a/packages/queued-request-controller/CHANGELOG.md b/packages/queued-request-controller/CHANGELOG.md index 32283716c7..07e06e1a01 100644 --- a/packages/queued-request-controller/CHANGELOG.md +++ b/packages/queued-request-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.0.1] + +### Fixed + +- Fix issue where `queuedRequestCount` state is not updated after flushing requests for an origin ([#4898](https://github.com/MetaMask/core/pull/4898)) + ## [7.0.0] ### Added @@ -291,7 +297,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@7.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@7.0.1...HEAD +[7.0.1]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@7.0.0...@metamask/queued-request-controller@7.0.1 [7.0.0]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@6.0.0...@metamask/queued-request-controller@7.0.0 [6.0.0]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@5.1.0...@metamask/queued-request-controller@6.0.0 [5.1.0]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@5.0.1...@metamask/queued-request-controller@5.1.0 diff --git a/packages/queued-request-controller/package.json b/packages/queued-request-controller/package.json index 7486e56cb6..47db36ed3e 100644 --- a/packages/queued-request-controller/package.json +++ b/packages/queued-request-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/queued-request-controller", - "version": "7.0.0", + "version": "7.0.1", "description": "Includes a controller and middleware that implements a request queue", "keywords": [ "MetaMask", @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/json-rpc-engine": "^10.0.1", "@metamask/rpc-errors": "^7.0.1", "@metamask/swappable-obj-proxy": "^2.2.0", @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.1", + "@metamask/network-controller": "^22.0.2", "@metamask/selected-network-controller": "^19.0.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/queued-request-controller/src/QueuedRequestController.test.ts b/packages/queued-request-controller/src/QueuedRequestController.test.ts index 3df072ad7e..1a9b51d7e1 100644 --- a/packages/queued-request-controller/src/QueuedRequestController.test.ts +++ b/packages/queued-request-controller/src/QueuedRequestController.test.ts @@ -34,6 +34,58 @@ describe('QueuedRequestController', () => { expect(controller.state).toStrictEqual({ queuedRequestCount: 0 }); }); + it('updates queuedRequestCount when flushing requests for an origin', async () => { + const { messenger } = buildControllerMessenger(); + const controller = new QueuedRequestController({ + messenger: buildQueuedRequestControllerMessenger(messenger), + shouldRequestSwitchNetwork: () => false, + canRequestSwitchNetworkWithoutApproval: () => false, + clearPendingConfirmations: jest.fn(), + showApprovalRequest: jest.fn(), + }); + + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://example.com' }, + () => Promise.resolve(), + ); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://example2.com' }, + () => Promise.resolve(), + ); + const thirdRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://example2.com' }, + () => Promise.resolve(), + ); + + expect(controller.state.queuedRequestCount).toBe(2); + + // When the selected network changes for a domain, the queued requests for that domain/origin are flushed + messenger.publish( + 'SelectedNetworkController:stateChange', + { domains: {} }, + [ + { + op: 'replace', + path: ['domains', 'https://example2.com'], + }, + ], + ); + + expect(controller.state.queuedRequestCount).toBe(0); + + await firstRequest; + await expect(secondRequest).rejects.toThrow( + new Error( + 'The request has been rejected due to a change in selected network. Please verify the selected network and retry the request.', + ), + ); + await expect(thirdRequest).rejects.toThrow( + new Error( + 'The request has been rejected due to a change in selected network. Please verify the selected network and retry the request.', + ), + ); + }); + describe('enqueueRequest', () => { it('throws an error if networkClientId is not provided', async () => { const controller = buildQueuedRequestController(); diff --git a/packages/queued-request-controller/src/QueuedRequestController.ts b/packages/queued-request-controller/src/QueuedRequestController.ts index 4004f1604e..88958cae6e 100644 --- a/packages/queued-request-controller/src/QueuedRequestController.ts +++ b/packages/queued-request-controller/src/QueuedRequestController.ts @@ -268,6 +268,7 @@ export class QueuedRequestController extends BaseController< this.#requestQueue = this.#requestQueue.filter( ({ request }) => request.origin !== flushOrigin, ); + this.#updateQueuedRequestCount(); } /** diff --git a/packages/selected-network-controller/package.json b/packages/selected-network-controller/package.json index 43f60c384a..6c90b0bf5c 100644 --- a/packages/selected-network-controller/package.json +++ b/packages/selected-network-controller/package.json @@ -54,7 +54,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.1", + "@metamask/network-controller": "^22.0.2", "@metamask/permission-controller": "^11.0.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/signature-controller/CHANGELOG.md b/packages/signature-controller/CHANGELOG.md index cfac8b24f9..e90718bed8 100644 --- a/packages/signature-controller/CHANGELOG.md +++ b/packages/signature-controller/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [22.0.0] + +### Changed + +- **BREAKING:** Bump `@metamask/keyring-controller` peer dependency from `^17.0.0` to `^18.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) +- Bump `@metamask/controller-utils` from `^11.4.2` to `^11.4.3` ([#4915](https://github.com/MetaMask/core/pull/4915)) + +## [21.1.0] + +### Added + +- Add `isDecodeSignatureRequestEnabled` constructor callback to determine if decoding API should be used ([#4903](https://github.com/MetaMask/core/pull/4903)) +- Add `decodingApiUrl` constructor property to specify URL of API to provide additional decoding data. ([#4855](https://github.com/MetaMask/core/pull/4855)) + ## [21.0.0] ### Added @@ -400,7 +414,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#1214](https://github.com/MetaMask/core/pull/1214)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@21.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@22.0.0...HEAD +[22.0.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@21.1.0...@metamask/signature-controller@22.0.0 +[21.1.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@21.0.0...@metamask/signature-controller@21.1.0 [21.0.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@20.1.0...@metamask/signature-controller@21.0.0 [20.1.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@20.0.0...@metamask/signature-controller@20.1.0 [20.0.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@19.1.0...@metamask/signature-controller@20.0.0 diff --git a/packages/signature-controller/package.json b/packages/signature-controller/package.json index bfa14109ba..7180728e34 100644 --- a/packages/signature-controller/package.json +++ b/packages/signature-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/signature-controller", - "version": "21.0.0", + "version": "22.0.0", "description": "Processes signing requests in order to sign arbitrary and typed data", "keywords": [ "MetaMask", @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/eth-sig-util": "^8.0.0", "@metamask/utils": "^10.0.0", "jsonschema": "^1.2.4", @@ -58,9 +58,9 @@ "devDependencies": { "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^17.3.1", - "@metamask/logging-controller": "^6.0.1", - "@metamask/network-controller": "^22.0.1", + "@metamask/keyring-controller": "^18.0.0", + "@metamask/logging-controller": "^6.0.2", + "@metamask/network-controller": "^22.0.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -71,7 +71,7 @@ }, "peerDependencies": { "@metamask/approval-controller": "^7.0.0", - "@metamask/keyring-controller": "^17.0.0", + "@metamask/keyring-controller": "^18.0.0", "@metamask/logging-controller": "^6.0.0", "@metamask/network-controller": "^22.0.0" }, diff --git a/packages/signature-controller/src/SignatureController.test.ts b/packages/signature-controller/src/SignatureController.test.ts index eebfca17da..368772bec2 100644 --- a/packages/signature-controller/src/SignatureController.test.ts +++ b/packages/signature-controller/src/SignatureController.test.ts @@ -18,6 +18,7 @@ import type { SignatureRequest, } from './types'; import { SignatureRequestStatus, SignatureRequestType } from './types'; +import * as DecodingDataUtils from './utils/decoding-api'; import { normalizePersonalMessageParams, normalizeTypedMessageParams, @@ -52,6 +53,7 @@ const PARAMS_MOCK = { const REQUEST_MOCK = { networkClientId: NETWORK_CLIENT_ID_MOCK, + params: [], }; const SIGNATURE_REQUEST_MOCK: SignatureRequest = { @@ -64,6 +66,27 @@ const SIGNATURE_REQUEST_MOCK: SignatureRequest = { type: SignatureRequestType.PersonalSign, }; +const PERMIT_PARAMS_MOCK = { + data: '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","domain":{"name":"MyToken","version":"1","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","chainId":1},"message":{"owner":"0x975e73efb9ff52e23bac7f7e043a1ecd06d05477","spender":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","value":3000,"nonce":0,"deadline":50000000000}}', + from: '0x975e73efb9ff52e23bac7f7e043a1ecd06d05477', + version: 'V4', + signatureMethod: 'eth_signTypedData_v4', +}; + +const PERMIT_REQUEST_MOCK = { + method: 'eth_signTypedData_v4', + params: [ + '0x975e73efb9ff52e23bac7f7e043a1ecd06d05477', + '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","domain":{"name":"MyToken","version":"1","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","chainId":1},"message":{"owner":"0x975e73efb9ff52e23bac7f7e043a1ecd06d05477","spender":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","value":3000,"nonce":0,"deadline":50000000000}}', + ], + jsonrpc: '2.0', + id: 1680528590, + origin: 'https://metamask.github.io', + networkClientId: 'mainnet', + tabId: 1048807181, + traceContext: null, +}; + /** * Create a mock messenger instance. * @returns The mock messenger instance plus individual mock functions for each action. @@ -890,6 +913,162 @@ describe('SignatureController', () => { ).version, ).toBe(SignTypedDataVersion.V3); }); + + describe('decodeSignature', () => { + it('invoke decodeSignature to get decoding data', async () => { + const MOCK_STATE_CHANGES = { + stateChanges: [ + { + assetType: 'ERC20', + changeType: 'APPROVE', + address: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', + amount: '1461501637330902918203684832716283019655932542975', + contractAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', + }, + ], + }; + const { controller } = createController({ + decodingApiUrl: 'www.test.com', + isDecodeSignatureRequestEnabled: () => true, + }); + + jest + .spyOn(DecodingDataUtils, 'decodeSignature') + .mockResolvedValue(MOCK_STATE_CHANGES); + + await controller.newUnsignedTypedMessage( + PERMIT_PARAMS_MOCK, + PERMIT_REQUEST_MOCK, + SignTypedDataVersion.V4, + { parseJsonData: false }, + ); + + expect( + controller.state.signatureRequests[ID_MOCK].decodingLoading, + ).toBe(false); + expect( + controller.state.signatureRequests[ID_MOCK].decodingData, + ).toStrictEqual(MOCK_STATE_CHANGES); + }); + + it('does not invoke decodeSignature if decodingApiUrl is not defined', async () => { + const { controller } = createController({ + decodingApiUrl: undefined, + isDecodeSignatureRequestEnabled: () => true, + }); + + await controller.newUnsignedTypedMessage( + PERMIT_PARAMS_MOCK, + PERMIT_REQUEST_MOCK, + SignTypedDataVersion.V4, + { parseJsonData: false }, + ); + + expect( + controller.state.signatureRequests[ID_MOCK].decodingLoading, + ).toBeUndefined(); + expect( + controller.state.signatureRequests[ID_MOCK].decodingData, + ).toBeUndefined(); + }); + + it('does not invoke decodeSignature if featureFLag disableDecodingApi is true', async () => { + const { controller } = createController({ + decodingApiUrl: 'www.test.com', + isDecodeSignatureRequestEnabled: () => false, + }); + + await controller.newUnsignedTypedMessage( + PERMIT_PARAMS_MOCK, + PERMIT_REQUEST_MOCK, + SignTypedDataVersion.V4, + { parseJsonData: false }, + ); + + expect( + controller.state.signatureRequests[ID_MOCK].decodingLoading, + ).toBeUndefined(); + expect( + controller.state.signatureRequests[ID_MOCK].decodingData, + ).toBeUndefined(); + }); + + it('does not invoke decodeSignature if isDecodeSignatureRequestEnabled is not defined', async () => { + const { controller } = createController({ + decodingApiUrl: 'www.test.com', + isDecodeSignatureRequestEnabled: undefined, + }); + + await controller.newUnsignedTypedMessage( + PERMIT_PARAMS_MOCK, + PERMIT_REQUEST_MOCK, + SignTypedDataVersion.V4, + { parseJsonData: false }, + ); + + expect( + controller.state.signatureRequests[ID_MOCK].decodingLoading, + ).toBeUndefined(); + expect( + controller.state.signatureRequests[ID_MOCK].decodingData, + ).toBeUndefined(); + }); + + it('correctly set decoding data if decodeSignature fails', async () => { + const { controller } = createController({ + decodingApiUrl: 'www.test.com', + isDecodeSignatureRequestEnabled: () => true, + }); + + jest + .spyOn(DecodingDataUtils, 'decodeSignature') + .mockRejectedValue(new Error('some error')); + + await controller.newUnsignedTypedMessage( + PERMIT_PARAMS_MOCK, + PERMIT_REQUEST_MOCK, + SignTypedDataVersion.V4, + { parseJsonData: false }, + ); + + expect( + controller.state.signatureRequests[ID_MOCK].decodingLoading, + ).toBe(false); + expect( + controller.state.signatureRequests[ID_MOCK].decodingData?.error?.type, + ).toStrictEqual( + DecodingDataUtils.DECODING_API_ERRORS.DECODING_FAILED_WITH_ERROR, + ); + }); + + it('set decodingLoading to true while api request is in progress', async () => { + const { controller } = createController({ + decodingApiUrl: 'www.test.com', + isDecodeSignatureRequestEnabled: () => true, + }); + + jest + .spyOn(DecodingDataUtils, 'decodeSignature') + .mockImplementation(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({}); + }, 300); + }); + }); + + await controller.newUnsignedTypedMessage( + PERMIT_PARAMS_MOCK, + PERMIT_REQUEST_MOCK, + SignTypedDataVersion.V4, + { parseJsonData: false }, + ); + + expect( + controller.state.signatureRequests[ID_MOCK].decodingLoading, + ).toBe(true); + }); + }); }); describe('setDeferredSignSuccess', () => { @@ -920,6 +1099,8 @@ describe('SignatureController', () => { const { controller } = createController(); let resolved = false; + jest.spyOn(DecodingDataUtils, 'decodeSignature').mockResolvedValue({}); + const signaturePromise = controller .newUnsignedPersonalMessage( { @@ -998,6 +1179,8 @@ describe('SignatureController', () => { const { controller } = createController(); let rejectedError; + jest.spyOn(DecodingDataUtils, 'decodeSignature').mockResolvedValue({}); + controller .newUnsignedPersonalMessage( { diff --git a/packages/signature-controller/src/SignatureController.ts b/packages/signature-controller/src/SignatureController.ts index 80dc3aa49f..86660b0cb1 100644 --- a/packages/signature-controller/src/SignatureController.ts +++ b/packages/signature-controller/src/SignatureController.ts @@ -44,6 +44,7 @@ import type { LegacyStateMessage, StateSIWEMessage, } from './types'; +import { DECODING_API_ERRORS, decodeSignature } from './utils/decoding-api'; import { normalizePersonalMessageParams, normalizeTypedMessageParams, @@ -155,6 +156,16 @@ export type SignatureControllerOptions = { // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => Promise; + /** + * URL of API to retrieve decoding data for typed requests. + */ + decodingApiUrl?: string; + + /** + * Function to check if decoding signature request is enabled + */ + isDecodeSignatureRequestEnabled?: () => boolean; + /** * Initial state of the controller. */ @@ -176,17 +187,29 @@ export class SignatureController extends BaseController< > { hub: EventEmitter; + #decodingApiUrl?: string; + + #isDecodeSignatureRequestEnabled?: () => boolean; + #trace: TraceCallback; /** * Construct a Sign controller. * * @param options - The controller options. + * @param options.decodingApiUrl - Api used to get decoded data for permits. + * @param options.isDecodeSignatureRequestEnabled - Function to check is decoding signature request is enabled. * @param options.messenger - The restricted controller messenger for the sign controller. * @param options.state - Initial state to set on this controller. * @param options.trace - Callback to generate trace information. */ - constructor({ messenger, state, trace }: SignatureControllerOptions) { + constructor({ + decodingApiUrl, + isDecodeSignatureRequestEnabled, + messenger, + state, + trace, + }: SignatureControllerOptions) { super({ name: controllerName, metadata: stateMetadata, @@ -199,6 +222,8 @@ export class SignatureController extends BaseController< this.hub = new EventEmitter(); this.#trace = trace ?? (((_request, fn) => fn?.()) as TraceCallback); + this.#decodingApiUrl = decodingApiUrl; + this.#isDecodeSignatureRequestEnabled = isDecodeSignatureRequestEnabled; } /** @@ -462,6 +487,7 @@ export class SignatureController extends BaseController< let approveOrSignError: unknown; const finalMetadataPromise = this.#waitForFinished(metadata.id); + this.#decodePermitSignatureRequest(metadata.id, request, chainId); try { resultCallbacks = await this.#processApproval({ @@ -880,4 +906,36 @@ export class SignatureController extends BaseController< return networkClient.configuration.chainId; } + + #decodePermitSignatureRequest( + signatureRequestId: string, + request: OriginalRequest, + chainId: string, + ) { + if (!this.#isDecodeSignatureRequestEnabled?.() || !this.#decodingApiUrl) { + return; + } + this.#updateMetadata(signatureRequestId, (draftMetadata) => { + draftMetadata.decodingLoading = true; + }); + decodeSignature(request, chainId, this.#decodingApiUrl) + .then((decodingData) => + this.#updateMetadata(signatureRequestId, (draftMetadata) => { + draftMetadata.decodingData = decodingData; + draftMetadata.decodingLoading = false; + }), + ) + .catch((error) => + this.#updateMetadata(signatureRequestId, (draftMetadata) => { + draftMetadata.decodingData = { + stateChanges: null, + error: { + message: (error as unknown as Error).message, + type: DECODING_API_ERRORS.DECODING_FAILED_WITH_ERROR, + }, + }; + draftMetadata.decodingLoading = false; + }), + ); + } } diff --git a/packages/signature-controller/src/types.ts b/packages/signature-controller/src/types.ts index 8465c5844e..b4610a96cd 100644 --- a/packages/signature-controller/src/types.ts +++ b/packages/signature-controller/src/types.ts @@ -2,17 +2,44 @@ import type { SIWEMessage } from '@metamask/controller-utils'; import type { SignTypedDataVersion } from '@metamask/keyring-controller'; import type { Hex, Json } from '@metamask/utils'; +/** + * Supported signature methods. + */ +export enum EthMethod { + PersonalSign = 'personal_sign', + SignTransaction = 'eth_signTransaction', + SignTypedDataV1 = 'eth_signTypedData_v1', + SignTypedDataV3 = 'eth_signTypedData_v3', + SignTypedDataV4 = 'eth_signTypedData_v4', +} + +/** Different decoding data state change types */ +export enum DecodingDataChangeType { + Receive = 'RECEIVE', + Transfer = 'TRANSFER', + Approve = 'APPROVE', + Revoke = 'REVOKE_APPROVE', + Bidding = 'BIDDING', + Listing = 'LISTING', +} + /** Original client request that triggered the signature request. */ export type OriginalRequest = { /** Unique ID to identify the client request. */ id?: number; + /** Method of signature request */ + method?: string; + /** ID of the network client associated with the request. */ networkClientId?: string; /** Source of the client request. */ origin?: string; + /** Parameters in signature request */ + params: string[]; + /** Response following a security scan of the request. */ securityAlertResponse?: Record; }; @@ -71,15 +98,45 @@ export type MessageParamsTyped = MessageParams & { primaryType: string; message: Json; }; - /** Version of the signTypedData request. */ version?: string; }; +/** Information about a single state change returned by decoding api. */ +export type DecodingDataStateChange = { + assetType: string; + changeType: (typeof DecodingDataChangeType)[keyof typeof DecodingDataChangeType]; + address: string; + amount: string; + contractAddress: string; + tokenID?: string; +}; + +/** Array of the various state changes returned by decoding api. */ +export type DecodingDataStateChanges = DecodingDataStateChange[]; + +/** Error details for unfulfilled the decoding request. */ +export type DecodingDataError = { + message: string; + type: string; +}; + +/** Decoding data about typed sign V4 signature request. */ +export type DecodingData = { + stateChanges: DecodingDataStateChanges | null; + error?: DecodingDataError; +}; + type SignatureRequestBase = { /** ID of the associated chain. */ chainId: Hex; + /** Response from message decoding api. */ + decodingData?: DecodingData; + + /** Whether decoding is in progress. */ + decodingLoading?: boolean; + /** Error message that occurred during the signing. */ error?: string; diff --git a/packages/signature-controller/src/utils/decoding-api.test.ts b/packages/signature-controller/src/utils/decoding-api.test.ts new file mode 100644 index 0000000000..e8ee61d31c --- /dev/null +++ b/packages/signature-controller/src/utils/decoding-api.test.ts @@ -0,0 +1,100 @@ +import { EthMethod, type OriginalRequest } from '../types'; +import { decodeSignature } from './decoding-api'; + +const PERMIT_REQUEST_MOCK = { + method: EthMethod.SignTypedDataV4, + params: [ + '0x975e73efb9ff52e23bac7f7e043a1ecd06d05477', + '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","domain":{"name":"MyToken","version":"1","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","chainId":1},"message":{"owner":"0x975e73efb9ff52e23bac7f7e043a1ecd06d05477","spender":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","value":3000,"nonce":0,"deadline":50000000000}}', + ], + jsonrpc: '2.0', + id: 1680528590, + origin: 'https://metamask.github.io', + networkClientId: 'mainnet', + tabId: 1048807181, + traceContext: null, +}; + +const MOCK_RESULT = { + stateChanges: [ + { + assetType: 'ERC20', + changeType: 'APPROVE', + address: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', + amount: '1461501637330902918203684832716283019655932542975', + contractAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', + }, + ], +}; + +const MOCK_ERROR = { + error: { + message: + 'Unsupported signature. Please contact our team about adding support.', + type: 'UNSUPPORTED_SIGNATURE', + }, +}; + +describe('Decoding api', () => { + let fetchMock: jest.MockedFunction; + + /** + * Mock a JSON response from fetch. + * @param jsonResponse - The response body to return. + */ + function mockFetchResponse(jsonResponse: unknown) { + fetchMock.mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue(jsonResponse), + } as unknown as Response); + } + + it('return the data from api', async () => { + fetchMock = jest.spyOn(global, 'fetch') as jest.MockedFunction< + typeof fetch + >; + mockFetchResponse(MOCK_RESULT); + + const result = await decodeSignature( + PERMIT_REQUEST_MOCK, + '0x1', + 'https://testdecodingurl.com', + ); + + expect(result.stateChanges).toStrictEqual(MOCK_RESULT.stateChanges); + }); + + it('return error from the api as it is', async () => { + fetchMock = jest.spyOn(global, 'fetch') as jest.MockedFunction< + typeof fetch + >; + mockFetchResponse(MOCK_ERROR); + + const result = await decodeSignature( + PERMIT_REQUEST_MOCK, + '0x1', + 'https://testdecodingurl.com', + ); + + expect(result.error).toStrictEqual(MOCK_ERROR.error); + }); + + it('return failure error if there is an exception while validating', async () => { + const result = await decodeSignature( + PERMIT_REQUEST_MOCK, + '0x1', + 'https://testdecodingurl.com', + ); + + expect(result.error.type).toBe('DECODING_FAILED_WITH_ERROR'); + }); + + it('return undefined for request not of method eth_signTypedData_v4', async () => { + const result = await decodeSignature( + { method: 'eth_signTypedData_v3' } as OriginalRequest, + '0x1', + 'https://testdecodingurl.com', + ); + + expect(result.error.type).toBe('UNSUPPORTED_SIGNATURE'); + }); +}); diff --git a/packages/signature-controller/src/utils/decoding-api.ts b/packages/signature-controller/src/utils/decoding-api.ts new file mode 100644 index 0000000000..fbbfe4518b --- /dev/null +++ b/packages/signature-controller/src/utils/decoding-api.ts @@ -0,0 +1,56 @@ +import { EthMethod, type OriginalRequest } from '../types'; +import { convertNumericValuesToQuotedString } from './normalize'; + +export const DECODING_API_ERRORS = { + UNSUPPORTED_SIGNATURE: 'UNSUPPORTED_SIGNATURE', + DECODING_FAILED_WITH_ERROR: 'DECODING_FAILED_WITH_ERROR', +}; + +/** + * The function calls decoding api for typed signature V4 requests and returns the result. + * + * @param request - Signature request. + * @param chainId - chainId of network of signature request. + * @param decodingApiUrl - URL of decoding api. + * @returns Promise that resolved to give decoded data. + */ +export async function decodeSignature( + request: OriginalRequest, + chainId: string, + decodingApiUrl: string, +) { + try { + const { method, origin, params } = request; + if (request.method === EthMethod.SignTypedDataV4) { + const response = await fetch( + `${decodingApiUrl}/signature?chainId=${chainId}`, + { + method: 'POST', + body: JSON.stringify({ + method, + origin, + params: [ + params[0], + JSON.parse(convertNumericValuesToQuotedString(params[1])), + ], + }), + headers: { 'Content-Type': 'application/json' }, + }, + ); + return await response.json(); + } + return { + error: { + message: 'Unsupported signature.', + type: DECODING_API_ERRORS.UNSUPPORTED_SIGNATURE, + }, + }; + } catch (error: unknown) { + return { + error: { + message: (error as unknown as Error).message, + type: DECODING_API_ERRORS.DECODING_FAILED_WITH_ERROR, + }, + }; + } +} diff --git a/packages/signature-controller/src/utils/normalize.test.ts b/packages/signature-controller/src/utils/normalize.test.ts index 09b57628d1..b8c6d80659 100644 --- a/packages/signature-controller/src/utils/normalize.test.ts +++ b/packages/signature-controller/src/utils/normalize.test.ts @@ -2,6 +2,7 @@ import { SignTypedDataVersion } from '@metamask/keyring-controller'; import type { MessageParamsPersonal, MessageParamsTyped } from '../types'; import { + convertNumericValuesToQuotedString, normalizePersonalMessageParams, normalizeTypedMessageParams, } from './normalize'; @@ -40,4 +41,16 @@ describe('Normalize Utils', () => { }, ); }); + + describe('convertNumericValuesToQuotedString', () => { + it('wraps numeric value in a json string in quotes', async () => { + expect(convertNumericValuesToQuotedString('{temp:123}')).toBe( + '{temp:"123"}', + ); + expect(convertNumericValuesToQuotedString('{temp:{test:123}}')).toBe( + '{temp:{test:"123"}}', + ); + expect(convertNumericValuesToQuotedString('')).toBe(''); + }); + }); }); diff --git a/packages/signature-controller/src/utils/normalize.ts b/packages/signature-controller/src/utils/normalize.ts index b39b3931fe..74e95b88f8 100644 --- a/packages/signature-controller/src/utils/normalize.ts +++ b/packages/signature-controller/src/utils/normalize.ts @@ -59,3 +59,16 @@ function normalizePersonalMessageData(data: string) { return bytesToHex(Buffer.from(data, 'utf8')); } + +/** + * Takes a stringified JSON and replaces all numeric values in it with quoted strings. + * + * @param str - String of JSON to be fixed. + * @returns String with all numeric values converted to quoted strings. + */ +export function convertNumericValuesToQuotedString(str: string) { + if (!str) { + return ''; + } + return str?.replace(/(?<=:\s*)(-?\d+(\.\d+)?)(?=[,\]}])/gu, '"$1"'); +} diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 8723f4ac3d..fd8065a8f2 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [39.0.0] + +### Changed + +- **BREAKING:** Bump peer dependency `@metamask/accounts-controller` from `^18.0.0` to `^19.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) +- Bump `@metamask/controller-utils` from `^11.4.2` to `^11.4.3` ([#4915](https://github.com/MetaMask/core/pull/4915)) + +## [38.3.0] + +### Added + +- Validate gas fee properties to ensure they are valid hexadecimal strings ([#4854](https://github.com/MetaMask/core/pull/4854)) + +### Fixed + +- Fix gas limit estimation on new transactions and via `estimateGas` and `estimateGasBuffered` methods ([#4897](https://github.com/MetaMask/core/pull/4897)) + ## [38.2.0] ### Added @@ -1103,7 +1120,9 @@ 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@38.2.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@39.0.0...HEAD +[39.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@38.3.0...@metamask/transaction-controller@39.0.0 +[38.3.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@38.2.0...@metamask/transaction-controller@38.3.0 [38.2.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@38.1.0...@metamask/transaction-controller@38.2.0 [38.1.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@38.0.0...@metamask/transaction-controller@38.1.0 [38.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@37.3.0...@metamask/transaction-controller@38.0.0 diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index c583c4f79c..56b30af616 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/transaction-controller", - "version": "38.2.0", + "version": "39.0.0", "description": "Stores transactions alongside their periodically updated statuses and manages interactions such as approval and cancellation", "keywords": [ "MetaMask", @@ -54,7 +54,7 @@ "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/eth-query": "^4.0.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/nonce-tracker": "^6.0.0", @@ -69,14 +69,14 @@ }, "devDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^18.2.3", + "@metamask/accounts-controller": "^19.0.0", "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.6", "@metamask/ethjs-provider-http": "^0.3.0", - "@metamask/gas-fee-controller": "^22.0.0", + "@metamask/gas-fee-controller": "^22.0.1", "@metamask/keyring-api": "^8.1.3", - "@metamask/network-controller": "^22.0.1", + "@metamask/network-controller": "^22.0.2", "@types/bn.js": "^5.1.5", "@types/jest": "^27.4.1", "@types/node": "^16.18.54", @@ -92,7 +92,7 @@ }, "peerDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^18.0.0", + "@metamask/accounts-controller": "^19.0.0", "@metamask/approval-controller": "^7.0.0", "@metamask/gas-fee-controller": "^22.0.0", "@metamask/network-controller": "^22.0.0" diff --git a/packages/transaction-controller/src/utils/gas.test.ts b/packages/transaction-controller/src/utils/gas.test.ts index 97c34c0e3c..3060f841a0 100644 --- a/packages/transaction-controller/src/utils/gas.test.ts +++ b/packages/transaction-controller/src/utils/gas.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable jsdoc/require-jsdoc */ - import { query } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; @@ -12,7 +10,8 @@ import { updateGas, FIXED_GAS, DEFAULT_GAS_MULTIPLIER, - GAS_ESTIMATE_FALLBACK_MULTIPLIER, + GAS_ESTIMATE_FALLBACK_BLOCK_PERCENT, + MAX_GAS_BLOCK_PERCENT, } from './gas'; jest.mock('@metamask/controller-utils', () => ({ @@ -21,20 +20,18 @@ jest.mock('@metamask/controller-utils', () => ({ })); const GAS_MOCK = 100; -const BLOCK_GAS_LIMIT_MOCK = 1234567; +const BLOCK_GAS_LIMIT_MOCK = 123456789; const BLOCK_NUMBER_MOCK = '0x5678'; -// TODO: Replace `any` with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const ETH_QUERY_MOCK = {} as any as EthQuery; +const ETH_QUERY_MOCK = {} as unknown as EthQuery; +const FALLBACK_MULTIPLIER = GAS_ESTIMATE_FALLBACK_BLOCK_PERCENT / 100; +const MAX_GAS_MULTIPLIER = MAX_GAS_BLOCK_PERCENT / 100; const TRANSACTION_META_MOCK = { txParams: { data: '0x1', to: '0x2', }, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any -} as any as TransactionMeta; +} as unknown as TransactionMeta; const UPDATE_GAS_REQUEST_MOCK = { txMeta: TRANSACTION_META_MOCK, @@ -43,6 +40,11 @@ const UPDATE_GAS_REQUEST_MOCK = { ethQuery: ETH_QUERY_MOCK, } as UpdateGasRequest; +/** + * Converts number to hex string. + * @param value - The number to convert. + * @returns The hex string. + */ function toHex(value: number) { return `0x${value.toString(16)}`; } @@ -51,22 +53,26 @@ describe('gas', () => { const queryMock = jest.mocked(query); let updateGasRequest: UpdateGasRequest; + /** + * Mocks query responses. + * @param options - The options. + * @param options.getCodeResponse - The response for getCode. + * @param options.getBlockByNumberResponse - The response for getBlockByNumber. + * @param options.estimateGasResponse - The response for estimateGas. + * @param options.estimateGasError - The error for estimateGas. + */ function mockQuery({ getCodeResponse, getBlockByNumberResponse, estimateGasResponse, estimateGasError, }: { - // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any getCodeResponse?: any; - // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any getBlockByNumberResponse?: any; - // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any estimateGasResponse?: any; - // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any estimateGasError?: any; }) { @@ -85,6 +91,9 @@ describe('gas', () => { } } + /** + * Assert that estimateGas was not called. + */ function expectEstimateGasNotCalled() { expect(queryMock).not.toHaveBeenCalledWith( expect.anything(), @@ -95,6 +104,7 @@ describe('gas', () => { beforeEach(() => { updateGasRequest = JSON.parse(JSON.stringify(UPDATE_GAS_REQUEST_MOCK)); + jest.resetAllMocks(); }); describe('updateGas', () => { @@ -149,8 +159,10 @@ describe('gas', () => { ); }); - it('to estimate if estimate greater than 90% of block gas limit', async () => { - const estimatedGas = Math.ceil(BLOCK_GAS_LIMIT_MOCK * 0.9 + 10); + it('to estimate if estimate greater than percentage of block gas limit', async () => { + const estimatedGas = Math.ceil( + BLOCK_GAS_LIMIT_MOCK * MAX_GAS_MULTIPLIER + 10, + ); mockQuery({ getBlockByNumberResponse: { gasLimit: toHex(BLOCK_GAS_LIMIT_MOCK) }, @@ -165,10 +177,12 @@ describe('gas', () => { ); }); - it('to padded estimate if padded estimate less than 90% of block gas limit', async () => { - const blockGasLimit90Percent = BLOCK_GAS_LIMIT_MOCK * 0.9; - const estimatedGasPadded = Math.ceil(blockGasLimit90Percent - 10); - const estimatedGas = Math.round(estimatedGasPadded / 1.5); + it('to padded estimate if padded estimate less than percentage of block gas limit', async () => { + const maxGasLimit = BLOCK_GAS_LIMIT_MOCK * MAX_GAS_MULTIPLIER; + const estimatedGasPadded = Math.floor(maxGasLimit) - 10; + const estimatedGas = Math.ceil( + estimatedGasPadded / DEFAULT_GAS_MULTIPLIER, + ); mockQuery({ getBlockByNumberResponse: { gasLimit: toHex(BLOCK_GAS_LIMIT_MOCK) }, @@ -185,9 +199,9 @@ describe('gas', () => { ); }); - it('to padded estimate using chain multiplier if padded estimate less than 90% of block gas limit', async () => { - const blockGasLimit90Percent = BLOCK_GAS_LIMIT_MOCK * 0.9; - const estimatedGasPadded = Math.ceil(blockGasLimit90Percent - 10); + it('to padded estimate using chain multiplier if padded estimate less than percentage of block gas limit', async () => { + const maxGasLimit = BLOCK_GAS_LIMIT_MOCK * MAX_GAS_MULTIPLIER; + const estimatedGasPadded = Math.ceil(maxGasLimit - 10); const estimatedGas = estimatedGasPadded; // Optimism multiplier is 1 updateGasRequest.chainId = CHAIN_IDS.OPTIMISM; @@ -207,10 +221,14 @@ describe('gas', () => { ); }); - it('to 90% of block gas limit if padded estimate only is greater than 90% of block gas limit', async () => { - const blockGasLimit90Percent = Math.round(BLOCK_GAS_LIMIT_MOCK * 0.9); - const estimatedGasPadded = blockGasLimit90Percent + 10; - const estimatedGas = Math.ceil(estimatedGasPadded / 1.5); + it('to percentage of block gas limit if padded estimate only is greater than percentage of block gas limit', async () => { + const maxGasLimit = Math.round( + BLOCK_GAS_LIMIT_MOCK * MAX_GAS_MULTIPLIER, + ); + const estimatedGasPadded = maxGasLimit + 10; + const estimatedGas = Math.ceil( + estimatedGasPadded / DEFAULT_GAS_MULTIPLIER, + ); mockQuery({ getBlockByNumberResponse: { gasLimit: toHex(BLOCK_GAS_LIMIT_MOCK) }, @@ -219,9 +237,7 @@ describe('gas', () => { await updateGas(updateGasRequest); - expect(updateGasRequest.txMeta.txParams.gas).toBe( - toHex(blockGasLimit90Percent), - ); + expect(updateGasRequest.txMeta.txParams.gas).toBe(toHex(maxGasLimit)); expect(updateGasRequest.txMeta.originalGasEstimate).toBe( updateGasRequest.txMeta.txParams.gas, ); @@ -267,7 +283,7 @@ describe('gas', () => { describe('on estimate query error', () => { it('sets gas to 35% of block gas limit', async () => { const fallbackGas = Math.floor( - BLOCK_GAS_LIMIT_MOCK * GAS_ESTIMATE_FALLBACK_MULTIPLIER, + BLOCK_GAS_LIMIT_MOCK * FALLBACK_MULTIPLIER, ); mockQuery({ @@ -357,7 +373,7 @@ describe('gas', () => { it('returns estimated gas as 35% of block gas limit on error', async () => { const fallbackGas = Math.floor( - BLOCK_GAS_LIMIT_MOCK * GAS_ESTIMATE_FALLBACK_MULTIPLIER, + BLOCK_GAS_LIMIT_MOCK * FALLBACK_MULTIPLIER, ); mockQuery({ @@ -378,47 +394,121 @@ describe('gas', () => { simulationFails: expect.any(Object), }); }); + + it('removes gas fee properties from estimate request', async () => { + mockQuery({ + getBlockByNumberResponse: { gasLimit: toHex(BLOCK_GAS_LIMIT_MOCK) }, + estimateGasResponse: toHex(GAS_MOCK), + }); + + await estimateGas( + { + ...TRANSACTION_META_MOCK.txParams, + gasPrice: '0x1', + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + }, + ETH_QUERY_MOCK, + ); + + expect(queryMock).toHaveBeenCalledWith(ETH_QUERY_MOCK, 'estimateGas', [ + { + ...TRANSACTION_META_MOCK.txParams, + value: expect.anything(), + }, + ]); + }); + + it('normalizes data in estimate request', async () => { + mockQuery({ + getBlockByNumberResponse: { gasLimit: toHex(BLOCK_GAS_LIMIT_MOCK) }, + estimateGasResponse: toHex(GAS_MOCK), + }); + + await estimateGas( + { + ...TRANSACTION_META_MOCK.txParams, + data: '123', + }, + ETH_QUERY_MOCK, + ); + + expect(queryMock).toHaveBeenCalledWith(ETH_QUERY_MOCK, 'estimateGas', [ + expect.objectContaining({ + ...TRANSACTION_META_MOCK.txParams, + data: '0x123', + }), + ]); + }); + + it('normalizes value in estimate request', async () => { + mockQuery({ + getBlockByNumberResponse: { gasLimit: toHex(BLOCK_GAS_LIMIT_MOCK) }, + estimateGasResponse: toHex(GAS_MOCK), + }); + + await estimateGas( + { + ...TRANSACTION_META_MOCK.txParams, + value: undefined, + }, + ETH_QUERY_MOCK, + ); + + expect(queryMock).toHaveBeenCalledWith(ETH_QUERY_MOCK, 'estimateGas', [ + { + ...TRANSACTION_META_MOCK.txParams, + value: '0x0', + }, + ]); + }); }); describe('addGasBuffer', () => { - it('returns estimated gas if greater than 90% of block gas limit', () => { - const estimatedGas = Math.ceil(BLOCK_GAS_LIMIT_MOCK * 0.9 + 10); + it('returns estimated gas if greater than percentage of block gas limit', () => { + const estimatedGas = Math.ceil( + BLOCK_GAS_LIMIT_MOCK * MAX_GAS_MULTIPLIER + 10, + ); const result = addGasBuffer( toHex(estimatedGas), toHex(BLOCK_GAS_LIMIT_MOCK), - 1.5, + DEFAULT_GAS_MULTIPLIER, ); expect(result).toBe(toHex(estimatedGas)); }); - it('returns padded estimate if less than 90% of block gas limit', () => { - const blockGasLimit90Percent = BLOCK_GAS_LIMIT_MOCK * 0.9; - const estimatedGasPadded = Math.ceil(blockGasLimit90Percent - 10); - const estimatedGas = Math.round(estimatedGasPadded / 1.5); + it('returns padded estimate if less than percentage of block gas limit', () => { + const maxGasLimit = BLOCK_GAS_LIMIT_MOCK * MAX_GAS_MULTIPLIER; + const estimatedGasPadded = Math.floor(maxGasLimit - 10); + const estimatedGas = Math.ceil( + estimatedGasPadded / DEFAULT_GAS_MULTIPLIER, + ); const result = addGasBuffer( toHex(estimatedGas), toHex(BLOCK_GAS_LIMIT_MOCK), - 1.5, + DEFAULT_GAS_MULTIPLIER, ); expect(result).toBe(toHex(estimatedGasPadded)); }); - it('returns 90% of block gas limit if padded estimate only is greater than 90% of block gas limit', () => { - const blockGasLimit90Percent = Math.round(BLOCK_GAS_LIMIT_MOCK * 0.9); - const estimatedGasPadded = blockGasLimit90Percent + 10; - const estimatedGas = Math.ceil(estimatedGasPadded / 1.5); + it('returns percentage of block gas limit if padded estimate only is greater than percentage of block gas limit', () => { + const maxGasLimit = Math.round(BLOCK_GAS_LIMIT_MOCK * MAX_GAS_MULTIPLIER); + const estimatedGasPadded = maxGasLimit + 10; + const estimatedGas = Math.ceil( + estimatedGasPadded / DEFAULT_GAS_MULTIPLIER, + ); const result = addGasBuffer( toHex(estimatedGas), toHex(BLOCK_GAS_LIMIT_MOCK), - 1.5, + DEFAULT_GAS_MULTIPLIER, ); - expect(result).toBe(toHex(blockGasLimit90Percent)); + expect(result).toBe(toHex(maxGasLimit)); }); }); }); diff --git a/packages/transaction-controller/src/utils/gas.ts b/packages/transaction-controller/src/utils/gas.ts index 91f83778d5..2ffc79df25 100644 --- a/packages/transaction-controller/src/utils/gas.ts +++ b/packages/transaction-controller/src/utils/gas.ts @@ -1,6 +1,11 @@ /* eslint-disable jsdoc/require-jsdoc */ -import { BNToHex, hexToBN, query } from '@metamask/controller-utils'; +import { + BNToHex, + fractionBN, + hexToBN, + query, +} from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; import type { Hex } from '@metamask/utils'; import { add0x, createModuleLogger } from '@metamask/utils'; @@ -20,7 +25,8 @@ export const log = createModuleLogger(projectLogger, 'gas'); export const FIXED_GAS = '0x5208'; export const DEFAULT_GAS_MULTIPLIER = 1.5; -export const GAS_ESTIMATE_FALLBACK_MULTIPLIER = 0.35; +export const GAS_ESTIMATE_FALLBACK_BLOCK_PERCENT = 35; +export const MAX_GAS_BLOCK_PERCENT = 90; export async function updateGas(request: UpdateGasRequest) { const { txMeta } = request; @@ -49,22 +55,28 @@ export async function estimateGas( const request = { ...txParams }; const { data, value } = request; - const { gasLimit: gasLimitHex, number: blockNumber } = await getLatestBlock( + const { gasLimit: blockGasLimit, number: blockNumber } = await getLatestBlock( ethQuery, ); - const gasLimitBN = hexToBN(gasLimitHex); + const blockGasLimitBN = hexToBN(blockGasLimit); + + const fallback = BNToHex( + fractionBN(blockGasLimitBN, GAS_ESTIMATE_FALLBACK_BLOCK_PERCENT, 100), + ); request.data = data ? add0x(data) : data; - request.gas = BNToHex(gasLimitBN.muln(GAS_ESTIMATE_FALLBACK_MULTIPLIER)); request.value = value || '0x0'; - let estimatedGas = request.gas; - let simulationFails; + delete request.gasPrice; + delete request.maxFeePerGas; + delete request.maxPriorityFeePerGas; + + let estimatedGas = fallback; + let simulationFails: TransactionMeta['simulationFails']; try { estimatedGas = await query(ethQuery, 'estimateGas', [request]); - // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { simulationFails = { @@ -72,15 +84,15 @@ export async function estimateGas( errorKey: error.errorKey, debug: { blockNumber, - blockGasLimit: gasLimitHex, + blockGasLimit, }, }; - log('Estimation failed', { ...simulationFails, fallback: estimateGas }); + log('Estimation failed', { ...simulationFails, fallback }); } return { - blockGasLimit: gasLimitHex, + blockGasLimit, estimatedGas, simulationFails, }; @@ -92,8 +104,14 @@ export function addGasBuffer( multiplier: number, ) { const estimatedGasBN = hexToBN(estimatedGas); - const maxGasBN = hexToBN(blockGasLimit).muln(0.9); - const paddedGasBN = estimatedGasBN.muln(multiplier); + + const maxGasBN = fractionBN( + hexToBN(blockGasLimit), + MAX_GAS_BLOCK_PERCENT, + 100, + ); + + const paddedGasBN = fractionBN(estimatedGasBN, multiplier * 100, 100); if (estimatedGasBN.gt(maxGasBN)) { const estimatedGasHex = add0x(estimatedGas); diff --git a/packages/user-operation-controller/CHANGELOG.md b/packages/user-operation-controller/CHANGELOG.md index 217b56190f..1edad7b589 100644 --- a/packages/user-operation-controller/CHANGELOG.md +++ b/packages/user-operation-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [18.0.0] + +### Changed + +- **BREAKING:** Bump peer depepdency `@metamask/accounts-controller` from `^38.0.0` to `^39.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) +- **BREAKING:** Bump peer depepdency `@metamask/keyring-controller` from `^17.0.0` to `^18.0.0` ([#4915](https://github.com/MetaMask/core/pull/4915)) +- Bump `@metamask/polling-controller` from `^12.0.0` to `^12.0.1` ([#4870](https://github.com/MetaMask/core/pull/4870)) +- Bump `@metamask/base-controller` from `^7.0.1` to `^7.0.2` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/controller-utils` from `^11.4.0` to `^11.4.3` ([#4862](https://github.com/MetaMask/core/pull/4862), [#4870](https://github.com/MetaMask/core/pull/4870), [#4915](https://github.com/MetaMask/core/pull/4915)) + ## [17.0.0] ### Changed @@ -258,7 +268,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial Release ([#3749](https://github.com/MetaMask/core/pull/3749)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@17.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@18.0.0...HEAD +[18.0.0]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@17.0.0...@metamask/user-operation-controller@18.0.0 [17.0.0]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@16.0.0...@metamask/user-operation-controller@17.0.0 [16.0.0]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@15.0.1...@metamask/user-operation-controller@16.0.0 [15.0.1]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@15.0.0...@metamask/user-operation-controller@15.0.1 diff --git a/packages/user-operation-controller/package.json b/packages/user-operation-controller/package.json index edb30db36a..219eda7a18 100644 --- a/packages/user-operation-controller/package.json +++ b/packages/user-operation-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/user-operation-controller", - "version": "17.0.0", + "version": "18.0.0", "description": "Creates user operations and manages their life cycle", "keywords": [ "MetaMask", @@ -49,7 +49,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/controller-utils": "^11.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/eth-query": "^4.0.0", "@metamask/polling-controller": "^12.0.1", "@metamask/rpc-errors": "^7.0.1", @@ -63,10 +63,10 @@ "devDependencies": { "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", - "@metamask/gas-fee-controller": "^22.0.0", - "@metamask/keyring-controller": "^17.3.1", - "@metamask/network-controller": "^22.0.1", - "@metamask/transaction-controller": "^38.2.0", + "@metamask/gas-fee-controller": "^22.0.1", + "@metamask/keyring-controller": "^18.0.0", + "@metamask/network-controller": "^22.0.2", + "@metamask/transaction-controller": "^39.0.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -78,9 +78,9 @@ "peerDependencies": { "@metamask/approval-controller": "^7.0.0", "@metamask/gas-fee-controller": "^22.0.0", - "@metamask/keyring-controller": "^17.0.0", + "@metamask/keyring-controller": "^18.0.0", "@metamask/network-controller": "^22.0.0", - "@metamask/transaction-controller": "^38.0.0" + "@metamask/transaction-controller": "^39.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/yarn.lock b/yarn.lock index 310ad33deb..be82fa2fad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2027,7 +2027,7 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^18.2.3, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^19.0.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: @@ -2036,7 +2036,7 @@ __metadata: "@metamask/base-controller": "npm:^7.0.2" "@metamask/eth-snap-keyring": "npm:^4.3.6" "@metamask/keyring-api": "npm:^8.1.3" - "@metamask/keyring-controller": "npm:^17.3.1" + "@metamask/keyring-controller": "npm:^18.0.0" "@metamask/snaps-controllers": "npm:^9.7.0" "@metamask/snaps-sdk": "npm:^6.5.0" "@metamask/snaps-utils": "npm:^8.1.1" @@ -2053,7 +2053,7 @@ __metadata: typescript: "npm:~5.2.2" uuid: "npm:^8.3.2" peerDependencies: - "@metamask/keyring-controller": ^17.0.0 + "@metamask/keyring-controller": ^18.0.0 "@metamask/snaps-controllers": ^9.7.0 languageName: unknown linkType: soft @@ -2075,7 +2075,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2133,20 +2133,20 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/accounts-controller": "npm:^18.2.3" + "@metamask/accounts-controller": "npm:^19.0.0" "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-provider-http": "npm:^0.3.0" "@metamask/keyring-api": "npm:^8.1.3" - "@metamask/keyring-controller": "npm:^17.3.1" + "@metamask/keyring-controller": "npm:^18.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/polling-controller": "npm:^12.0.1" - "@metamask/preferences-controller": "npm:^13.2.0" + "@metamask/preferences-controller": "npm:^14.0.0" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" @@ -2172,11 +2172,11 @@ __metadata: typescript: "npm:~5.2.2" uuid: "npm:^8.3.2" peerDependencies: - "@metamask/accounts-controller": ^18.0.0 + "@metamask/accounts-controller": ^19.0.0 "@metamask/approval-controller": ^7.0.0 - "@metamask/keyring-controller": ^17.0.0 + "@metamask/keyring-controller": ^18.0.0 "@metamask/network-controller": ^22.0.0 - "@metamask/preferences-controller": ^13.0.0 + "@metamask/preferences-controller": ^14.0.0 languageName: unknown linkType: soft @@ -2327,7 +2327,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.4.2, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.3, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -2437,8 +2437,8 @@ __metadata: "@ethersproject/providers": "npm:^5.7.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/controller-utils": "npm:^11.4.3" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2804,7 +2804,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2817,16 +2817,16 @@ __metadata: languageName: unknown linkType: soft -"@metamask/gas-fee-controller@npm:^22.0.0, @metamask/gas-fee-controller@workspace:packages/gas-fee-controller": +"@metamask/gas-fee-controller@npm:^22.0.1, @metamask/gas-fee-controller@workspace:packages/gas-fee-controller": version: 0.0.0-use.local resolution: "@metamask/gas-fee-controller@workspace:packages/gas-fee-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-unit": "npm:^0.3.0" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/polling-controller": "npm:^12.0.1" "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" @@ -2931,7 +2931,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^17.3.1, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^18.0.0, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -2967,13 +2967,13 @@ __metadata: languageName: unknown linkType: soft -"@metamask/logging-controller@npm:^6.0.1, @metamask/logging-controller@workspace:packages/logging-controller": +"@metamask/logging-controller@npm:^6.0.2, @metamask/logging-controller@workspace:packages/logging-controller": version: 0.0.0-use.local resolution: "@metamask/logging-controller@workspace:packages/logging-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -2991,7 +2991,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -3035,7 +3035,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" async-mutex: "npm:^0.5.0" @@ -3048,14 +3048,14 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^22.0.1, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@npm:^22.0.2, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: "@json-rpc-specification/meta-schema": "npm:^1.0.6" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-block-tracker": "npm:^11.0.2" "@metamask/eth-json-rpc-infura": "npm:^10.0.0" "@metamask/eth-json-rpc-middleware": "npm:^15.0.0" @@ -3125,9 +3125,9 @@ __metadata: "@lavamoat/allow-scripts": "npm:^3.0.4" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" - "@metamask/keyring-controller": "npm:^17.3.1" - "@metamask/profile-sync-controller": "npm:^0.9.7" + "@metamask/controller-utils": "npm:^11.4.3" + "@metamask/keyring-controller": "npm:^18.0.0" + "@metamask/profile-sync-controller": "npm:^1.0.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" "@types/readable-stream": "npm:^2.3.0" @@ -3145,8 +3145,8 @@ __metadata: typescript: "npm:~5.2.2" uuid: "npm:^8.3.2" peerDependencies: - "@metamask/keyring-controller": ^17.0.0 - "@metamask/profile-sync-controller": ^0.0.0 + "@metamask/keyring-controller": ^18.0.0 + "@metamask/profile-sync-controller": ^1.0.0 languageName: unknown linkType: soft @@ -3187,7 +3187,7 @@ __metadata: "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" @@ -3234,7 +3234,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@noble/hashes": "npm:^1.4.0" "@types/jest": "npm:^27.4.1" "@types/punycode": "npm:^2.1.0" @@ -3258,8 +3258,8 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/controller-utils": "npm:^11.4.3" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" "@types/uuid": "npm:^8.3.0" @@ -3287,14 +3287,14 @@ __metadata: languageName: node linkType: hard -"@metamask/preferences-controller@npm:^13.2.0, @metamask/preferences-controller@workspace:packages/preferences-controller": +"@metamask/preferences-controller@npm:^14.0.0, @metamask/preferences-controller@workspace:packages/preferences-controller": version: 0.0.0-use.local resolution: "@metamask/preferences-controller@workspace:packages/preferences-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" - "@metamask/keyring-controller": "npm:^17.3.1" + "@metamask/controller-utils": "npm:^11.4.3" + "@metamask/keyring-controller": "npm:^18.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -3304,21 +3304,21 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/keyring-controller": ^17.0.0 + "@metamask/keyring-controller": ^18.0.0 languageName: unknown linkType: soft -"@metamask/profile-sync-controller@npm:^0.9.7, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": +"@metamask/profile-sync-controller@npm:^1.0.0, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": version: 0.0.0-use.local resolution: "@metamask/profile-sync-controller@workspace:packages/profile-sync-controller" dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" - "@metamask/accounts-controller": "npm:^18.2.3" + "@metamask/accounts-controller": "npm:^19.0.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/keyring-api": "npm:^8.1.3" - "@metamask/keyring-controller": "npm:^17.3.1" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/keyring-controller": "npm:^18.0.0" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/snaps-controllers": "npm:^9.7.0" "@metamask/snaps-sdk": "npm:^6.5.0" "@metamask/snaps-utils": "npm:^8.1.1" @@ -3338,8 +3338,8 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/accounts-controller": ^18.1.1 - "@metamask/keyring-controller": ^17.2.0 + "@metamask/accounts-controller": ^19.0.0 + "@metamask/keyring-controller": ^18.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/snaps-controllers": ^9.7.0 languageName: unknown @@ -3372,9 +3372,9 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/json-rpc-engine": "npm:^10.0.1" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/selected-network-controller": "npm:^19.0.0" "@metamask/swappable-obj-proxy": "npm:^2.2.0" @@ -3458,7 +3458,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/json-rpc-engine": "npm:^10.0.1" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/permission-controller": "npm:^11.0.3" "@metamask/swappable-obj-proxy": "npm:^2.2.0" "@metamask/utils": "npm:^10.0.0" @@ -3486,11 +3486,11 @@ __metadata: "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-sig-util": "npm:^8.0.0" - "@metamask/keyring-controller": "npm:^17.3.1" - "@metamask/logging-controller": "npm:^6.0.1" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/keyring-controller": "npm:^18.0.0" + "@metamask/logging-controller": "npm:^6.0.2" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3504,7 +3504,7 @@ __metadata: uuid: "npm:^8.3.2" peerDependencies: "@metamask/approval-controller": ^7.0.0 - "@metamask/keyring-controller": ^17.0.0 + "@metamask/keyring-controller": ^18.0.0 "@metamask/logging-controller": ^6.0.0 "@metamask/network-controller": ^22.0.0 languageName: unknown @@ -3678,7 +3678,7 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^38.2.0, @metamask/transaction-controller@workspace:packages/transaction-controller": +"@metamask/transaction-controller@npm:^39.0.0, @metamask/transaction-controller@workspace:packages/transaction-controller": version: 0.0.0-use.local resolution: "@metamask/transaction-controller@workspace:packages/transaction-controller" dependencies: @@ -3689,18 +3689,18 @@ __metadata: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^18.2.3" + "@metamask/accounts-controller": "npm:^19.0.0" "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-json-rpc-provider": "npm:^4.1.6" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-provider-http": "npm:^0.3.0" - "@metamask/gas-fee-controller": "npm:^22.0.0" + "@metamask/gas-fee-controller": "npm:^22.0.1" "@metamask/keyring-api": "npm:^8.1.3" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/nonce-tracker": "npm:^6.0.0" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" @@ -3724,7 +3724,7 @@ __metadata: uuid: "npm:^8.3.2" peerDependencies: "@babel/runtime": ^7.23.9 - "@metamask/accounts-controller": ^18.0.0 + "@metamask/accounts-controller": ^19.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/gas-fee-controller": ^22.0.0 "@metamask/network-controller": ^22.0.0 @@ -3738,15 +3738,15 @@ __metadata: "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-query": "npm:^4.0.0" - "@metamask/gas-fee-controller": "npm:^22.0.0" - "@metamask/keyring-controller": "npm:^17.3.1" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/gas-fee-controller": "npm:^22.0.1" + "@metamask/keyring-controller": "npm:^18.0.0" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/polling-controller": "npm:^12.0.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^38.2.0" + "@metamask/transaction-controller": "npm:^39.0.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" bn.js: "npm:^5.2.1" @@ -3762,9 +3762,9 @@ __metadata: peerDependencies: "@metamask/approval-controller": ^7.0.0 "@metamask/gas-fee-controller": ^22.0.0 - "@metamask/keyring-controller": ^17.0.0 + "@metamask/keyring-controller": ^18.0.0 "@metamask/network-controller": ^22.0.0 - "@metamask/transaction-controller": ^38.0.0 + "@metamask/transaction-controller": ^39.0.0 languageName: unknown linkType: soft