diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3fd483ab14e..c3d65089c7e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -69,6 +69,8 @@ /packages/approval-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/wallet-framework-engineers /packages/assets-controller/package.json @MetaMask/metamask-assets @MetaMask/wallet-framework-engineers /packages/assets-controller/CHANGELOG.md @MetaMask/metamask-assets @MetaMask/wallet-framework-engineers +/packages/chain-controller/package.json @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers +/packages/chain-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/ens-controller/package.json @MetaMask/confirmations @MetaMask/wallet-framework-engineers /packages/ens-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/wallet-framework-engineers /packages/gas-fee-controller/package.json @MetaMask/confirmations @MetaMask/wallet-framework-engineers @@ -85,6 +87,8 @@ /packages/notification-controller/CHANGELOG.md @MetaMask/snaps-devs @MetaMask/wallet-framework-engineers /packages/notification-services-controller/package.json @MetaMask/notifications @MetaMask/wallet-framework-engineers /packages/notification-services-controller/CHANGELOG.md @MetaMask/notifications @MetaMask/wallet-framework-engineers +/packages/phishing-controller/package.json @MetaMask/product-safety @MetaMask/wallet-framework-engineers +/packages/phishing-controller/CHANGELOG.md @MetaMask/product-safety @MetaMask/wallet-framework-engineers /packages/profile-sync-controller/package.json @MetaMask/notifications @MetaMask/wallet-framework-engineers /packages/profile-sync-controller/CHANGELOG.md @MetaMask/notifications @MetaMask/wallet-framework-engineers /packages/queued-request-controller/package.json @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers diff --git a/examples/example-controllers/package.json b/examples/example-controllers/package.json index 57f55e2cd8c..0f9f2f0d266 100644 --- a/examples/example-controllers/package.json +++ b/examples/example-controllers/package.json @@ -47,12 +47,12 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/utils": "^10.0.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/controller-utils": "^11.4.0", + "@metamask/controller-utils": "^11.4.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/package.json b/package.json index f42f40c188f..326b68f058b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "232.0.0", + "version": "236.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { @@ -57,8 +57,8 @@ "@metamask/eslint-config-nodejs": "^12.1.0", "@metamask/eslint-config-typescript": "^12.1.0", "@metamask/eth-block-tracker": "^11.0.2", - "@metamask/eth-json-rpc-provider": "^4.1.5", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/eth-json-rpc-provider": "^4.1.6", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/utils": "^10.0.0", "@ts-bridge/cli": "^0.5.1", "@types/jest": "^27.4.1", diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index e888866f89d..f617adb7ccc 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [18.2.3] + +### Changed + +- Bump `@metamask/base-controller` from `^7.0.1` to `^7.0.2` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) +- Bump dev dependency `@metamask/keyring-controller` from `^17.2.2` to `^17.3.1` ([#4810](https://github.com/MetaMask/core/pull/4810), [#4870](https://github.com/MetaMask/core/pull/4870)) + ## [18.2.2] ### Changed @@ -329,7 +337,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.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@18.2.3...HEAD +[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 [18.2.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@18.1.1...@metamask/accounts-controller@18.2.0 diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index aedd3f12ed0..8732b1f425f 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/accounts-controller", - "version": "18.2.2", + "version": "18.2.3", "description": "Manages internal accounts", "keywords": [ "MetaMask", @@ -48,7 +48,7 @@ }, "dependencies": { "@ethereumjs/util": "^8.1.0", - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/eth-snap-keyring": "^4.3.6", "@metamask/keyring-api": "^8.1.3", "@metamask/snaps-sdk": "^6.5.0", @@ -61,7 +61,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^17.3.0", + "@metamask/keyring-controller": "^17.3.1", "@metamask/snaps-controllers": "^9.7.0", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", diff --git a/packages/address-book-controller/package.json b/packages/address-book-controller/package.json index eb99239134f..bd8f3b37bb6 100644 --- a/packages/address-book-controller/package.json +++ b/packages/address-book-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@metamask/utils": "^10.0.0" }, "devDependencies": { diff --git a/packages/announcement-controller/package.json b/packages/announcement-controller/package.json index 1d7eb8be80e..8967a252d8d 100644 --- a/packages/announcement-controller/package.json +++ b/packages/announcement-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1" + "@metamask/base-controller": "^7.0.2" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/approval-controller/CHANGELOG.md b/packages/approval-controller/CHANGELOG.md index d37a69fb91a..29d6a5d4a21 100644 --- a/packages/approval-controller/CHANGELOG.md +++ b/packages/approval-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.1.1] + +### Changed + +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) + ## [7.1.0] ### Added @@ -247,7 +253,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/approval-controller@7.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.1.1...HEAD +[7.1.1]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.1.0...@metamask/approval-controller@7.1.1 [7.1.0]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.0.4...@metamask/approval-controller@7.1.0 [7.0.4]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.0.3...@metamask/approval-controller@7.0.4 [7.0.3]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.0.2...@metamask/approval-controller@7.0.3 diff --git a/packages/approval-controller/package.json b/packages/approval-controller/package.json index 7cb3084fecf..c736ff7edbb 100644 --- a/packages/approval-controller/package.json +++ b/packages/approval-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/approval-controller", - "version": "7.1.0", + "version": "7.1.1", "description": "Manages requests that require user approval", "keywords": [ "MetaMask", @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", "nanoid": "^3.1.31" diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index ded76feffef..5c0b9fc657a 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [41.0.0] + +### Changed + +- **BREAKING**: The polling input accepted by `CurrencyRateController` is now an object with a `nativeCurrencies` property that is defined as a `string` array type ([#4852](https://github.com/MetaMask/core/pull/4852)) + - The `input` parameters of the controller's `_executePoll`, `_startPolling`, `onPollingComplete` methods now only accept this new polling input type. + - The `nativeCurrency` property (`string` type) has been removed. +- **BREAKING**: `RatesController` now types the `conversionRate` and `usdConversionRate` in its state as `number` instead of `string`, to match what it was actually storing. ([#4852](https://github.com/MetaMask/core/pull/4852)) +- 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.1` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump dev dependency `@metamask/approval-controller` from `^7.1.0` to `^7.1.1` ([#4862](https://github.com/MetaMask/core/pull/4862)) + ## [40.0.0] ### Changed @@ -1164,7 +1176,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@40.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@41.0.0...HEAD +[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 [39.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@38.3.0...@metamask/assets-controllers@39.0.0 [38.3.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@38.2.0...@metamask/assets-controllers@38.3.0 diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 8854e0a989c..6701ade97c7 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/assets-controllers", - "version": "40.0.0", + "version": "41.0.0", "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", "keywords": [ "MetaMask", @@ -53,12 +53,12 @@ "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@metamask/abi-utils": "^2.0.3", - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/contract-metadata": "^2.4.0", - "@metamask/controller-utils": "^11.4.0", + "@metamask/controller-utils": "^11.4.2", "@metamask/eth-query": "^4.0.0", "@metamask/metamask-eth-abis": "^3.1.1", - "@metamask/polling-controller": "^12.0.0", + "@metamask/polling-controller": "^12.0.1", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", "@types/bn.js": "^5.1.5", @@ -73,14 +73,14 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/accounts-controller": "^18.2.2", - "@metamask/approval-controller": "^7.1.0", + "@metamask/accounts-controller": "^18.2.3", + "@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.0", - "@metamask/network-controller": "^22.0.0", - "@metamask/preferences-controller": "^13.1.0", + "@metamask/keyring-controller": "^17.3.1", + "@metamask/network-controller": "^22.0.1", + "@metamask/preferences-controller": "^13.2.0", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.191", "@types/node": "^16.18.54", diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index 4ecc4642dc6..f22e8c1471b 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -118,46 +118,50 @@ describe('CurrencyRateController', () => { }); it('should not poll before being started', async () => { - const fetchExchangeRateStub = jest.fn(); + const fetchMultiExchangeRateStub = jest.fn(); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ interval: 100, - fetchExchangeRate: fetchExchangeRateStub, + fetchMultiExchangeRate: fetchMultiExchangeRateStub, messenger, }); await advanceTime({ clock, duration: 200 }); - expect(fetchExchangeRateStub).not.toHaveBeenCalled(); + expect(fetchMultiExchangeRateStub).not.toHaveBeenCalled(); controller.destroy(); }); it('should poll and update state in the right interval', async () => { + const currentCurrency = 'cad'; + jest .spyOn(global.Date, 'now') .mockReturnValueOnce(10000) .mockReturnValueOnce(20000); - const fetchExchangeRateStub = jest + const fetchMultiExchangeRateStub = jest .fn() .mockResolvedValueOnce({ - conversionRate: 1, - usdConversionRate: 11, + eth: { [currentCurrency]: 1, usd: 11 }, }) .mockResolvedValueOnce({ - conversionRate: 2, - usdConversionRate: 22, + eth: { + [currentCurrency]: 2, + usd: 22, + }, }); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ interval: 100, - fetchExchangeRate: fetchExchangeRateStub, + fetchMultiExchangeRate: fetchMultiExchangeRateStub, messenger, + state: { currentCurrency }, }); - controller.startPolling({ nativeCurrency: 'ETH' }); + controller.startPolling({ nativeCurrencies: ['ETH'] }); await advanceTime({ clock, duration: 0 }); - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(1); expect(controller.state.currencyRates).toStrictEqual({ ETH: { conversionDate: 10, @@ -167,11 +171,11 @@ describe('CurrencyRateController', () => { }); await advanceTime({ clock, duration: 99 }); - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(1); await advanceTime({ clock, duration: 1 }); - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(2); + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(2); expect(controller.state.currencyRates).toStrictEqual({ ETH: { conversionDate: 20, @@ -184,67 +188,70 @@ describe('CurrencyRateController', () => { }); it('should not poll after being stopped', async () => { - const fetchExchangeRateStub = jest.fn(); + const fetchMultiExchangeRateStub = jest.fn(); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ interval: 100, - fetchExchangeRate: fetchExchangeRateStub, + fetchMultiExchangeRate: fetchMultiExchangeRateStub, messenger, }); - controller.startPolling({ nativeCurrency: 'ETH' }); + controller.startPolling({ nativeCurrencies: ['ETH'] }); await advanceTime({ clock, duration: 0 }); controller.stopAllPolling(); // called once upon initial start - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(1); await advanceTime({ clock, duration: 150, stepSize: 50 }); - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(1); controller.destroy(); }); it('should poll correctly after being started, stopped, and started again', async () => { - const fetchExchangeRateStub = jest.fn(); + const fetchMultiExchangeRateStub = jest.fn(); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ interval: 100, - fetchExchangeRate: fetchExchangeRateStub, + fetchMultiExchangeRate: fetchMultiExchangeRateStub, messenger, }); - controller.startPolling({ nativeCurrency: 'ETH' }); + controller.startPolling({ nativeCurrencies: ['ETH'] }); await advanceTime({ clock, duration: 0 }); controller.stopAllPolling(); // called once upon initial start - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(1); - controller.startPolling({ nativeCurrency: 'ETH' }); + controller.startPolling({ nativeCurrencies: ['ETH'] }); await advanceTime({ clock, duration: 0 }); - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(2); + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(2); await advanceTime({ clock, duration: 100 }); - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(3); + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(3); }); it('should update exchange rate', async () => { + const currentCurrency = 'cad'; + jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate()); - const fetchExchangeRateStub = jest + const fetchMultiExchangeRateStub = jest .fn() - .mockResolvedValue({ conversionRate: 10, usdConversionRate: 111 }); + .mockResolvedValue({ eth: { [currentCurrency]: 10, usd: 111 } }); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ interval: 10, - fetchExchangeRate: fetchExchangeRateStub, + fetchMultiExchangeRate: fetchMultiExchangeRateStub, messenger, + state: { currentCurrency }, }); expect(controller.state.currencyRates).toStrictEqual({ @@ -255,7 +262,7 @@ describe('CurrencyRateController', () => { }, }); - await controller.updateExchangeRate('ETH'); + await controller.updateExchangeRate(['ETH']); expect(controller.state.currencyRates).toStrictEqual({ ETH: { @@ -269,25 +276,33 @@ describe('CurrencyRateController', () => { }); it('should use the exchange rate for ETH when native currency is testnet ETH', async () => { + const currentCurrency = 'cad'; + jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate()); - const fetchExchangeRateStub = jest + const fetchMultiExchangeRateStub = jest .fn() - .mockImplementation((_, nativeCurrency) => { + .mockImplementation((_, cryptocurrencies) => { + const nativeCurrency = cryptocurrencies[0]; if (nativeCurrency === 'ETH') { return { - conversionRate: 10, - usdConversionRate: 110, + [nativeCurrency.toLowerCase()]: { + [currentCurrency.toLowerCase()]: 10, + usd: 110, + }, }; } return { - conversionRate: 0, - usdConversionRate: 100, + [nativeCurrency.toLowerCase()]: { + [currentCurrency.toLowerCase()]: 0, + usd: 100, + }, }; }); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ - fetchExchangeRate: fetchExchangeRateStub, + fetchMultiExchangeRate: fetchMultiExchangeRateStub, messenger, + state: { currentCurrency }, }); expect(controller.state.currencyRates).toStrictEqual({ @@ -298,7 +313,7 @@ describe('CurrencyRateController', () => { }, }); - await controller.updateExchangeRate('SepoliaETH'); + await controller.updateExchangeRate(['SepoliaETH']); expect(controller.state.currencyRates).toStrictEqual({ ETH: { @@ -317,14 +332,16 @@ describe('CurrencyRateController', () => { }); it('should update current currency then clear and refetch rates', async () => { + const currentCurrency = 'cad'; jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate()); - const fetchExchangeRateStub = jest - .fn() - .mockResolvedValue({ conversionRate: 10, usdConversionRate: 11 }); + const fetchMultiExchangeRateStub = jest.fn().mockResolvedValue({ + eth: { [currentCurrency]: 10, usd: 11 }, + btc: { [currentCurrency]: 10, usd: 11 }, + }); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ interval: 10, - fetchExchangeRate: fetchExchangeRateStub, + fetchMultiExchangeRate: fetchMultiExchangeRateStub, messenger, state: { currencyRates: { @@ -342,10 +359,10 @@ describe('CurrencyRateController', () => { }, }); - await controller.setCurrentCurrency('CAD'); + await controller.setCurrentCurrency(currentCurrency); expect(controller.state).toStrictEqual({ - currentCurrency: 'CAD', + currentCurrency, currencyRates: { ETH: { conversionDate: 0, @@ -358,7 +375,7 @@ describe('CurrencyRateController', () => { await advanceTime({ clock, duration: 0 }); expect(controller.state).toStrictEqual({ - currentCurrency: 'CAD', + currentCurrency, currencyRates: { ETH: { conversionDate: getStubbedDate() / 1000, @@ -377,19 +394,19 @@ describe('CurrencyRateController', () => { }); it('should add usd rate to state when includeUsdRate is configured true', async () => { - const fetchExchangeRateStub = jest.fn().mockResolvedValue({}); + const fetchMultiExchangeRateStub = jest.fn().mockResolvedValue({}); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ includeUsdRate: true, - fetchExchangeRate: fetchExchangeRateStub, + fetchMultiExchangeRate: fetchMultiExchangeRateStub, messenger, state: { currentCurrency: 'xyz' }, }); - await controller.updateExchangeRate('SepoliaETH'); + await controller.updateExchangeRate(['SepoliaETH']); - expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); - expect(fetchExchangeRateStub.mock.calls).toMatchObject([ - ['xyz', 'ETH', true], + expect(fetchMultiExchangeRateStub).toHaveBeenCalledTimes(1); + expect(fetchMultiExchangeRateStub.mock.calls).toMatchObject([ + ['xyz', ['ETH'], true], ]); controller.destroy(); @@ -399,8 +416,8 @@ describe('CurrencyRateController', () => { jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate()); const cryptoCompareHost = 'https://min-api.cryptocompare.com'; nock(cryptoCompareHost) - .get('/data/price?fsym=ETH&tsyms=XYZ') - .reply(200, { XYZ: 2000.42 }) + .get('/data/pricemulti?fsyms=ETH&tsyms=xyz') + .reply(200, { ETH: { XYZ: 2000.42 } }) .persist(); const messenger = getRestrictedMessenger(); const controller = new CurrencyRateController({ @@ -408,7 +425,7 @@ describe('CurrencyRateController', () => { state: { currentCurrency: 'xyz' }, }); - await controller.updateExchangeRate('ETH'); + await controller.updateExchangeRate(['ETH']); expect(controller.state).toStrictEqual({ currentCurrency: 'xyz', @@ -416,7 +433,7 @@ describe('CurrencyRateController', () => { ETH: { conversionDate: getStubbedDate() / 1000, conversionRate: 2000.42, - usdConversionRate: NaN, + usdConversionRate: null, }, }, }); @@ -427,7 +444,7 @@ describe('CurrencyRateController', () => { it('should throw unexpected errors', async () => { const cryptoCompareHost = 'https://min-api.cryptocompare.com'; nock(cryptoCompareHost) - .get('/data/price?fsym=ETH&tsyms=XYZ') + .get('/data/pricemulti?fsyms=ETH&tsyms=xyz') .reply(200, { Response: 'Error', Message: 'this method has been deprecated', @@ -440,49 +457,17 @@ describe('CurrencyRateController', () => { state: { currentCurrency: 'xyz' }, }); - await expect(controller.updateExchangeRate('ETH')).rejects.toThrow( + await expect(controller.updateExchangeRate(['ETH'])).rejects.toThrow( 'this method has been deprecated', ); controller.destroy(); }); - it('should catch expected errors', async () => { - const cryptoCompareHost = 'https://min-api.cryptocompare.com'; - nock(cryptoCompareHost) - .get('/data/price?fsym=ETH&tsyms=XYZ') - .reply(200, { - Response: 'Error', - Message: 'market does not exist for this coin pair', - }) - .persist(); - - const messenger = getRestrictedMessenger(); - const controller = new CurrencyRateController({ - messenger, - state: { currentCurrency: 'xyz' }, - }); - - await controller.updateExchangeRate('ETH'); - - expect(controller.state).toStrictEqual({ - currentCurrency: 'xyz', - currencyRates: { - ETH: { - conversionDate: null, - conversionRate: null, - usdConversionRate: null, - }, - }, - }); - - controller.destroy(); - }); - it('should not update state on unexpected / transient errors', async () => { const cryptoCompareHost = 'https://min-api.cryptocompare.com'; nock(cryptoCompareHost) - .get('/data/price?fsym=ETH&tsyms=XYZ') + .get('/data/pricemulti?fsyms=ETH&tsyms=xyz') .reply(500) // HTTP 500 transient error .persist(); @@ -500,8 +485,8 @@ describe('CurrencyRateController', () => { const controller = new CurrencyRateController({ messenger, state }); // Error should still be thrown - await expect(controller.updateExchangeRate('ETH')).rejects.toThrow( - `Fetch failed with status '500' for request 'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=XYZ'`, + await expect(controller.updateExchangeRate(['ETH'])).rejects.toThrow( + `Fetch failed with status '500' for request 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=ETH&tsyms=xyz'`, ); // But state should not be changed @@ -509,4 +494,48 @@ describe('CurrencyRateController', () => { controller.destroy(); }); + + it('fetches exchange rates for multiple native currencies', async () => { + jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate()); + const cryptoCompareHost = 'https://min-api.cryptocompare.com'; + nock(cryptoCompareHost) + .get('/data/pricemulti?fsyms=ETH,POL,BNB&tsyms=xyz') + .reply(200, { + BNB: { XYZ: 500.1 }, + ETH: { XYZ: 4000.42 }, + POL: { XYZ: 0.3 }, + }) + .persist(); + const messenger = getRestrictedMessenger(); + const controller = new CurrencyRateController({ + messenger, + state: { currentCurrency: 'xyz' }, + }); + + await controller.updateExchangeRate(['ETH', 'POL', 'BNB']); + + const conversionDate = getStubbedDate() / 1000; + expect(controller.state).toStrictEqual({ + currentCurrency: 'xyz', + currencyRates: { + BNB: { + conversionDate, + conversionRate: 500.1, + usdConversionRate: null, + }, + ETH: { + conversionDate, + conversionRate: 4000.42, + usdConversionRate: null, + }, + POL: { + conversionDate, + conversionRate: 0.3, + usdConversionRate: null, + }, + }, + }); + + controller.destroy(); + }); }); diff --git a/packages/assets-controllers/src/CurrencyRateController.ts b/packages/assets-controllers/src/CurrencyRateController.ts index 4f3f21011d1..ccb12282dfc 100644 --- a/packages/assets-controllers/src/CurrencyRateController.ts +++ b/packages/assets-controllers/src/CurrencyRateController.ts @@ -11,7 +11,7 @@ import type { NetworkControllerGetNetworkClientByIdAction } from '@metamask/netw import { StaticIntervalPollingController } from '@metamask/polling-controller'; import { Mutex } from 'async-mutex'; -import { fetchExchangeRate as defaultFetchExchangeRate } from './crypto-compare-service'; +import { fetchMultiExchangeRate as defaultFetchMultiExchangeRate } from './crypto-compare-service'; /** * @type CurrencyRateState @@ -77,7 +77,7 @@ const defaultState = { /** The input to start polling for the {@link CurrencyRateController} */ type CurrencyRatePollingInput = { - nativeCurrency: string; + nativeCurrencies: string[]; }; /** @@ -91,7 +91,7 @@ export class CurrencyRateController extends StaticIntervalPollingController { private readonly mutex = new Mutex(); - private readonly fetchExchangeRate; + private readonly fetchMultiExchangeRate; private readonly includeUsdRate; @@ -103,20 +103,20 @@ export class CurrencyRateController extends StaticIntervalPollingController; - fetchExchangeRate?: typeof defaultFetchExchangeRate; + fetchMultiExchangeRate?: typeof defaultFetchMultiExchangeRate; }) { super({ name, @@ -126,7 +126,7 @@ export class CurrencyRateController extends StaticIntervalPollingController { + async updateExchangeRate(nativeCurrencies: string[]): Promise { const releaseLock = await this.mutex.acquire(); - const { currentCurrency, currencyRates } = this.state; - - let conversionDate: number | null = null; - let conversionRate: number | null = null; - let usdConversionRate: number | null = null; - - // For preloaded testnets (Goerli, Sepolia) we want to fetch exchange rate for real ETH. - const nativeCurrencyForExchangeRate = Object.values( - TESTNET_TICKER_SYMBOLS, - ).includes(nativeCurrency) - ? FALL_BACK_VS_CURRENCY // ETH - : nativeCurrency; - - let shouldUpdateState = true; try { - if ( - currentCurrency && - nativeCurrency && - // if either currency is an empty string we can skip the comparison - // because it will result in an error from the api and ultimately - // a null conversionRate either way. - currentCurrency !== '' && - nativeCurrency !== '' - ) { - const fetchExchangeRateResponse = await this.fetchExchangeRate( - currentCurrency, - nativeCurrencyForExchangeRate, - this.includeUsdRate, - ); - conversionRate = fetchExchangeRateResponse.conversionRate; - usdConversionRate = fetchExchangeRateResponse.usdConversionRate; - conversionDate = Date.now() / 1000; - } + const { currentCurrency } = this.state; + + // For preloaded testnets (Goerli, Sepolia) we want to fetch exchange rate for real ETH. + // Map each native currency to the symbol we want to fetch for it. + const testnetSymbols = Object.values(TESTNET_TICKER_SYMBOLS); + const nativeCurrenciesToFetch = nativeCurrencies.reduce( + (acc, nativeCurrency) => { + acc[nativeCurrency] = testnetSymbols.includes(nativeCurrency) + ? FALL_BACK_VS_CURRENCY + : nativeCurrency; + return acc; + }, + {} as Record, + ); + + const fetchExchangeRateResponse = await this.fetchMultiExchangeRate( + currentCurrency, + [...new Set(Object.values(nativeCurrenciesToFetch))], + this.includeUsdRate, + ); + + const rates = Object.entries(nativeCurrenciesToFetch).reduce( + (acc, [nativeCurrency, fetchedCurrency]) => { + const rate = fetchExchangeRateResponse[fetchedCurrency.toLowerCase()]; + acc[nativeCurrency] = { + conversionDate: rate !== undefined ? Date.now() / 1000 : null, + conversionRate: rate?.[currentCurrency.toLowerCase()] ?? null, + usdConversionRate: rate?.usd ?? null, + }; + return acc; + }, + {} as CurrencyRateState['currencyRates'], + ); + + this.update((state) => { + state.currencyRates = { + ...state.currencyRates, + ...rates, + }; + }); } catch (error) { - if ( - !( - error instanceof Error && - error.message.includes('market does not exist for this coin pair') - ) - ) { - // Don't update state on transient / unexpected errors - shouldUpdateState = false; - throw error; - } + console.error('Failed to fetch exchange rates.', error); + throw error; } finally { - try { - if (shouldUpdateState) { - this.update(() => { - return { - currencyRates: { - ...currencyRates, - [nativeCurrency]: { - conversionDate, - conversionRate, - usdConversionRate, - }, - }, - currentCurrency, - }; - }); - } - } finally { - releaseLock(); - } + releaseLock(); } } @@ -240,12 +222,12 @@ export class CurrencyRateController extends StaticIntervalPollingController { - await this.updateExchangeRate(nativeCurrency); + await this.updateExchangeRate(nativeCurrencies); } } diff --git a/packages/assets-controllers/src/RatesController/RatesController.test.ts b/packages/assets-controllers/src/RatesController/RatesController.test.ts index 12277764f58..bf9d0fc1938 100644 --- a/packages/assets-controllers/src/RatesController/RatesController.test.ts +++ b/packages/assets-controllers/src/RatesController/RatesController.test.ts @@ -119,7 +119,7 @@ describe('RatesController', () => { const publishActionSpy = jest.spyOn(messenger, 'publish'); jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate()); - const mockRateValue = '57715.42'; + const mockRateValue = 57715.42; const fetchExchangeRateStub = jest.fn(() => { return Promise.resolve({ btc: { @@ -142,7 +142,7 @@ describe('RatesController', () => { expect(ratesPreUpdate).toStrictEqual({ btc: { conversionDate: 0, - conversionRate: '0', + conversionRate: 0, }, }); @@ -177,12 +177,12 @@ describe('RatesController', () => { it('starts the polling process with custom values', async () => { jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate()); - const mockBtcUsdRateValue = '62235.48'; - const mockSolUsdRateValue = '148.41'; - const mockStrkUsdRateValue = '1.248'; - const mockBtcEurRateValue = '57715.42'; - const mockSolEurRateValue = '137.68'; - const mockStrkEurRateValue = '1.157'; + const mockBtcUsdRateValue = 62235.48; + const mockSolUsdRateValue = 148.41; + const mockStrkUsdRateValue = 1.248; + const mockBtcEurRateValue = 57715.42; + const mockSolEurRateValue = 137.68; + const mockStrkEurRateValue = 1.157; const fetchExchangeRateStub = jest.fn(() => { return Promise.resolve({ btc: { diff --git a/packages/assets-controllers/src/RatesController/RatesController.ts b/packages/assets-controllers/src/RatesController/RatesController.ts index 461e471d579..7abed73eed5 100644 --- a/packages/assets-controllers/src/RatesController/RatesController.ts +++ b/packages/assets-controllers/src/RatesController/RatesController.ts @@ -29,7 +29,7 @@ const defaultState = { rates: { [Cryptocurrency.Btc]: { conversionDate: 0, - conversionRate: '0', + conversionRate: 0, }, }, cryptocurrencies: [Cryptocurrency.Btc], @@ -119,7 +119,7 @@ export class RatesController extends BaseController< const { fiatCurrency, cryptocurrencies } = this.state; const response: Record< Cryptocurrency, - Record + Record > = await this.#fetchMultiExchangeRate( fiatCurrency, cryptocurrencies, diff --git a/packages/assets-controllers/src/RatesController/types.ts b/packages/assets-controllers/src/RatesController/types.ts index 7a422aaef2a..ba8ad5aa377 100644 --- a/packages/assets-controllers/src/RatesController/types.ts +++ b/packages/assets-controllers/src/RatesController/types.ts @@ -12,17 +12,16 @@ import type { /** * Represents the conversion rates from one currency to others, including the conversion date. - * The `conversionRate` field is a string that maps a cryptocurrency code (e.g., "BTC") to its - * conversion rate. For this field we use string as the data type to avoid potential rounding - * errors and precision loss. - * The `usdConversionRate` provides the conversion rate to USD as a string, or `null` if the - * conversion rate to USD is not available. We also use string for the same reason as stated before. + * The `conversionRate` field is a number that maps a cryptocurrency code (e.g., "BTC") to its + * conversion rate. + * The `usdConversionRate` provides the conversion rate to USD as a number, or `null` if the + * conversion rate to USD is not available. * The `conversionDate` is a Unix timestamp (number) indicating when the conversion rate was last updated. */ export type Rate = { - conversionRate: string; + conversionRate: number; conversionDate: number; - usdConversionRate?: string; + usdConversionRate?: number; }; /** diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index fdce86d678e..7dc2fd66162 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -334,11 +334,17 @@ describe('TokenDetectionController', () => { getSelectedAccount: defaultSelectedAccount, }, }, - async ({ controller, mockNetworkState }) => { + async ({ controller, mockNetworkState, mockGetNetworkClientById }) => { mockNetworkState({ ...getDefaultNetworkControllerState(), selectedNetworkClientId: NetworkType.goerli, }); + mockGetNetworkClientById( + () => + ({ + configuration: { chainId: '0x5' }, + } as unknown as AutoManagedNetworkClient), + ); await controller.start(); expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled(); @@ -1058,15 +1064,20 @@ describe('TokenDetectionController', () => { mockGetAccount(firstSelectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + '0x1': { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1111,15 +1122,20 @@ describe('TokenDetectionController', () => { }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1170,15 +1186,20 @@ describe('TokenDetectionController', () => { }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1224,15 +1245,20 @@ describe('TokenDetectionController', () => { }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1288,15 +1314,20 @@ describe('TokenDetectionController', () => { }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1341,15 +1372,20 @@ describe('TokenDetectionController', () => { }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1528,15 +1564,20 @@ describe('TokenDetectionController', () => { }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1581,15 +1622,20 @@ describe('TokenDetectionController', () => { }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1635,15 +1681,20 @@ describe('TokenDetectionController', () => { }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); @@ -1710,7 +1761,6 @@ describe('TokenDetectionController', () => { }; const tokenListState = { ...getDefaultTokenListState(), - tokenList, tokensChainsCache: { '0x1': { timestamp: 0, @@ -1760,7 +1810,7 @@ describe('TokenDetectionController', () => { }) => { const tokenListState = { ...getDefaultTokenListState(), - tokenList: {}, + tokensChainsCache: {}, }; mockTokenListGetState(tokenListState); @@ -1801,15 +1851,20 @@ describe('TokenDetectionController', () => { }) => { const tokenListState = { ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }; @@ -1853,15 +1908,20 @@ describe('TokenDetectionController', () => { }) => { const tokenListState = { ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }; @@ -1877,6 +1937,220 @@ describe('TokenDetectionController', () => { ); }); }); + + describe('when previous and incoming tokensChainsCache are equal with the same timestamp', () => { + it('should not call detect tokens', async () => { + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ + [sampleTokenA.address]: new BN(1), + }); + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + await withController( + { + options: { + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + }, + mocks: { + getSelectedAccount: selectedAccount, + getAccount: selectedAccount, + }, + }, + async ({ + mockTokenListGetState, + triggerTokenListStateChange, + controller, + }) => { + const tokenListState = { + ...getDefaultTokenListState(), + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, + }, + }, + }; + mockTokenListGetState(tokenListState); + // This should set the tokensChainsCache value + triggerTokenListStateChange(tokenListState); + await advanceTime({ clock, duration: 1 }); + + const mockTokens = jest.spyOn(controller, 'detectTokens'); + + // Re-trigger state change so that incoming list is equal the current list in state + triggerTokenListStateChange(tokenListState); + await advanceTime({ clock, duration: 1 }); + expect(mockTokens).toHaveBeenCalledTimes(0); + }, + ); + }); + }); + + describe('when previous and incoming tokensChainsCache are equal with different timestamp', () => { + it('should not call detect tokens', async () => { + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ + [sampleTokenA.address]: new BN(1), + }); + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + await withController( + { + options: { + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + }, + mocks: { + getSelectedAccount: selectedAccount, + getAccount: selectedAccount, + }, + }, + async ({ + mockTokenListGetState, + triggerTokenListStateChange, + controller, + }) => { + const tokenListState = { + ...getDefaultTokenListState(), + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, + }, + }, + }; + mockTokenListGetState(tokenListState); + // This should set the tokensChainsCache value + triggerTokenListStateChange(tokenListState); + await advanceTime({ clock, duration: 1 }); + + const mockTokens = jest.spyOn(controller, 'detectTokens'); + + // Re-trigger state change so that incoming list is equal the current list in state + triggerTokenListStateChange({ + ...tokenListState, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 3424, // same list with different timestamp should not trigger detectTokens again + }, + }, + }); + await advanceTime({ clock, duration: 1 }); + expect(mockTokens).toHaveBeenCalledTimes(0); + }, + ); + }); + }); + + describe('when previous and incoming tokensChainsCache are not equal', () => { + it('should call detect tokens', async () => { + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ + [sampleTokenA.address]: new BN(1), + }); + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + await withController( + { + options: { + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + }, + mocks: { + getSelectedAccount: selectedAccount, + getAccount: selectedAccount, + }, + }, + async ({ + mockTokenListGetState, + triggerTokenListStateChange, + controller, + }) => { + const tokenListState = { + ...getDefaultTokenListState(), + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, + }, + }, + }; + mockTokenListGetState(tokenListState); + // This should set the tokensChainsCache value + triggerTokenListStateChange(tokenListState); + await advanceTime({ clock, duration: 1 }); + + const mockTokens = jest.spyOn(controller, 'detectTokens'); + + // Re-trigger state change so that incoming list is equal the current list in state + triggerTokenListStateChange({ + ...tokenListState, + tokensChainsCache: { + ...tokenListState.tokensChainsCache, + [ChainId['linea-mainnet']]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 5546454, + }, + }, + }); + await advanceTime({ clock, duration: 1 }); + expect(mockTokens).toHaveBeenCalledTimes(1); + }, + ); + }); + }); }); describe('startPolling', () => { @@ -1910,15 +2184,20 @@ describe('TokenDetectionController', () => { async ({ controller, mockTokenListGetState }) => { mockTokenListGetState({ ...getDefaultTokenListState(), - tokenList: { - [sampleTokenA.address]: { - name: sampleTokenA.name, - symbol: sampleTokenA.symbol, - decimals: sampleTokenA.decimals, - address: sampleTokenA.address, - occurrences: 1, - aggregators: sampleTokenA.aggregators, - iconUrl: sampleTokenA.image, + tokensChainsCache: { + [ChainId.mainnet]: { + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + timestamp: 0, }, }, }); diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index a01433bc4d8..e5846abb4a8 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -35,6 +35,7 @@ import type { } from '@metamask/preferences-controller'; import type { Hex } from '@metamask/utils'; import { hexToNumber } from '@metamask/utils'; +import { isEqual, mapValues, isObject, get } from 'lodash'; import type { AssetsContractController } from './AssetsContractController'; import { isTokenDetectionSupportedForNetwork } from './assetsUtil'; @@ -46,6 +47,7 @@ import type { GetTokenListState, TokenListMap, TokenListStateChange, + TokensChainsCache, } from './TokenListController'; import type { Token } from './TokenRatesController'; import type { @@ -83,6 +85,20 @@ export const STATIC_MAINNET_TOKEN_LIST = Object.entries( }; }, {}); +/** + * Function that takes a TokensChainsCache object and maps chainId with TokenListMap. + * @param tokensChainsCache - TokensChainsCache input object + * @returns returns the map of chainId with TokenListMap + */ +function mapChainIdWithTokenListMap(tokensChainsCache: TokensChainsCache) { + return mapValues(tokensChainsCache, (value) => { + if (isObject(value) && 'data' in value) { + return get(value, ['data']); + } + return value; + }); +} + export const controllerName = 'TokenDetectionController'; export type TokenDetectionState = Record; @@ -156,7 +172,7 @@ export class TokenDetectionController extends StaticIntervalPollingController { - const hasTokens = Object.keys(tokenList).length; - - if (hasTokens) { + async ({ tokensChainsCache }) => { + const isEqualValues = this.#compareTokensChainsCache( + tokensChainsCache, + this.#tokensChainsCache, + ); + if (!isEqualValues) { await this.#restartTokenDetection(); } }, @@ -445,6 +468,29 @@ export class TokenDetectionController extends StaticIntervalPollingController ({ + ...acc, + [key]: { + name: value.name, + symbol: value.symbol, + decimals: value.decimals, + address: value.address, + aggregators: [], + iconUrl: value?.iconUrl, + }, + }), + {}, + ); + return { + '0x1': { + data, + timestamp: 0, + }, + }; + } + /** * This adds detected tokens from the Accounts API, avoiding the multi-call RPC calls for balances * @param options - method arguments @@ -671,14 +742,14 @@ export class TokenDetectionController extends StaticIntervalPollingController t.toUpperCase() === 'USD') + ? [...tsyms, 'USD'] + : tsyms; const params = new URLSearchParams(); - params.append('fsyms', fsyms); - params.append('tsyms', updatedTsyms); + params.append('fsyms', fsyms.join(',')); + params.append('tsyms', updatedTsyms.join(',')); const url = new URL(`${CRYPTO_COMPARE_DOMAIN}/data/pricemulti`); url.search = params.toString(); - return url.toString(); } @@ -139,19 +140,19 @@ export async function fetchMultiExchangeRate( fiatCurrency: string, cryptocurrencies: string[], includeUSDRate: boolean, -): Promise>> { +): Promise>> { const url = getMultiPricingURL( - Object.values(cryptocurrencies).join(','), - fiatCurrency, + cryptocurrencies, + [fiatCurrency], includeUSDRate, ); const response = await handleFetch(url); handleErrorResponse(response); - const rates: Record> = {}; + const rates: Record> = {}; for (const [cryptocurrency, values] of Object.entries(response) as [ string, - Record, + Record, ][]) { rates[cryptocurrency.toLowerCase()] = { [fiatCurrency.toLowerCase()]: values[fiatCurrency.toUpperCase()], diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md index a13d6d7994d..b8ce86fe454 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.0.2] + +### Changed + +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) + ## [7.0.1] ### Fixed @@ -263,7 +269,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/base-controller@7.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.0.2...HEAD +[7.0.2]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.0.1...@metamask/base-controller@7.0.2 [7.0.1]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.0.0...@metamask/base-controller@7.0.1 [7.0.0]: https://github.com/MetaMask/core/compare/@metamask/base-controller@6.0.3...@metamask/base-controller@7.0.0 [6.0.3]: https://github.com/MetaMask/core/compare/@metamask/base-controller@6.0.2...@metamask/base-controller@6.0.3 diff --git a/packages/base-controller/package.json b/packages/base-controller/package.json index a3c056b0ff4..ae72b464b40 100644 --- a/packages/base-controller/package.json +++ b/packages/base-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/base-controller", - "version": "7.0.1", + "version": "7.0.2", "description": "Provides scaffolding for controllers as well a communication system for all controllers", "keywords": [ "MetaMask", @@ -51,7 +51,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/json-rpc-engine": "^10.0.1", "@types/jest": "^27.4.1", "@types/sinon": "^9.0.10", "deepmerge": "^4.2.2", diff --git a/packages/chain-controller/package.json b/packages/chain-controller/package.json index e7f1107d761..e6fdbe57316 100644 --- a/packages/chain-controller/package.json +++ b/packages/chain-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/chain-api": "^0.1.0", "@metamask/keyring-api": "^8.1.3", "@metamask/snaps-controllers": "^9.7.0", diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json index a5af404a756..b2be46f99bf 100644 --- a/packages/composable-controller/package.json +++ b/packages/composable-controller/package.json @@ -47,11 +47,11 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1" + "@metamask/base-controller": "^7.0.2" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/json-rpc-engine": "^10.0.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "immer": "^9.0.6", diff --git a/packages/controller-utils/CHANGELOG.md b/packages/controller-utils/CHANGELOG.md index 0884fe89cf6..77156452820 100644 --- a/packages/controller-utils/CHANGELOG.md +++ b/packages/controller-utils/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.4.2] + +### Changed + +- Move BigNumber.js from devDependencies to dependencies ([#4873](https://github.com/MetaMask/core/pull/4873)) + +## [11.4.1] + +### Changed + +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) + ## [11.4.0] ### Added @@ -406,7 +418,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/controller-utils@11.4.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.2...HEAD +[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 [11.3.0]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.2.0...@metamask/controller-utils@11.3.0 [11.2.0]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.1.0...@metamask/controller-utils@11.2.0 diff --git a/packages/controller-utils/package.json b/packages/controller-utils/package.json index 82d39855b2e..8c4d0216050 100644 --- a/packages/controller-utils/package.json +++ b/packages/controller-utils/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/controller-utils", - "version": "11.4.0", + "version": "11.4.2", "description": "Data and convenience functions shared by multiple packages", "keywords": [ "MetaMask", @@ -53,6 +53,7 @@ "@metamask/utils": "^10.0.0", "@spruceid/siwe-parser": "2.1.0", "@types/bn.js": "^5.1.5", + "bignumber.js": "^9.1.2", "bn.js": "^5.2.1", "eth-ens-namehash": "^2.0.8", "fast-deep-equal": "^3.1.3" @@ -60,7 +61,6 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@types/jest": "^27.4.1", - "bignumber.js": "^9.1.2", "deepmerge": "^4.2.2", "jest": "^27.5.1", "nock": "^13.3.1", diff --git a/packages/ens-controller/package.json b/packages/ens-controller/package.json index 0be37c38e5f..aac3c26f59a 100644 --- a/packages/ens-controller/package.json +++ b/packages/ens-controller/package.json @@ -48,14 +48,14 @@ }, "dependencies": { "@ethersproject/providers": "^5.7.0", - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@metamask/utils": "^10.0.0", "punycode": "^2.1.1" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.0", + "@metamask/network-controller": "^22.0.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index 57408de42b1..639e1af8786 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [4.1.6] + +### Changed + +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) +- Bump `@metamask/rpc-errors` from `^6.3.1` to `^7.0.0` ([#4769](https://github.com/MetaMask/core/pull/4769)) + ## [4.1.5] ### Fixed @@ -168,7 +175,8 @@ Release `v2.0.0` is identical to `v1.0.1` aside from Node.js version requirement - Initial release, including `providerFromEngine` and `providerFromMiddleware`. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/eth-json-rpc-provider@4.1.5...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/eth-json-rpc-provider@4.1.6...HEAD +[4.1.6]: https://github.com/MetaMask/core/compare/@metamask/eth-json-rpc-provider@4.1.5...@metamask/eth-json-rpc-provider@4.1.6 [4.1.5]: https://github.com/MetaMask/core/compare/@metamask/eth-json-rpc-provider@4.1.4...@metamask/eth-json-rpc-provider@4.1.5 [4.1.4]: https://github.com/MetaMask/core/compare/@metamask/eth-json-rpc-provider@4.1.3...@metamask/eth-json-rpc-provider@4.1.4 [4.1.3]: https://github.com/MetaMask/core/compare/@metamask/eth-json-rpc-provider@4.1.2...@metamask/eth-json-rpc-provider@4.1.3 diff --git a/packages/eth-json-rpc-provider/package.json b/packages/eth-json-rpc-provider/package.json index d1ebc6f568c..6b791d90fdc 100644 --- a/packages/eth-json-rpc-provider/package.json +++ b/packages/eth-json-rpc-provider/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/eth-json-rpc-provider", - "version": "4.1.5", + "version": "4.1.6", "description": "Create an Ethereum provider using a JSON-RPC engine or middleware", "keywords": [ "MetaMask", @@ -52,7 +52,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/rpc-errors": "^7.0.1", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^10.0.0", diff --git a/packages/gas-fee-controller/package.json b/packages/gas-fee-controller/package.json index 2b31f681716..573ca692281 100644 --- a/packages/gas-fee-controller/package.json +++ b/packages/gas-fee-controller/package.json @@ -47,11 +47,11 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@metamask/eth-query": "^4.0.0", "@metamask/ethjs-unit": "^0.3.0", - "@metamask/polling-controller": "^12.0.0", + "@metamask/polling-controller": "^12.0.1", "@metamask/utils": "^10.0.0", "@types/bn.js": "^5.1.5", "@types/uuid": "^8.3.0", @@ -60,7 +60,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.0", + "@metamask/network-controller": "^22.0.1", "@types/jest": "^27.4.1", "@types/jest-when": "^2.7.3", "deepmerge": "^4.2.2", diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index 85d1fb8629c..08602c02b8d 100644 --- a/packages/json-rpc-engine/CHANGELOG.md +++ b/packages/json-rpc-engine/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.0.1] + +### Changed + +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) + ## [10.0.0] ### Fixed @@ -210,7 +216,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This change may affect consumers that depend on the eager execution of middleware _during_ request processing, _outside of_ middleware functions and request handlers. - In general, it is a bad practice to work with state that depends on middleware execution, while the middleware are executing. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.0.1...HEAD +[10.0.1]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.0.0...@metamask/json-rpc-engine@10.0.1 [10.0.0]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@9.0.3...@metamask/json-rpc-engine@10.0.0 [9.0.3]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@9.0.2...@metamask/json-rpc-engine@9.0.3 [9.0.2]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@9.0.1...@metamask/json-rpc-engine@9.0.2 diff --git a/packages/json-rpc-engine/package.json b/packages/json-rpc-engine/package.json index d512d0b1d4f..ae72df69334 100644 --- a/packages/json-rpc-engine/package.json +++ b/packages/json-rpc-engine/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/json-rpc-engine", - "version": "10.0.0", + "version": "10.0.1", "description": "A tool for processing JSON-RPC messages", "keywords": [ "MetaMask", diff --git a/packages/json-rpc-middleware-stream/CHANGELOG.md b/packages/json-rpc-middleware-stream/CHANGELOG.md index a55ef8e8b0b..e55eaebc423 100644 --- a/packages/json-rpc-middleware-stream/CHANGELOG.md +++ b/packages/json-rpc-middleware-stream/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.5] + +### Changed + +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) + ## [8.0.4] ### Fixed @@ -177,7 +183,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - TypeScript typings ([#11](https://github.com/MetaMask/json-rpc-middleware-stream/pull/11)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-middleware-stream@8.0.4...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-middleware-stream@8.0.5...HEAD +[8.0.5]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-middleware-stream@8.0.4...@metamask/json-rpc-middleware-stream@8.0.5 [8.0.4]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-middleware-stream@8.0.3...@metamask/json-rpc-middleware-stream@8.0.4 [8.0.3]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-middleware-stream@8.0.2...@metamask/json-rpc-middleware-stream@8.0.3 [8.0.2]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-middleware-stream@8.0.1...@metamask/json-rpc-middleware-stream@8.0.2 diff --git a/packages/json-rpc-middleware-stream/package.json b/packages/json-rpc-middleware-stream/package.json index aaea60214ee..c005be9b5a7 100644 --- a/packages/json-rpc-middleware-stream/package.json +++ b/packages/json-rpc-middleware-stream/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/json-rpc-middleware-stream", - "version": "8.0.4", + "version": "8.0.5", "description": "A small toolset for streaming JSON-RPC data and matching requests and responses", "keywords": [ "MetaMask", @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^10.0.0", "readable-stream": "^3.6.2" diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 5e152c33a6f..13d066a87ae 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [17.3.1] + +### Changed + +- Bump `@metamask/base-controller` from `^7.0.1` to `^7.0.2` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) +- Bump `@metamask/eth-sig-util` from `^7.0.1` to `^8.0.0` ([#4830](https://github.com/MetaMask/core/pull/4830)) + ## [17.3.0] ### Changed @@ -567,7 +575,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.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@17.3.1...HEAD +[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 [17.2.1]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@17.2.0...@metamask/keyring-controller@17.2.1 diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index f1fbbcc07f1..dbb01e8dbad 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/keyring-controller", - "version": "17.3.0", + "version": "17.3.1", "description": "Stores identities seen in the wallet and manages interactions such as signing", "keywords": [ "MetaMask", @@ -49,13 +49,13 @@ "dependencies": { "@ethereumjs/util": "^8.1.0", "@keystonehq/metamask-airgapped-keyring": "^0.14.1", - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/browser-passworder": "^4.3.0", "@metamask/eth-hd-keyring": "^7.0.4", "@metamask/eth-sig-util": "^8.0.0", "@metamask/eth-simple-keyring": "^6.0.5", "@metamask/keyring-api": "^8.1.3", - "@metamask/message-manager": "^11.0.0", + "@metamask/message-manager": "^11.0.1", "@metamask/utils": "^10.0.0", "async-mutex": "^0.5.0", "ethereumjs-wallet": "^1.0.1", diff --git a/packages/logging-controller/package.json b/packages/logging-controller/package.json index c6eaeeba677..6c375c895da 100644 --- a/packages/logging-controller/package.json +++ b/packages/logging-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/packages/message-manager/CHANGELOG.md b/packages/message-manager/CHANGELOG.md index 41d4c0aa6c2..29bfe9a4484 100644 --- a/packages/message-manager/CHANGELOG.md +++ b/packages/message-manager/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.0.1] + +### Changed + +- 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.3.0` to `^11.4.2` ([#4834](https://github.com/MetaMask/core/pull/4834), [#4862](https://github.com/MetaMask/core/pull/4862), [#4870](https://github.com/MetaMask/core/pull/4870)) +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) +- Bump `@metamask/eth-sig-util` from `^7.0.1` to `^8.0.0` ([#4830](https://github.com/MetaMask/core/pull/4830)) + ## [11.0.0] ### Removed @@ -325,7 +334,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/message-manager@11.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/message-manager@11.0.1...HEAD +[11.0.1]: https://github.com/MetaMask/core/compare/@metamask/message-manager@11.0.0...@metamask/message-manager@11.0.1 [11.0.0]: https://github.com/MetaMask/core/compare/@metamask/message-manager@10.1.1...@metamask/message-manager@11.0.0 [10.1.1]: https://github.com/MetaMask/core/compare/@metamask/message-manager@10.1.0...@metamask/message-manager@10.1.1 [10.1.0]: https://github.com/MetaMask/core/compare/@metamask/message-manager@10.0.3...@metamask/message-manager@10.1.0 diff --git a/packages/message-manager/package.json b/packages/message-manager/package.json index a1eb5f7a420..407eb2693e4 100644 --- a/packages/message-manager/package.json +++ b/packages/message-manager/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/message-manager", - "version": "11.0.0", + "version": "11.0.1", "description": "Stores and manages interactions with signing requests", "keywords": [ "MetaMask", @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@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 de3a33ec7cd..e604ea12fe1 100644 --- a/packages/name-controller/package.json +++ b/packages/name-controller/package.json @@ -48,8 +48,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@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 a1368fa7aba..f547dc5046c 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [22.0.1] + +### Changed + +- 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.2` ([#4862](https://github.com/MetaMask/core/pull/4862), [#4870](https://github.com/MetaMask/core/pull/4870)) +- Bump `@metamask/eth-json-rpc-provider` from `^4.1.5` to `^4.1.6` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/json-rpc-engine` from `^10.0.0` to `^10.0.1` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/rpc-errors` from `^7.0.0` to `^7.0.1` ([#4831](https://github.com/MetaMask/core/pull/4831)) + ## [22.0.0] ### Changed @@ -643,7 +653,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.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.0.1...HEAD +[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 [21.0.1]: https://github.com/MetaMask/core/compare/@metamask/network-controller@21.0.0...@metamask/network-controller@21.0.1 diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 7adeaaace0b..d6eea69342e 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/network-controller", - "version": "22.0.0", + "version": "22.0.1", "description": "Provides an interface to the currently selected network via a MetaMask-compatible provider object", "keywords": [ "MetaMask", @@ -47,14 +47,14 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@metamask/eth-block-tracker": "^11.0.2", "@metamask/eth-json-rpc-infura": "^10.0.0", "@metamask/eth-json-rpc-middleware": "^15.0.0", - "@metamask/eth-json-rpc-provider": "^4.1.5", + "@metamask/eth-json-rpc-provider": "^4.1.6", "@metamask/eth-query": "^4.0.0", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/rpc-errors": "^7.0.1", "@metamask/swappable-obj-proxy": "^2.2.0", "@metamask/utils": "^10.0.0", diff --git a/packages/notification-controller/package.json b/packages/notification-controller/package.json index ceaa76761de..68ac1c26a13 100644 --- a/packages/notification-controller/package.json +++ b/packages/notification-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/utils": "^10.0.0", "nanoid": "^3.1.31" }, diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index 93c715cee8a..100a82bdaf6 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -100,8 +100,8 @@ }, "dependencies": { "@contentful/rich-text-html-renderer": "^16.5.2", - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@metamask/utils": "^10.0.0", "bignumber.js": "^9.1.2", "firebase": "^10.11.0", @@ -111,7 +111,7 @@ "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^17.3.0", + "@metamask/keyring-controller": "^17.3.1", "@metamask/profile-sync-controller": "^0.9.7", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index 0547eae7a35..8253718050b 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.0.3] + +### Changed + +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) + ## [11.0.2] ### Fixed @@ -290,7 +296,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/permission-controller@11.0.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.3...HEAD +[11.0.3]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.2...@metamask/permission-controller@11.0.3 [11.0.2]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.1...@metamask/permission-controller@11.0.2 [11.0.1]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.0...@metamask/permission-controller@11.0.1 [11.0.0]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@10.0.1...@metamask/permission-controller@11.0.0 diff --git a/packages/permission-controller/package.json b/packages/permission-controller/package.json index 1f0b79f8ae0..332b5438899 100644 --- a/packages/permission-controller/package.json +++ b/packages/permission-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/permission-controller", - "version": "11.0.2", + "version": "11.0.3", "description": "Mediates access to JSON-RPC methods, used to interact with pieces of the MetaMask stack, via middleware for json-rpc-engine", "keywords": [ "MetaMask", @@ -47,9 +47,9 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", "@types/deep-freeze-strict": "^1.1.0", @@ -58,7 +58,7 @@ "nanoid": "^3.1.31" }, "devDependencies": { - "@metamask/approval-controller": "^7.1.0", + "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/permission-log-controller/package.json b/packages/permission-log-controller/package.json index 4cf259d9098..2e260568713 100644 --- a/packages/permission-log-controller/package.json +++ b/packages/permission-log-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/utils": "^10.0.0" }, "devDependencies": { diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json index d480aaf264a..f34e9ec7257 100644 --- a/packages/phishing-controller/package.json +++ b/packages/phishing-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@noble/hashes": "^1.4.0", "@types/punycode": "^2.1.0", "ethereum-cryptography": "^2.1.2", diff --git a/packages/polling-controller/CHANGELOG.md b/packages/polling-controller/CHANGELOG.md index 9ea1bf0c06e..77a780a8c68 100644 --- a/packages/polling-controller/CHANGELOG.md +++ b/packages/polling-controller/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.0.1] + +### Changed + +- 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.2` ([#4862](https://github.com/MetaMask/core/pull/4862), [#4870](https://github.com/MetaMask/core/pull/4870)) + ## [12.0.0] ### Changed @@ -203,7 +210,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/polling-controller@12.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/polling-controller@12.0.1...HEAD +[12.0.1]: https://github.com/MetaMask/core/compare/@metamask/polling-controller@12.0.0...@metamask/polling-controller@12.0.1 [12.0.0]: https://github.com/MetaMask/core/compare/@metamask/polling-controller@11.0.0...@metamask/polling-controller@12.0.0 [11.0.0]: https://github.com/MetaMask/core/compare/@metamask/polling-controller@10.0.1...@metamask/polling-controller@11.0.0 [10.0.1]: https://github.com/MetaMask/core/compare/@metamask/polling-controller@10.0.0...@metamask/polling-controller@10.0.1 diff --git a/packages/polling-controller/package.json b/packages/polling-controller/package.json index b9857c9ee95..aff01c539b9 100644 --- a/packages/polling-controller/package.json +++ b/packages/polling-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/polling-controller", - "version": "12.0.0", + "version": "12.0.1", "description": "Polling Controller is the base for controllers that polling by networkClientId", "keywords": [ "MetaMask", @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@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.0", + "@metamask/network-controller": "^22.0.1", "@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 fc4cfcd8342..5f61f716087 100644 --- a/packages/preferences-controller/CHANGELOG.md +++ b/packages/preferences-controller/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [13.2.0] + +### Added + +- Add `useSafeChainsListValidation` preference ([#4860](https://github.com/MetaMask/core/pull/4860)) + - Add `useSafeChainsListValidation` property to the `PreferencesController` state (default: `true`) + - Add `setUseSafeChainsListValidation` method to set this property +- Add `tokenSortConfig` preference ([#4860](https://github.com/MetaMask/core/pull/4860)) + - Add `tokenSortConfig` property to the `PreferencesController` state (default value: `{ key: 'tokenFiatAmount', order: 'dsc', sortCallback: 'stringNumeric' }`) + - Add `setTokenSortConfig` method to set this property +- Add `privacyMode` preference ([#4860](https://github.com/MetaMask/core/pull/4860)) + - Add `privacyMode` property to the `PreferencesController` state (default value: `false`) + - Add `setPrivacyMode` method to set this property +- Add `useMultiRpcMigration` preference ([#4732](https://github.com/MetaMask/core/pull/4732)) + +### Changed + +- 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.3.0` to `^11.4.2` ([#4834](https://github.com/MetaMask/core/pull/4834), [#4862](https://github.com/MetaMask/core/pull/4862), [#4870](https://github.com/MetaMask/core/pull/4870)) + ## [13.1.0] ### Changed @@ -288,7 +308,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/preferences-controller@13.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@13.2.0...HEAD +[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 [13.0.2]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@13.0.1...@metamask/preferences-controller@13.0.2 diff --git a/packages/preferences-controller/package.json b/packages/preferences-controller/package.json index 6be6bd14250..e64da21e791 100644 --- a/packages/preferences-controller/package.json +++ b/packages/preferences-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/preferences-controller", - "version": "13.1.0", + "version": "13.2.0", "description": "Manages user-configurable settings for MetaMask", "keywords": [ "MetaMask", @@ -47,12 +47,12 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0" + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^17.3.0", + "@metamask/keyring-controller": "^17.3.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/preferences-controller/src/PreferencesController.test.ts b/packages/preferences-controller/src/PreferencesController.test.ts index 9d9d4f05bcf..03d9332cc87 100644 --- a/packages/preferences-controller/src/PreferencesController.test.ts +++ b/packages/preferences-controller/src/PreferencesController.test.ts @@ -36,6 +36,13 @@ describe('PreferencesController', () => { return acc; }, {} as { [chainId in EtherscanSupportedHexChainId]: boolean }), smartTransactionsOptInStatus: false, + useSafeChainsListValidation: true, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + privacyMode: false, }); }); @@ -427,6 +434,45 @@ describe('PreferencesController', () => { controller.setUseTransactionSimulations(false); expect(controller.state.useTransactionSimulations).toBe(false); }); + + it('should set useSafeChainsListValidation', () => { + const controller = setupPreferencesController({ + options: { + state: { + useSafeChainsListValidation: false, + }, + }, + }); + expect(controller.state.useSafeChainsListValidation).toBe(false); + controller.setUseSafeChainsListValidation(true); + expect(controller.state.useSafeChainsListValidation).toBe(true); + }); + + it('should set tokenSortConfig', () => { + const controller = setupPreferencesController(); + expect(controller.state.tokenSortConfig).toStrictEqual({ + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }); + controller.setTokenSortConfig({ + key: 'someToken', + order: 'asc', + sortCallback: 'stringNumeric', + }); + expect(controller.state.tokenSortConfig).toStrictEqual({ + key: 'someToken', + order: 'asc', + sortCallback: 'stringNumeric', + }); + }); + + it('should set privacyMode', () => { + const controller = setupPreferencesController(); + expect(controller.state.privacyMode).toBe(false); + controller.setPrivacyMode(true); + expect(controller.state.privacyMode).toBe(true); + }); }); /** diff --git a/packages/preferences-controller/src/PreferencesController.ts b/packages/preferences-controller/src/PreferencesController.ts index 7f5815e7ccb..e67450caed3 100644 --- a/packages/preferences-controller/src/PreferencesController.ts +++ b/packages/preferences-controller/src/PreferencesController.ts @@ -44,6 +44,12 @@ export type EtherscanSupportedChains = export type EtherscanSupportedHexChainId = (typeof ETHERSCAN_SUPPORTED_CHAIN_IDS)[EtherscanSupportedChains]; +type TokenSortConfig = { + key: string; + order: 'asc' | 'dsc'; + sortCallback: string; +}; + /** * Preferences controller state */ @@ -114,6 +120,18 @@ export type PreferencesState = { * Controls whether Multi rpc modal is displayed or not */ useMultiRpcMigration: boolean; + /** + * Controls whether to use the safe chains list validation + */ + useSafeChainsListValidation: boolean; + /** + * Controls which order tokens are sorted in + */ + tokenSortConfig: TokenSortConfig; + /** + * Controls whether balance and assets are hidden or not + */ + privacyMode: boolean; }; const metadata = { @@ -133,6 +151,9 @@ const metadata = { smartTransactionsOptInStatus: { persist: true, anonymous: false }, useTransactionSimulations: { persist: true, anonymous: true }, useMultiRpcMigration: { persist: true, anonymous: true }, + useSafeChainsListValidation: { persist: true, anonymous: true }, + tokenSortConfig: { persist: true, anonymous: true }, + privacyMode: { persist: true, anonymous: true }, }; const name = 'PreferencesController'; @@ -166,7 +187,7 @@ export type PreferencesControllerMessenger = RestrictedControllerMessenger< * * @returns The default PreferencesController state. */ -export function getDefaultPreferencesState() { +export function getDefaultPreferencesState(): PreferencesState { return { featureFlags: {}, identities: {}, @@ -205,6 +226,13 @@ export function getDefaultPreferencesState() { useMultiRpcMigration: true, smartTransactionsOptInStatus: false, useTransactionSimulations: true, + useSafeChainsListValidation: true, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + privacyMode: false, }; } @@ -524,6 +552,39 @@ export class PreferencesController extends BaseController< state.useTransactionSimulations = useTransactionSimulations; }); } + + /** + * A setter to update the user's preferred token sorting order. + * + * @param tokenSortConfig - a configuration representing the sort order of tokens. + */ + setTokenSortConfig(tokenSortConfig: TokenSortConfig) { + this.update((state) => { + state.tokenSortConfig = tokenSortConfig; + }); + } + + /** + * A setter for the user preferences to enable/disable safe chains list validation. + * + * @param useSafeChainsListValidation - true to enable safe chains list validation, false to disable it. + */ + setUseSafeChainsListValidation(useSafeChainsListValidation: boolean) { + this.update((state) => { + state.useSafeChainsListValidation = useSafeChainsListValidation; + }); + } + + /** + * A setter for the user preferences to enable/disable privacy mode. + * + * @param privacyMode - true to enable privacy mode, false to disable it. + */ + setPrivacyMode(privacyMode: boolean) { + this.update((state) => { + state.privacyMode = privacyMode; + }); + } } export default PreferencesController; diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index 9fad268d396..5d2d6223db8 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -100,10 +100,10 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/keyring-api": "^8.1.3", - "@metamask/keyring-controller": "^17.3.0", - "@metamask/network-controller": "^22.0.0", + "@metamask/keyring-controller": "^17.3.1", + "@metamask/network-controller": "^22.0.1", "@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.2", + "@metamask/accounts-controller": "^18.2.3", "@metamask/auto-changelog": "^3.4.4", "@metamask/snaps-controllers": "^9.7.0", "@types/jest": "^27.4.1", diff --git a/packages/queued-request-controller/CHANGELOG.md b/packages/queued-request-controller/CHANGELOG.md index a8dace6f900..32283716c78 100644 --- a/packages/queued-request-controller/CHANGELOG.md +++ b/packages/queued-request-controller/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.0.0] + +### Added + +- **BREAKING:** The `QueuedRequestController` now requires the `canRequestSwitchNetworkWithoutApproval` callback in its constructor params. ([#4846](https://github.com/MetaMask/core/pull/4846)) + +### Changed + +- The `QueuedRequestController` now ensures that a request that can switch the globally selected network without approval is queued behind any existing pending requests. ([#4846](https://github.com/MetaMask/core/pull/4846)) + +### Fixed + +- The `QueuedRequestController` now ensures that any queued requests for a origin are failed if a request that can switch the globally selected network without approval actually does change the globally selected network for that origin. ([#4846](https://github.com/MetaMask/core/pull/4846)) + ## [6.0.0] ### Changed @@ -277,7 +291,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@6.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@7.0.0...HEAD +[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 [5.0.1]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@5.0.0...@metamask/queued-request-controller@5.0.1 diff --git a/packages/queued-request-controller/package.json b/packages/queued-request-controller/package.json index 32e519f65e0..7486e56cb6b 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": "6.0.0", + "version": "7.0.0", "description": "Includes a controller and middleware that implements a request queue", "keywords": [ "MetaMask", @@ -47,16 +47,16 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/rpc-errors": "^7.0.1", "@metamask/swappable-obj-proxy": "^2.2.0", "@metamask/utils": "^10.0.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.0", + "@metamask/network-controller": "^22.0.1", "@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 30c8ffbdfcf..3df072ad7ee 100644 --- a/packages/queued-request-controller/src/QueuedRequestController.test.ts +++ b/packages/queued-request-controller/src/QueuedRequestController.test.ts @@ -25,6 +25,7 @@ describe('QueuedRequestController', () => { const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(), shouldRequestSwitchNetwork: () => false, + canRequestSwitchNetworkWithoutApproval: () => false, clearPendingConfirmations: jest.fn(), showApprovalRequest: jest.fn(), }; @@ -222,6 +223,40 @@ describe('QueuedRequestController', () => { expect(mockShowApprovalRequest).toHaveBeenCalledTimes(1); }); + it('queues request if a requests are already being processed on the same origin, but canRequestSwitchNetworkWithoutApproval returns true', async () => { + const controller = buildQueuedRequestController({ + canRequestSwitchNetworkWithoutApproval: jest + .fn() + .mockImplementation( + (request) => + request.method === 'method_can_switch_network_without_approval', + ), + }); + // Trigger first request + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://sameorigin.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); + + const secondRequestNext = jest.fn(); + const secondRequest = controller.enqueueRequest( + { + ...buildRequest(), + origin: 'https://sameorigin.metamask.io', + method: 'method_can_switch_network_without_approval', + }, + secondRequestNext, + ); + + expect(controller.state.queuedRequestCount).toBe(1); + expect(secondRequestNext).not.toHaveBeenCalled(); + + await firstRequest; + await secondRequest; + }); + it('drains batch from queue when current batch finishes', async () => { const controller = buildQueuedRequestController(); // Trigger first batch @@ -1063,6 +1098,146 @@ describe('QueuedRequestController', () => { }); }); + describe('when the first request in a batch can switch the network', () => { + it('waits on processing the request first in the current batch', async () => { + const { messenger } = buildControllerMessenger({ + networkControllerGetState: jest.fn().mockReturnValue({ + ...getDefaultNetworkControllerState(), + selectedNetworkClientId: 'mainnet', + }), + }); + const controller = buildQueuedRequestController({ + messenger: buildQueuedRequestControllerMessenger(messenger), + canRequestSwitchNetworkWithoutApproval: jest + .fn() + .mockImplementation( + (request) => + request.method === 'method_can_switch_network_without_approval', + ), + }); + + const firstRequest = controller.enqueueRequest( + buildRequest(), + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); + + const secondRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + const secondRequest = controller.enqueueRequest( + { + ...buildRequest(), + + method: 'method_can_switch_network_without_approval', + }, + secondRequestNext, + ); + + const thirdRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + const thirdRequest = controller.enqueueRequest( + buildRequest(), + thirdRequestNext, + ); + + // ensure test starts with two requests queued up + expect(controller.state.queuedRequestCount).toBe(2); + expect(secondRequestNext).not.toHaveBeenCalled(); + expect(thirdRequestNext).not.toHaveBeenCalled(); + + // does not call the third request yet since it + // should be waiting for the second to complete + await firstRequest; + await secondRequest; + expect(secondRequestNext).toHaveBeenCalled(); + expect(thirdRequestNext).not.toHaveBeenCalled(); + + await thirdRequest; + expect(thirdRequestNext).toHaveBeenCalled(); + }); + + it('flushes the queue for the origin if the request changes the network', async () => { + const networkControllerGetState = jest.fn().mockReturnValue({ + ...getDefaultNetworkControllerState(), + selectedNetworkClientId: 'mainnet', + }); + const { messenger } = buildControllerMessenger({ + networkControllerGetState, + }); + const controller = buildQueuedRequestController({ + messenger: buildQueuedRequestControllerMessenger(messenger), + canRequestSwitchNetworkWithoutApproval: jest + .fn() + .mockImplementation( + (request) => + request.method === 'method_can_switch_network_without_approval', + ), + }); + + // no switch required + const firstRequest = controller.enqueueRequest( + buildRequest(), + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); + + const secondRequestNext = jest.fn().mockImplementation( + () => + new Promise((resolve) => { + networkControllerGetState.mockReturnValue({ + ...getDefaultNetworkControllerState(), + selectedNetworkClientId: 'newNetworkClientId', + }); + resolve(undefined); + }), + ); + const secondRequest = controller.enqueueRequest( + { + ...buildRequest(), + method: 'method_can_switch_network_without_approval', + }, + secondRequestNext, + ); + + const thirdRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + const thirdRequest = controller.enqueueRequest( + buildRequest(), + thirdRequestNext, + ); + + // ensure test starts with two requests queued up + expect(controller.state.queuedRequestCount).toBe(2); + expect(secondRequestNext).not.toHaveBeenCalled(); + expect(thirdRequestNext).not.toHaveBeenCalled(); + + // does not call the third request yet since it + // should not be in the same batch as the second + await firstRequest; + await secondRequest; + expect(secondRequestNext).toHaveBeenCalled(); + expect(thirdRequestNext).not.toHaveBeenCalled(); + + 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.', + ), + ); + expect(thirdRequestNext).not.toHaveBeenCalled(); + }); + }); + describe('when a request fails', () => { it('throws error', async () => { const controller = buildQueuedRequestController(); @@ -1146,6 +1321,7 @@ describe('QueuedRequestController', () => { messenger: buildQueuedRequestControllerMessenger(messenger), shouldRequestSwitchNetwork: ({ method }) => method === 'eth_sendTransaction', + canRequestSwitchNetworkWithoutApproval: () => false, clearPendingConfirmations: jest.fn(), showApprovalRequest: jest.fn(), }; @@ -1229,6 +1405,7 @@ describe('QueuedRequestController', () => { messenger: buildQueuedRequestControllerMessenger(messenger), shouldRequestSwitchNetwork: ({ method }) => method === 'eth_sendTransaction', + canRequestSwitchNetworkWithoutApproval: () => false, clearPendingConfirmations: jest.fn(), showApprovalRequest: jest.fn(), }; @@ -1352,6 +1529,7 @@ function buildQueuedRequestController( const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(), shouldRequestSwitchNetwork: () => false, + canRequestSwitchNetworkWithoutApproval: () => false, clearPendingConfirmations: jest.fn(), showApprovalRequest: jest.fn(), ...overrideOptions, diff --git a/packages/queued-request-controller/src/QueuedRequestController.ts b/packages/queued-request-controller/src/QueuedRequestController.ts index 0f35ffdfdd6..4004f1604e8 100644 --- a/packages/queued-request-controller/src/QueuedRequestController.ts +++ b/packages/queued-request-controller/src/QueuedRequestController.ts @@ -79,6 +79,9 @@ export type QueuedRequestControllerOptions = { shouldRequestSwitchNetwork: ( request: QueuedRequestMiddlewareJsonRpcRequest, ) => boolean; + canRequestSwitchNetworkWithoutApproval: ( + request: QueuedRequestMiddlewareJsonRpcRequest, + ) => boolean; clearPendingConfirmations: () => void; showApprovalRequest: () => void; }; @@ -88,19 +91,19 @@ export type QueuedRequestControllerOptions = { */ type QueuedRequest = { /** - * The origin of the queued request. + * The request being queued. */ - origin: string; + request: QueuedRequestMiddlewareJsonRpcRequest; /** - * The networkClientId of the queuedRequest. + * A callback used to continue processing the request, called when the request is dequeued. */ - networkClientId: NetworkClientId; + processRequest: (error?: unknown) => void; /** - * A callback used to continue processing the request, called when the request is dequeued. + * A deferred promise that resolves when the request is processed. */ - processRequest: (error: unknown) => void; + requestHasBeenProcessed: Promise; }; /** @@ -159,6 +162,18 @@ export class QueuedRequestController extends BaseController< request: QueuedRequestMiddlewareJsonRpcRequest, ) => boolean; + /** + * This is a function that returns true if a request can change the + * globally selected network without prompting the user for approval. + * This is necessary to prevent UI/UX problems that can arise when methods + * change the globally selected network without prompting the user as the + * QueuedRequestController must clear any queued requests that come after + * the request that changed the globally selected network. + */ + readonly #canRequestSwitchNetworkWithoutApproval: ( + request: QueuedRequestMiddlewareJsonRpcRequest, + ) => boolean; + /** * This is a function that clears all pending confirmations across * several controllers that may handle them. @@ -177,6 +192,7 @@ export class QueuedRequestController extends BaseController< * @param options - Controller options. * @param options.messenger - The restricted controller messenger that facilitates communication with other controllers. * @param options.shouldRequestSwitchNetwork - A function that returns if a request requires the globally selected network to match the dapp selected network. + * @param options.canRequestSwitchNetworkWithoutApproval - A function that returns if a request will switch the globally selected network without prompting for user approval. * @param options.clearPendingConfirmations - A function that will clear all the pending confirmations. * @param options.showApprovalRequest - A function for opening the UI such that * the existing request can be displayed to the user. @@ -184,6 +200,7 @@ export class QueuedRequestController extends BaseController< constructor({ messenger, shouldRequestSwitchNetwork, + canRequestSwitchNetworkWithoutApproval, clearPendingConfirmations, showApprovalRequest, }: QueuedRequestControllerOptions) { @@ -200,6 +217,8 @@ export class QueuedRequestController extends BaseController< }); this.#shouldRequestSwitchNetwork = shouldRequestSwitchNetwork; + this.#canRequestSwitchNetworkWithoutApproval = + canRequestSwitchNetworkWithoutApproval; this.#clearPendingConfirmations = clearPendingConfirmations; this.#showApprovalRequest = showApprovalRequest; this.#registerMessageHandlers(); @@ -238,7 +257,7 @@ export class QueuedRequestController extends BaseController< // we intend to remove queueing for multichain requests in the future, so for now we have to live with this. #flushQueueForOrigin(flushOrigin: string) { this.#requestQueue - .filter(({ origin }) => origin === flushOrigin) + .filter(({ request }) => request.origin === flushOrigin) .forEach(({ processRequest }) => { processRequest( new Error( @@ -247,7 +266,7 @@ export class QueuedRequestController extends BaseController< ); }); this.#requestQueue = this.#requestQueue.filter( - ({ origin }) => origin !== flushOrigin, + ({ request }) => request.origin !== flushOrigin, ); } @@ -263,30 +282,66 @@ export class QueuedRequestController extends BaseController< */ async #processNextBatch() { const firstRequest = this.#requestQueue.shift() as QueuedRequest; - this.#originOfCurrentBatch = firstRequest.origin; - this.#networkClientIdOfCurrentBatch = firstRequest.networkClientId; - const batch = [firstRequest.processRequest]; + this.#originOfCurrentBatch = firstRequest.request.origin; + this.#networkClientIdOfCurrentBatch = firstRequest.request.networkClientId; + const batch = [firstRequest]; + + let networkSwitchError: unknown; + try { + // If globally selected network is different from origin selected network, + // switch network before processing batch + await this.#switchNetworkIfNecessary( + firstRequest.request.networkClientId, + ); + } catch (error: unknown) { + networkSwitchError = error; + } + + // If the first request might switch the network, process the request by + // itself. If the request does change the network, clear the queue for the + // origin since it any remaining requests are now invalidated + if (this.#canRequestSwitchNetworkWithoutApproval(firstRequest.request)) { + // This hack prevents the next batch from being processed + // after this request returns. This is necessary because + // we may need to flush the queue before the next set of requests + // are batched and processed, which we cannot do without blocking + // the queue from continuing by artificially increasing the processing + // request count + this.#processingRequestCount += 1; + try { + firstRequest.processRequest(networkSwitchError); + this.#updateQueuedRequestCount(); + await firstRequest.requestHasBeenProcessed; + } finally { + this.#processingRequestCount -= 1; + } + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + if (this.#networkClientIdOfCurrentBatch !== selectedNetworkClientId) { + this.#flushQueueForOrigin(this.#originOfCurrentBatch); + } + // Re-trigger processing of next batch because the `this.#processingRequestCount` guard above + // prevents it from being triggered when it typically would, after the request resolves. + this.#processNextBatchIfReady(); + return; + } // alternatively we could still batch by only origin but switch networks in batches by // adding the network clientId to the values in the batch array while ( - this.#requestQueue[0]?.networkClientId === + this.#requestQueue[0]?.request.networkClientId === this.#networkClientIdOfCurrentBatch && - this.#requestQueue[0]?.origin === this.#originOfCurrentBatch + this.#requestQueue[0]?.request.origin === this.#originOfCurrentBatch && + !this.#canRequestSwitchNetworkWithoutApproval( + this.#requestQueue[0]?.request, + ) ) { const nextEntry = this.#requestQueue.shift() as QueuedRequest; - batch.push(nextEntry.processRequest); - } - // If globally selected network is different from origin selected network, - // switch network before processing batch - let networkSwitchError: unknown; - try { - await this.#switchNetworkIfNecessary(firstRequest.networkClientId); - } catch (error: unknown) { - networkSwitchError = error; + batch.push(nextEntry); } - for (const processRequest of batch) { + for (const { processRequest } of batch) { processRequest(networkSwitchError); } this.#updateQueuedRequestCount(); @@ -329,30 +384,60 @@ export class QueuedRequestController extends BaseController< }); } - async #waitForDequeue({ - origin, - networkClientId, - }: { - origin: string; - networkClientId: NetworkClientId; - }): Promise { - const { promise, reject, resolve } = createDeferredPromise({ + /** + * Adds a request to the queue to be processed. A promise is returned that resolves/rejects when + * this request should continue execution/fail early. Additionally it returns a callback that + * must be called after the request finishes execution. + * + * Internally, the controller triggers the above returned promise to resolve via the `processRequest`. + * + * @param request - The JSON-RPC request to process. + * @returns A promise resolves on dequeue and callback to notify request completion. + */ + #waitForDequeue(request: QueuedRequestMiddlewareJsonRpcRequest) { + const { + promise: dequeuedPromise, + reject, + resolve, + } = createDeferredPromise({ suppressUnhandledRejection: true, }); + const { promise: requestHasBeenProcessed, resolve: requestHasEnded } = + createDeferredPromise({ + suppressUnhandledRejection: true, + }); this.#requestQueue.push({ - origin, - networkClientId, - processRequest: (error: unknown) => { + request, + processRequest: (error?: unknown) => { if (error) { reject(error); } else { resolve(); } }, + requestHasBeenProcessed, }); this.#updateQueuedRequestCount(); - return promise; + return { dequeuedPromise, requestHasEnded }; + } + + /** + * Prepares controller state for the next batch if the current + * batch is completed and starts processing the next batch if + * there are requests left in the queue. + */ + #processNextBatchIfReady() { + if (this.#processingRequestCount === 0) { + this.#originOfCurrentBatch = undefined; + this.#networkClientIdOfCurrentBatch = undefined; + if (this.#requestQueue.length > 0) { + // The next batch is triggered here. We intentionally omit the `await` because we don't + // want the next batch to block resolution of the current request. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.#processNextBatch(); + } + } } /** @@ -387,18 +472,37 @@ export class QueuedRequestController extends BaseController< } try { + let requestHasEnded: (() => void) | undefined; + + // This case exists because request with methods like + // wallet_addEthereumChain and wallet_switchEthereumChain + // have the potential to change the globally selected network + // without prompting for user approval. When there are existing + // processing requests and a new request for one of the methods + // above is not queued but instead allowed to execute immediately + // and change the globally selected network, all existing processing + // requests get cleared. It is not obvious to the user why those + // requests were cleared as the new wallet_addEthereumChain or + // wallet_switchEthereumChain request may not have an + // associated approval with it. To deal with this potential + // edge case, we always queue these type of requests if there + // are existing requests still being processed. + const requestCouldClearProcessingBatchWithoutApproval = + this.#processingRequestCount > 0 && + this.#canRequestSwitchNetworkWithoutApproval(request); + // Queue request for later processing // Network switch is handled when this batch is processed if ( this.state.queuedRequestCount > 0 || this.#originOfCurrentBatch !== request.origin || - this.#networkClientIdOfCurrentBatch !== request.networkClientId + this.#networkClientIdOfCurrentBatch !== request.networkClientId || + requestCouldClearProcessingBatchWithoutApproval ) { this.#showApprovalRequest(); - await this.#waitForDequeue({ - origin: request.origin, - networkClientId: request.networkClientId, - }); + const dequeue = this.#waitForDequeue(request); + requestHasEnded = dequeue.requestHasEnded; + await dequeue.dequeuedPromise; } else if (this.#shouldRequestSwitchNetwork(request)) { // Process request immediately // Requires switching network now if necessary @@ -408,20 +512,12 @@ export class QueuedRequestController extends BaseController< try { await requestNext(); } finally { + requestHasEnded?.(); this.#processingRequestCount -= 1; } return undefined; } finally { - if (this.#processingRequestCount === 0) { - this.#originOfCurrentBatch = undefined; - this.#networkClientIdOfCurrentBatch = undefined; - if (this.#requestQueue.length > 0) { - // The next batch is triggered here. We intentionally omit the `await` because we don't - // want the next batch to block resolution of the current request. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.#processNextBatch(); - } - } + this.#processNextBatchIfReady(); } } } diff --git a/packages/rate-limit-controller/package.json b/packages/rate-limit-controller/package.json index a76efaa1e28..4e91e3eb7f7 100644 --- a/packages/rate-limit-controller/package.json +++ b/packages/rate-limit-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", + "@metamask/base-controller": "^7.0.2", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0" }, diff --git a/packages/selected-network-controller/package.json b/packages/selected-network-controller/package.json index bf85929b80a..43f60c384a4 100644 --- a/packages/selected-network-controller/package.json +++ b/packages/selected-network-controller/package.json @@ -47,15 +47,15 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/swappable-obj-proxy": "^2.2.0", "@metamask/utils": "^10.0.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.0", - "@metamask/permission-controller": "^11.0.2", + "@metamask/network-controller": "^22.0.1", + "@metamask/permission-controller": "^11.0.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "immer": "^9.0.6", diff --git a/packages/signature-controller/CHANGELOG.md b/packages/signature-controller/CHANGELOG.md index 18ecb23af4a..cfac8b24f92 100644 --- a/packages/signature-controller/CHANGELOG.md +++ b/packages/signature-controller/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [21.0.0] + +### Added + +- Add `chainId` and `networkClientId` to `SignatureRequest` and `LegacyStateMessage` types ([#4797](https://github.com/MetaMask/core/pull/4797)) +- Add `networkClientId` to `OriginalRequest` type ([#4797](https://github.com/MetaMask/core/pull/4797)) + +### Changed + +- **BREAKING:** Make `request` argument required in `newUnsignedPersonalMessage` and `newUnsignedTypedMessage` methods ([#4797](https://github.com/MetaMask/core/pull/4797)) +- Throw if no `networkClientId` in `request` or if chain ID cannot be determined ([#4797](https://github.com/MetaMask/core/pull/4797)) +- Bump `@metamask/approval-controller` from `^7.1.0` to `^7.1.1` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/controller-utils` from `^11.4.0` to `^11.4.1` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/base-controller` from `7.0.1` to `^7.0.2` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/utils` from `^9.1.0` to `^10.0.0` ([#4831](https://github.com/MetaMask/core/pull/4831)) +- Bump `@metamask/controller-utils` from `^11.3.0` to `^11.4.0` ([#4834](https://github.com/MetaMask/core/pull/4834)) + +### Removed + +- Remove `getCurrentChainId` and `getAllState` callbacks from constructor options ([#4797](https://github.com/MetaMask/core/pull/4797)) + ## [20.1.0] ### Added @@ -379,7 +400,8 @@ 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@20.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@21.0.0...HEAD +[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 [19.1.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@19.0.0...@metamask/signature-controller@19.1.0 diff --git a/packages/signature-controller/package.json b/packages/signature-controller/package.json index 3f4b774873c..bfa14109ba0 100644 --- a/packages/signature-controller/package.json +++ b/packages/signature-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/signature-controller", - "version": "20.1.0", + "version": "21.0.0", "description": "Processes signing requests in order to sign arbitrary and typed data", "keywords": [ "MetaMask", @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@metamask/eth-sig-util": "^8.0.0", "@metamask/utils": "^10.0.0", "jsonschema": "^1.2.4", @@ -56,11 +56,11 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/approval-controller": "^7.1.0", + "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^17.3.0", + "@metamask/keyring-controller": "^17.3.1", "@metamask/logging-controller": "^6.0.1", - "@metamask/network-controller": "^22.0.0", + "@metamask/network-controller": "^22.0.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 3fd7e33c7f6..ac9999f5238 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [38.1.0] + +### Added + +- Automatically re-simulate transactions based on security criteria ([#4792](https://github.com/MetaMask/core/pull/4792)) + - If the security provider marks the transaction as malicious. + - If the simulated native balance change does not match the `value`. + - Set new `isUpdatedAfterSecurityCheck` property to `true` if the subsequent simulation response has changed. + +### Changed + +- Bump `@metamask/eth-json-rpc-provider` from `^4.1.5` to `^4.1.6` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/approval-controller` from `^7.1.0` to `^7.1.1` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/controller-utils` from `^11.4.0` to `^11.4.1` ([#4862](https://github.com/MetaMask/core/pull/4862)) +- Bump `@metamask/base-controller` from `7.0.1` to `^7.0.2` ([#4862](https://github.com/MetaMask/core/pull/4862)) + ## [38.0.0] ### Changed @@ -1072,7 +1088,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@38.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@38.1.0...HEAD +[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 [37.3.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@37.2.0...@metamask/transaction-controller@37.3.0 [37.2.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@37.1.0...@metamask/transaction-controller@37.2.0 diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index c618be15548..d84ee83366b 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 94.42, - functions: 97.45, - lines: 98.37, - statements: 98.38, + branches: 93.74, + functions: 97.51, + lines: 98.34, + statements: 98.35, }, }, diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 1db02a71a72..0c09c02d6fa 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/transaction-controller", - "version": "38.0.0", + "version": "38.1.0", "description": "Stores transactions alongside their periodically updated statuses and manages interactions such as approval and cancellation", "keywords": [ "MetaMask", @@ -53,8 +53,8 @@ "@ethersproject/abi": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@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.2", - "@metamask/approval-controller": "^7.1.0", + "@metamask/accounts-controller": "^18.2.3", + "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", - "@metamask/eth-json-rpc-provider": "^4.1.5", + "@metamask/eth-json-rpc-provider": "^4.1.6", "@metamask/ethjs-provider-http": "^0.3.0", "@metamask/gas-fee-controller": "^22.0.0", "@metamask/keyring-api": "^8.1.3", - "@metamask/network-controller": "^22.0.0", + "@metamask/network-controller": "^22.0.1", "@types/bn.js": "^5.1.5", "@types/jest": "^27.4.1", "@types/node": "^16.18.54", diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 030703f8cba..a729dbc9a39 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -87,6 +87,7 @@ import { getTransactionLayer1GasFee, updateTransactionLayer1GasFee, } from './utils/layer1-gas-fee-flow'; +import { shouldResimulate } from './utils/resimulate'; import { getSimulationData } from './utils/simulation'; import { updatePostTransactionBalance, @@ -112,9 +113,10 @@ jest.mock('./helpers/PendingTransactionTracker'); jest.mock('./utils/gas'); jest.mock('./utils/gas-fees'); jest.mock('./utils/gas-flow'); -jest.mock('./utils/swaps'); jest.mock('./utils/layer1-gas-fee-flow'); +jest.mock('./utils/resimulate'); jest.mock('./utils/simulation'); +jest.mock('./utils/swaps'); jest.mock('uuid'); // TODO: Replace `any` with type @@ -485,6 +487,7 @@ describe('TransactionController', () => { getTransactionLayer1GasFee, ); const getGasFeeFlowMock = jest.mocked(getGasFeeFlow); + const shouldResimulateMock = jest.mocked(shouldResimulate); let mockEthQuery: EthQuery; let getNonceLockSpy: jest.Mock; @@ -1829,13 +1832,18 @@ describe('TransactionController', () => { await flushPromises(); expect(getSimulationDataMock).toHaveBeenCalledTimes(1); - expect(getSimulationDataMock).toHaveBeenCalledWith({ - chainId: MOCK_NETWORK.chainId, - data: undefined, - from: ACCOUNT_MOCK, - to: ACCOUNT_MOCK, - value: '0x0', - }); + expect(getSimulationDataMock).toHaveBeenCalledWith( + { + chainId: MOCK_NETWORK.chainId, + data: undefined, + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + value: '0x0', + }, + { + blockTime: undefined, + }, + ); expect(controller.state.transactions[0].simulationData).toStrictEqual( SIMULATION_DATA_MOCK, @@ -5667,35 +5675,6 @@ describe('TransactionController', () => { .toThrow(`TransactionsController: Can only call updateEditableParams on an unapproved transaction. Current tx status: ${TransactionStatus.submitted}`); }); - - it.each(['value', 'to', 'data'])( - 'updates simulation data if %s changes', - async (param) => { - const { controller } = setupController({ - options: { - state: { - transactions: [ - { - ...transactionMeta, - }, - ], - }, - }, - updateToInitialState: true, - }); - - expect(getSimulationDataMock).toHaveBeenCalledTimes(0); - - await controller.updateEditableParams(transactionMeta.id, { - ...transactionMeta.txParams, - [param]: ACCOUNT_2_MOCK, - }); - - await flushPromises(); - - expect(getSimulationDataMock).toHaveBeenCalledTimes(1); - }, - ); }); describe('abortTransactionSigning', () => { @@ -5826,4 +5805,84 @@ describe('TransactionController', () => { ); }); }); + + describe('resimulate', () => { + it('triggers simulation if re-simulation detected on state update', async () => { + const { controller } = setupController({ + options: { + state: { + transactions: [ + { + ...TRANSACTION_META_MOCK, + status: TransactionStatus.unapproved, + }, + ], + }, + }, + updateToInitialState: true, + }); + + expect(getSimulationDataMock).toHaveBeenCalledTimes(0); + + shouldResimulateMock.mockReturnValueOnce({ + blockTime: 123, + resimulate: true, + }); + + await controller.updateEditableParams(TRANSACTION_META_MOCK.id, {}); + + await flushPromises(); + + expect(getSimulationDataMock).toHaveBeenCalledTimes(1); + expect(getSimulationDataMock).toHaveBeenCalledWith( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + value: TRANSACTION_META_MOCK.txParams.value, + }, + { + blockTime: 123, + }, + ); + }); + + it('does not trigger simulation loop', async () => { + const { controller } = setupController({ + options: { + state: { + transactions: [ + { + ...TRANSACTION_META_MOCK, + status: TransactionStatus.unapproved, + }, + ], + }, + }, + updateToInitialState: true, + }); + + expect(getSimulationDataMock).toHaveBeenCalledTimes(0); + + shouldResimulateMock.mockReturnValue({ + blockTime: 123, + resimulate: true, + }); + + await controller.updateEditableParams(TRANSACTION_META_MOCK.id, {}); + + await flushPromises(); + + expect(getSimulationDataMock).toHaveBeenCalledTimes(1); + expect(getSimulationDataMock).toHaveBeenCalledWith( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + value: TRANSACTION_META_MOCK.txParams.value, + }, + { + blockTime: 123, + }, + ); + }); + }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index eec71928213..64fb2d3064b 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -49,7 +49,7 @@ import { add0x } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import { MethodRegistry } from 'eth-method-registry'; import { EventEmitter } from 'events'; -import { cloneDeep, mapValues, merge, pickBy, sortBy, isEqual } from 'lodash'; +import { cloneDeep, mapValues, merge, pickBy, sortBy } from 'lodash'; import { v1 as random } from 'uuid'; import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow'; @@ -105,6 +105,8 @@ import { getAndFormatTransactionsForNonceTracker, getNextNonce, } from './utils/nonce'; +import type { ResimulateResponse } from './utils/resimulate'; +import { hasSimulationDataChanged, shouldResimulate } from './utils/resimulate'; import { getTransactionParamsWithIncreasedGasFee } from './utils/retry'; import { getSimulationData } from './utils/simulation'; import { @@ -3546,15 +3548,17 @@ export class TransactionController extends BaseController< note, skipHistory, skipValidation, + skipResimulateCheck, }: { transactionId: string; note?: string; skipHistory?: boolean; skipValidation?: boolean; + skipResimulateCheck?: boolean; }, callback: (transactionMeta: TransactionMeta) => TransactionMeta | void, ): Readonly { - let updatedTransactionParams: (keyof TransactionParams)[] = []; + let resimulateResponse: ResimulateResponse | undefined; this.update((state) => { const index = state.transactions.findIndex( @@ -3563,6 +3567,8 @@ export class TransactionController extends BaseController< let transactionMeta = state.transactions[index]; + const originalTransactionMeta = cloneDeep(transactionMeta); + // eslint-disable-next-line n/callback-return transactionMeta = callback(transactionMeta) ?? transactionMeta; @@ -3574,8 +3580,12 @@ export class TransactionController extends BaseController< validateTxParams(transactionMeta.txParams); } - updatedTransactionParams = - this.#checkIfTransactionParamsUpdated(transactionMeta); + if (!skipResimulateCheck && this.#isSimulationEnabled()) { + resimulateResponse = shouldResimulate( + originalTransactionMeta, + transactionMeta, + ); + } const shouldSkipHistory = this.isHistoryDisabled || skipHistory; @@ -3592,64 +3602,35 @@ export class TransactionController extends BaseController< transactionId, ) as TransactionMeta; - if (updatedTransactionParams.length > 0) { - this.#onTransactionParamsUpdated( - transactionMeta, - updatedTransactionParams, - ); - } - - return transactionMeta; - } - - #checkIfTransactionParamsUpdated(newTransactionMeta: TransactionMeta) { - const { id: transactionId, txParams: newParams } = newTransactionMeta; - - const originalParams = this.getTransaction(transactionId)?.txParams; - - if (!originalParams || isEqual(originalParams, newParams)) { - return []; - } - - const params = Object.keys(newParams) as (keyof TransactionParams)[]; - - const updatedProperties = params.filter( - (param) => newParams[param] !== originalParams[param], - ); - - log( - 'Transaction parameters have been updated', - transactionId, - updatedProperties, - originalParams, - newParams, - ); - - return updatedProperties; - } - - #onTransactionParamsUpdated( - transactionMeta: TransactionMeta, - updatedParams: (keyof TransactionParams)[], - ) { - if ( - (['to', 'value', 'data'] as const).some((param) => - updatedParams.includes(param), - ) - ) { - log('Updating simulation data due to transaction parameter update'); - this.#updateSimulationData(transactionMeta).catch((error) => { - log('Error updating simulation data', error); + if (resimulateResponse?.resimulate) { + this.#updateSimulationData(transactionMeta, { + blockTime: resimulateResponse.blockTime, + }).catch((error) => { + log('Error during re-simulation', error); throw error; }); } + + return transactionMeta; } async #updateSimulationData( transactionMeta: TransactionMeta, - { traceContext }: { traceContext?: TraceContext } = {}, + { + blockTime, + traceContext, + }: { + blockTime?: number; + traceContext?: TraceContext; + } = {}, ) { - const { id: transactionId, chainId, txParams } = transactionMeta; + const { + id: transactionId, + chainId, + txParams, + simulationData: prevSimulationData, + } = transactionMeta; + const { from, to, value, data } = txParams; let simulationData: SimulationData = { @@ -3661,24 +3642,33 @@ export class TransactionController extends BaseController< }; if (this.#isSimulationEnabled()) { - this.#updateTransactionInternal( - { transactionId, skipHistory: true }, - (txMeta) => { - txMeta.simulationData = undefined; - }, - ); - simulationData = await this.#trace( { name: 'Simulate', parentContext: traceContext }, () => - getSimulationData({ - chainId, - from: from as Hex, - to: to as Hex, - value: value as Hex, - data: data as Hex, - }), + getSimulationData( + { + chainId, + from: from as Hex, + to: to as Hex, + value: value as Hex, + data: data as Hex, + }, + { + blockTime, + }, + ), ); + + if ( + blockTime && + prevSimulationData && + hasSimulationDataChanged(prevSimulationData, simulationData) + ) { + simulationData = { + ...simulationData, + isUpdatedAfterSecurityCheck: true, + }; + } } const finalTransactionMeta = this.getTransaction(transactionId); @@ -3698,6 +3688,7 @@ export class TransactionController extends BaseController< { transactionId, note: 'TransactionController#updateSimulationData - Update simulation data', + skipResimulateCheck: Boolean(blockTime), }, (txMeta) => { txMeta.simulationData = simulationData; diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index d013817be01..f70679a6bc8 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -542,6 +542,7 @@ export enum WalletDevice { OTHER = 'other_device', } +/* eslint-disable @typescript-eslint/naming-convention */ /** * The type of the transaction. */ @@ -549,8 +550,6 @@ export enum TransactionType { /** * A transaction that bridges tokens to a different chain through Metamask Bridge. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention bridge = 'bridge', /** @@ -559,15 +558,11 @@ export enum TransactionType { * of the user for the MetaMask Bridge contract. The first bridge for any token * will have an accompanying bridgeApproval transaction. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention bridgeApproval = 'bridgeApproval', /** * A transaction sending a network's native asset to a recipient. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention cancel = 'cancel', /** @@ -575,43 +570,31 @@ export enum TransactionType { * have not treated as a special case, such as approve, transfer, and * transferfrom. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention contractInteraction = 'contractInteraction', /** * A transaction that deployed a smart contract. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention deployContract = 'contractDeployment', /** * A transaction for Ethereum decryption. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention ethDecrypt = 'eth_decrypt', /** * A transaction for getting an encryption public key. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention ethGetEncryptionPublicKey = 'eth_getEncryptionPublicKey', /** * An incoming (deposit) transaction. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention incoming = 'incoming', /** * A transaction for personal sign. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention personalSign = 'personal_sign', /** @@ -620,43 +603,46 @@ export enum TransactionType { * to speed up pending transactions. This is accomplished by creating a new tx with * the same nonce and higher gas fees. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention retry = 'retry', /** * A transaction sending a network's native asset to a recipient. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention simpleSend = 'simpleSend', /** * A transaction that is signing typed data. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention signTypedData = 'eth_signTypedData', /** * A transaction sending a network's native asset to a recipient. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention smart = 'smart', + /** + * A transaction that claims staking rewards. + */ + stakingClaim = 'stakingClaim', + + /** + * A transaction that deposits tokens into a staking contract. + */ + stakingDeposit = 'stakingDeposit', + + /** + * A transaction that unstakes tokens from a staking contract. + */ + stakingUnstake = 'stakingUnstake', + /** * A transaction swapping one token for another through MetaMask Swaps. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention swap = 'swap', /** * A transaction swapping one token for another through MetaMask Swaps, then sending the swapped token to a recipient. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention swapAndSend = 'swapAndSend', /** @@ -665,16 +651,12 @@ export enum TransactionType { * of the user for the MetaMask Swaps contract. The first swap for any token * will have an accompanying swapApproval transaction. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention swapApproval = 'swapApproval', /** * A token transaction requesting an allowance of the token to spend on * behalf of the user. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention tokenMethodApprove = 'approve', /** @@ -683,16 +665,12 @@ export enum TransactionType { * this method the contract checks to ensure that the receiver is an address * capable of handling the token being sent. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention tokenMethodSafeTransferFrom = 'safetransferfrom', /** * A token transaction where the user is sending tokens that they own to * another address. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention tokenMethodTransfer = 'transfer', /** @@ -700,25 +678,20 @@ export enum TransactionType { * has an allowance of. For more information on allowances, see the approve * type. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention tokenMethodTransferFrom = 'transferfrom', /** * A token transaction requesting an allowance of all of a user's tokens to * spend on behalf of the user. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention tokenMethodSetApprovalForAll = 'setapprovalforall', /** * Increase the allowance by a given increment */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention tokenMethodIncreaseAllowance = 'increaseAllowance', } +/* eslint-enable @typescript-eslint/naming-convention */ /** * Standard data concerning a transaction to be processed by the blockchain. @@ -1119,7 +1092,7 @@ export type TransactionError = { export type SecurityAlertResponse = { reason: string; features?: string[]; - // TODO: Either fix this lint violation or explain why it's necessary to ignore. + // This is API specific hence naming convention is not followed. // eslint-disable-next-line @typescript-eslint/naming-convention result_type: string; providerRequestsCount?: Record; @@ -1310,6 +1283,9 @@ export type SimulationData = { /** Error data if the simulation failed or the transaction reverted. */ error?: SimulationError; + /** Whether the simulation response changed after a security check triggered a re-simulation. */ + isUpdatedAfterSecurityCheck?: boolean; + /** Data concerning a change to the user's native balance. */ nativeBalanceChange?: SimulationBalanceChange; diff --git a/packages/transaction-controller/src/utils/resimulate.test.ts b/packages/transaction-controller/src/utils/resimulate.test.ts new file mode 100644 index 00000000000..1c34f220650 --- /dev/null +++ b/packages/transaction-controller/src/utils/resimulate.test.ts @@ -0,0 +1,375 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { BN } from 'bn.js'; + +import { CHAIN_IDS } from '../constants'; +import type { + SecurityAlertResponse, + SimulationData, + SimulationTokenBalanceChange, + TransactionMeta, +} from '../types'; +import { SimulationTokenStandard, TransactionStatus } from '../types'; +import { + BLOCK_TIME_ADDITIONAL_SECONDS, + BLOCKAID_RESULT_TYPE_MALICIOUS, + hasSimulationDataChanged, + RESIMULATE_PARAMS, + shouldResimulate, + VALUE_COMPARISON_PERCENT_THRESHOLD, +} from './resimulate'; +import { getPercentageChange } from './utils'; + +jest.mock('./utils'); + +const CURRENT_TIME_MOCK = 1234567890; +const CURRENT_TIME_SECONDS_MOCK = 1234567; + +const SECURITY_ALERT_RESPONSE_MOCK: SecurityAlertResponse = { + reason: 'TestReason', + result_type: 'TestResultType', +}; + +const TOKEN_BALANCE_CHANGE_MOCK: SimulationTokenBalanceChange = { + address: '0x1', + standard: SimulationTokenStandard.erc20, + difference: '0x1', + previousBalance: '0x1', + newBalance: '0x2', + isDecrease: true, +}; + +const SIMULATION_DATA_MOCK: SimulationData = { + nativeBalanceChange: { + difference: '0x1', + previousBalance: '0x1', + newBalance: '0x2', + isDecrease: true, + }, + tokenBalanceChanges: [], +}; + +const SIMULATION_DATA_2_MOCK: SimulationData = { + nativeBalanceChange: { + difference: '0x1', + previousBalance: '0x2', + newBalance: '0x3', + isDecrease: false, + }, + tokenBalanceChanges: [], +}; + +const TRANSACTION_META_MOCK: TransactionMeta = { + chainId: CHAIN_IDS.MAINNET, + id: '123-456', + securityAlertResponse: SECURITY_ALERT_RESPONSE_MOCK, + status: TransactionStatus.unapproved, + time: 1234567890, + txParams: { + data: '0x1', + from: '0x2', + to: '0x3', + value: '0x4', + }, +}; + +describe('Resimulate Utils', () => { + const getPercentageChangeMock = jest.mocked(getPercentageChange); + + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(Date, 'now').mockReturnValue(CURRENT_TIME_MOCK); + + getPercentageChangeMock.mockReturnValue(0); + }); + + describe('shouldResimulate', () => { + it('does not resimulate if metadata unchanged', () => { + const result = shouldResimulate( + TRANSACTION_META_MOCK, + TRANSACTION_META_MOCK, + ); + + expect(result).toStrictEqual({ + blockTime: undefined, + resimulate: false, + }); + }); + + describe('Parameters', () => { + it.each(RESIMULATE_PARAMS)( + 'resimulates if %s parameter updated', + (param) => { + const result = shouldResimulate(TRANSACTION_META_MOCK, { + ...TRANSACTION_META_MOCK, + txParams: { + ...TRANSACTION_META_MOCK.txParams, + [param]: '0x6', + }, + }); + + expect(result).toStrictEqual({ + blockTime: undefined, + resimulate: true, + }); + }, + ); + + it('does not resimulate if no original params', () => { + const result = shouldResimulate( + { ...TRANSACTION_META_MOCK, txParams: undefined } as never, + TRANSACTION_META_MOCK, + ); + + expect(result).toStrictEqual({ + blockTime: undefined, + resimulate: false, + }); + }); + }); + + describe('Security Alert', () => { + it('resimulates if security alert updated and malicious', () => { + const result = shouldResimulate(TRANSACTION_META_MOCK, { + ...TRANSACTION_META_MOCK, + securityAlertResponse: { + ...SECURITY_ALERT_RESPONSE_MOCK, + result_type: BLOCKAID_RESULT_TYPE_MALICIOUS, + }, + }); + + expect(result.resimulate).toBe(true); + }); + + it('includes block time if security alert updated and malicious', () => { + const result = shouldResimulate(TRANSACTION_META_MOCK, { + ...TRANSACTION_META_MOCK, + securityAlertResponse: { + ...SECURITY_ALERT_RESPONSE_MOCK, + result_type: BLOCKAID_RESULT_TYPE_MALICIOUS, + }, + }); + + expect(result.blockTime).toBe( + CURRENT_TIME_SECONDS_MOCK + BLOCK_TIME_ADDITIONAL_SECONDS, + ); + }); + + it('does not resimulate if security alert updated but not malicious', () => { + const result = shouldResimulate(TRANSACTION_META_MOCK, { + ...TRANSACTION_META_MOCK, + securityAlertResponse: { + ...SECURITY_ALERT_RESPONSE_MOCK, + result_type: 'TestResultType2', + }, + }); + + expect(result).toStrictEqual({ + blockTime: undefined, + resimulate: false, + }); + }); + }); + + describe('Value & Native Balance', () => { + it('resimulates if value does not match native balance difference from simulation', () => { + getPercentageChangeMock.mockReturnValueOnce( + VALUE_COMPARISON_PERCENT_THRESHOLD + 1, + ); + + const result = shouldResimulate(TRANSACTION_META_MOCK, { + ...TRANSACTION_META_MOCK, + simulationData: SIMULATION_DATA_MOCK, + }); + + expect(result.resimulate).toBe(true); + }); + + it('includes block time if value does not match native balance difference from simulation', () => { + getPercentageChangeMock.mockReturnValueOnce( + VALUE_COMPARISON_PERCENT_THRESHOLD + 1, + ); + + const result = shouldResimulate(TRANSACTION_META_MOCK, { + ...TRANSACTION_META_MOCK, + simulationData: SIMULATION_DATA_MOCK, + }); + + expect(result.blockTime).toBe( + CURRENT_TIME_SECONDS_MOCK + BLOCK_TIME_ADDITIONAL_SECONDS, + ); + }); + + it('does not resimulate if simulation data changed but value and native balance match', () => { + getPercentageChangeMock.mockReturnValueOnce(0); + + const result = shouldResimulate(TRANSACTION_META_MOCK, { + ...TRANSACTION_META_MOCK, + simulationData: SIMULATION_DATA_MOCK, + }); + + expect(result).toStrictEqual({ + blockTime: undefined, + resimulate: false, + }); + }); + + it('does not resimulate if simulation data changed but value and native balance not specified', () => { + const result = shouldResimulate( + { + ...TRANSACTION_META_MOCK, + txParams: { + ...TRANSACTION_META_MOCK.txParams, + value: undefined, + }, + }, + { + ...TRANSACTION_META_MOCK, + txParams: { + ...TRANSACTION_META_MOCK.txParams, + value: undefined, + }, + simulationData: { + ...SIMULATION_DATA_MOCK, + nativeBalanceChange: undefined, + }, + }, + ); + + expect(getPercentageChangeMock).toHaveBeenCalledTimes(1); + expect(getPercentageChangeMock).toHaveBeenCalledWith( + new BN(0), + new BN(0), + ); + + expect(result).toStrictEqual({ + blockTime: undefined, + resimulate: false, + }); + }); + }); + }); + + describe('hasSimulationDataChanged', () => { + it('returns false if simulation data unchanged', () => { + const result = hasSimulationDataChanged( + SIMULATION_DATA_MOCK, + SIMULATION_DATA_MOCK, + ); + + expect(result).toBe(false); + }); + + it('returns true if native balance changed', () => { + getPercentageChangeMock.mockReturnValueOnce( + VALUE_COMPARISON_PERCENT_THRESHOLD + 1, + ); + + const result = hasSimulationDataChanged( + SIMULATION_DATA_MOCK, + SIMULATION_DATA_2_MOCK, + ); + + expect(result).toBe(true); + }); + + it('returns true if token balance count does not match', () => { + getPercentageChangeMock.mockReturnValueOnce(0); + + const result = hasSimulationDataChanged(SIMULATION_DATA_MOCK, { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [TOKEN_BALANCE_CHANGE_MOCK], + }); + + expect(result).toBe(true); + }); + + it('returns true if token balance does not match', () => { + getPercentageChangeMock + .mockReturnValueOnce(0) + .mockReturnValueOnce(VALUE_COMPARISON_PERCENT_THRESHOLD + 1); + + const result = hasSimulationDataChanged( + { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [TOKEN_BALANCE_CHANGE_MOCK], + }, + { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [ + { ...TOKEN_BALANCE_CHANGE_MOCK, difference: '0x2' }, + ], + }, + ); + + expect(result).toBe(true); + }); + + it('returns false if token balance changed but within threshold', () => { + getPercentageChangeMock + .mockReturnValueOnce(0) + .mockReturnValueOnce(VALUE_COMPARISON_PERCENT_THRESHOLD); + + const result = hasSimulationDataChanged( + { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [TOKEN_BALANCE_CHANGE_MOCK], + }, + { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [ + { ...TOKEN_BALANCE_CHANGE_MOCK, difference: '0x2' }, + ], + }, + ); + + expect(result).toBe(false); + }); + + it('returns true if new token balance not found', () => { + getPercentageChangeMock.mockReturnValueOnce(0).mockReturnValueOnce(0); + + const result = hasSimulationDataChanged( + { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [TOKEN_BALANCE_CHANGE_MOCK], + }, + { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [ + { ...TOKEN_BALANCE_CHANGE_MOCK, address: '0x2' }, + ], + }, + ); + + expect(result).toBe(true); + }); + + it('supports increased balance', () => { + getPercentageChangeMock + .mockReturnValueOnce(0) + .mockReturnValueOnce(VALUE_COMPARISON_PERCENT_THRESHOLD + 1); + + const result = hasSimulationDataChanged( + { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [TOKEN_BALANCE_CHANGE_MOCK], + }, + { + ...SIMULATION_DATA_MOCK, + tokenBalanceChanges: [ + { ...TOKEN_BALANCE_CHANGE_MOCK, isDecrease: false }, + ], + }, + ); + + expect(getPercentageChangeMock).toHaveBeenCalledTimes(2); + expect(getPercentageChangeMock).toHaveBeenNthCalledWith( + 2, + new BN(1), + new BN(-1), + ); + + expect(result).toBe(true); + }); + }); +}); diff --git a/packages/transaction-controller/src/utils/resimulate.ts b/packages/transaction-controller/src/utils/resimulate.ts new file mode 100644 index 00000000000..b339356c7a2 --- /dev/null +++ b/packages/transaction-controller/src/utils/resimulate.ts @@ -0,0 +1,288 @@ +import type { Hex } from '@metamask/utils'; +import { createModuleLogger, remove0x } from '@metamask/utils'; +import { BN } from 'bn.js'; +import { isEqual } from 'lodash'; + +import { projectLogger } from '../logger'; +import type { + SimulationBalanceChange, + SimulationData, + TransactionMeta, + TransactionParams, +} from '../types'; +import { getPercentageChange } from './utils'; + +const log = createModuleLogger(projectLogger, 'resimulate'); + +export const RESIMULATE_PARAMS = ['to', 'value', 'data'] as const; +export const BLOCKAID_RESULT_TYPE_MALICIOUS = 'Malicious'; +export const VALUE_COMPARISON_PERCENT_THRESHOLD = 5; +export const BLOCK_TIME_ADDITIONAL_SECONDS = 60; + +export type ResimulateResponse = { + blockTime?: number; + resimulate: boolean; +}; + +/** + * Determine if a transaction should be resimulated. + * @param originalTransactionMeta - The original transaction metadata. + * @param newTransactionMeta - The new transaction metadata. + * @returns Whether the transaction should be resimulated. + */ +export function shouldResimulate( + originalTransactionMeta: TransactionMeta, + newTransactionMeta: TransactionMeta, +) { + const { id: transactionId } = newTransactionMeta; + + const parametersUpdated = isParametersUpdated( + originalTransactionMeta, + newTransactionMeta, + ); + + const securityAlert = hasNewSecurityAlert( + originalTransactionMeta, + newTransactionMeta, + ); + + const valueAndNativeBalanceMismatch = hasValueAndNativeBalanceMismatch( + originalTransactionMeta, + newTransactionMeta, + ); + + const resimulate = + parametersUpdated || securityAlert || valueAndNativeBalanceMismatch; + + let blockTime: number | undefined; + + if (securityAlert || valueAndNativeBalanceMismatch) { + const nowSeconds = Math.floor(Date.now() / 1000); + blockTime = nowSeconds + BLOCK_TIME_ADDITIONAL_SECONDS; + } + + if (resimulate) { + log('Transaction should be resimulated', { + transactionId, + blockTime, + parametersUpdated, + securityAlert, + valueAndNativeBalanceMismatch, + }); + } + + return { + blockTime, + resimulate, + }; +} + +/** + * Determine if the simulation data has changed. + * @param originalSimulationData - The original simulation data. + * @param newSimulationData - The new simulation data. + * @returns Whether the simulation data has changed. + */ +export function hasSimulationDataChanged( + originalSimulationData: SimulationData, + newSimulationData: SimulationData, +): boolean { + if (isEqual(originalSimulationData, newSimulationData)) { + return false; + } + + if ( + isBalanceChangeUpdated( + originalSimulationData?.nativeBalanceChange, + newSimulationData?.nativeBalanceChange, + ) + ) { + log('Simulation data native balance changed'); + return true; + } + + if ( + originalSimulationData.tokenBalanceChanges.length !== + newSimulationData.tokenBalanceChanges.length + ) { + return true; + } + + for (const originalTokenBalanceChange of originalSimulationData.tokenBalanceChanges) { + const newTokenBalanceChange = newSimulationData.tokenBalanceChanges.find( + ({ address, id }) => + address === originalTokenBalanceChange.address && + id === originalTokenBalanceChange.id, + ); + + if (!newTokenBalanceChange) { + log('Missing new token balance', { + address: originalTokenBalanceChange.address, + id: originalTokenBalanceChange.id, + }); + + return true; + } + + if ( + isBalanceChangeUpdated(originalTokenBalanceChange, newTokenBalanceChange) + ) { + log('Simulation data token balance changed', { + originalTokenBalanceChange, + newTokenBalanceChange, + }); + + return true; + } + } + + return false; +} + +/** + * Determine if the transaction parameters have been updated. + * @param originalTransactionMeta - The original transaction metadata. + * @param newTransactionMeta - The new transaction metadata. + * @returns Whether the transaction parameters have been updated. + */ +function isParametersUpdated( + originalTransactionMeta: TransactionMeta, + newTransactionMeta: TransactionMeta, +): boolean { + const { id: transactionId, txParams: newParams } = newTransactionMeta; + const { txParams: originalParams } = originalTransactionMeta; + + if (!originalParams || isEqual(originalParams, newParams)) { + return false; + } + + const params = Object.keys(newParams) as (keyof TransactionParams)[]; + + const updatedProperties = params.filter( + (param) => newParams[param] !== originalParams[param], + ); + + log('Transaction parameters updated', { + transactionId, + updatedProperties, + originalParams, + newParams, + }); + + return RESIMULATE_PARAMS.some((param) => updatedProperties.includes(param)); +} + +/** + * Determine if a transaction has a new security alert. + * @param originalTransactionMeta - The original transaction metadata. + * @param newTransactionMeta - The new transaction metadata. + * @returns Whether the transaction has a new security alert. + */ +function hasNewSecurityAlert( + originalTransactionMeta: TransactionMeta, + newTransactionMeta: TransactionMeta, +): boolean { + const { securityAlertResponse: originalSecurityAlertResponse } = + originalTransactionMeta; + + const { id: transactionId, securityAlertResponse: newSecurityAlertResponse } = + newTransactionMeta; + + if (isEqual(originalSecurityAlertResponse, newSecurityAlertResponse)) { + return false; + } + + log('Security alert updated', { + transactionId, + originalSecurityAlertResponse, + newSecurityAlertResponse, + }); + + return ( + newSecurityAlertResponse?.result_type === BLOCKAID_RESULT_TYPE_MALICIOUS + ); +} + +/** + * Determine if a transaction has a value and simulation native balance mismatch. + * @param originalTransactionMeta - The original transaction metadata. + * @param newTransactionMeta - The new transaction metadata. + * @returns Whether the transaction has a value and simulation native balance mismatch. + */ +function hasValueAndNativeBalanceMismatch( + originalTransactionMeta: TransactionMeta, + newTransactionMeta: TransactionMeta, +): boolean { + const { simulationData: originalSimulationData } = originalTransactionMeta; + + const { simulationData: newSimulationData, txParams: newTxParams } = + newTransactionMeta; + + if ( + !newSimulationData || + isEqual(originalSimulationData, newSimulationData) + ) { + return false; + } + + const newValue = newTxParams?.value ?? '0x0'; + + const newNativeBalanceDifference = + newSimulationData?.nativeBalanceChange?.difference ?? '0x0'; + + return !percentageChangeWithinThreshold( + newValue as Hex, + newNativeBalanceDifference, + false, + newSimulationData?.nativeBalanceChange?.isDecrease === false, + ); +} + +/** + * Determine if a balance change has been updated. + * @param originalBalanceChange - The original balance change. + * @param newBalanceChange - The new balance change. + * @returns Whether the balance change has been updated. + */ +function isBalanceChangeUpdated( + originalBalanceChange?: SimulationBalanceChange, + newBalanceChange?: SimulationBalanceChange, +): boolean { + return !percentageChangeWithinThreshold( + originalBalanceChange?.difference ?? '0x0', + newBalanceChange?.difference ?? '0x0', + originalBalanceChange?.isDecrease === false, + newBalanceChange?.isDecrease === false, + ); +} + +/** + * Determine if the percentage change between two values is within a threshold. + * @param originalValue - The original value. + * @param newValue - The new value. + * @param originalNegative - Whether the original value is negative. + * @param newNegative - Whether the new value is negative. + * @returns Whether the percentage change between the two values is within a threshold. + */ +function percentageChangeWithinThreshold( + originalValue: Hex, + newValue: Hex, + originalNegative?: boolean, + newNegative?: boolean, +): boolean { + let originalValueBN = new BN(remove0x(originalValue), 'hex'); + let newValueBN = new BN(remove0x(newValue), 'hex'); + + if (originalNegative) { + originalValueBN = originalValueBN.neg(); + } + + if (newNegative) { + newValueBN = newValueBN.neg(); + } + + return ( + getPercentageChange(originalValueBN, newValueBN) <= + VALUE_COMPARISON_PERCENT_THRESHOLD + ); +} diff --git a/packages/transaction-controller/src/utils/simulation-api.ts b/packages/transaction-controller/src/utils/simulation-api.ts index 7b0b8e535b3..19ba26546a9 100644 --- a/packages/transaction-controller/src/utils/simulation-api.ts +++ b/packages/transaction-controller/src/utils/simulation-api.ts @@ -42,6 +42,10 @@ export type SimulationRequest = { */ transactions: SimulationRequestTransaction[]; + blockOverrides?: { + time?: Hex; + }; + /** * Overrides to the state of the blockchain, keyed by smart contract address. */ diff --git a/packages/transaction-controller/src/utils/simulation.ts b/packages/transaction-controller/src/utils/simulation.ts index cb6ff73f067..d6b845019ac 100644 --- a/packages/transaction-controller/src/utils/simulation.ts +++ b/packages/transaction-controller/src/utils/simulation.ts @@ -46,10 +46,10 @@ type ABI = Fragment[]; export type GetSimulationDataRequest = { chainId: Hex; + data?: Hex; from: Hex; to?: Hex; value?: Hex; - data?: Hex; }; type ParsedEvent = { @@ -60,6 +60,10 @@ type ParsedEvent = { abi: ABI; }; +type GetSimulationDataOptions = { + blockTime?: number; +}; + const log = createModuleLogger(projectLogger, 'simulation'); const SUPPORTED_EVENTS = [ @@ -105,12 +109,16 @@ type BalanceTransactionMap = Map; * @param request.to - The recipient of the transaction. * @param request.value - The value of the transaction. * @param request.data - The data of the transaction. + * @param options - Additional options. + * @param options.blockTime - An optional block time to simulate the transaction at. * @returns The simulation data. */ export async function getSimulationData( request: GetSimulationDataRequest, + options: GetSimulationDataOptions = {}, ): Promise { const { chainId, from, to, value, data } = request; + const { blockTime } = options; log('Getting simulation data', request); @@ -128,6 +136,11 @@ export async function getSimulationData( ], withCallTrace: true, withLogs: true, + ...(blockTime && { + blockOverrides: { + time: toHex(blockTime), + }, + }), }); const transactionError = response.transactions?.[0]?.error; @@ -141,7 +154,11 @@ export async function getSimulationData( log('Parsed events', events); - const tokenBalanceChanges = await getTokenBalanceChanges(request, events); + const tokenBalanceChanges = await getTokenBalanceChanges( + request, + events, + options, + ); return { nativeBalanceChange, @@ -296,12 +313,16 @@ function normalizeEventArgValue(value: any): any { * Generate token balance changes from parsed events. * @param request - The transaction that was simulated. * @param events - The parsed events. + * @param options - Additional options. + * @param options.blockTime - An optional block time to simulate the transaction at. * @returns An array of token balance changes. */ async function getTokenBalanceChanges( request: GetSimulationDataRequest, events: ParsedEvent[], + options: GetSimulationDataOptions, ): Promise { + const { blockTime } = options; const balanceTxs = getTokenBalanceTransactions(request, events); log('Generated balance transactions', [...balanceTxs.after.values()]); @@ -318,6 +339,11 @@ async function getTokenBalanceChanges( const response = await simulateTransactions(request.chainId as Hex, { transactions, + ...(blockTime && { + blockOverrides: { + time: toHex(blockTime), + }, + }), }); log('Balance simulation response', response); diff --git a/packages/transaction-controller/src/utils/utils.test.ts b/packages/transaction-controller/src/utils/utils.test.ts index bd81a9a4b4a..a49b883dece 100644 --- a/packages/transaction-controller/src/utils/utils.test.ts +++ b/packages/transaction-controller/src/utils/utils.test.ts @@ -1,3 +1,5 @@ +import { BN } from 'bn.js'; + import type { FeeMarketEIP1559Values, GasPriceValue, @@ -214,4 +216,47 @@ describe('utils', () => { expect(util.padHexToEvenLength('0x0')).toBe('0x00'); }); }); + + describe('getPercentageChange', () => { + it('supports original and new value as zero', () => { + expect(util.getPercentageChange(new BN(0), new BN(0))).toBe(0); + }); + + it('supports original value as zero and new value not', () => { + expect(util.getPercentageChange(new BN(0), new BN(1))).toBe(100); + }); + + it('supports new value greater than original value', () => { + expect(util.getPercentageChange(new BN(10), new BN(11))).toBe(10); + }); + + it('supports new value less than original value', () => { + expect(util.getPercentageChange(new BN(11), new BN(10))).toBe(9); + }); + + it('supports large numbers', () => { + expect( + util.getPercentageChange( + new BN( + '100000000000000000000000000000000000000000000000000000000000000000000000000000000', + ), + new BN( + '200000000000000000000000000000000000000000000000000000000000000000000000000000000', + ), + ), + ).toBe(100); + }); + + it('supports identical original and new value', () => { + expect(util.getPercentageChange(new BN(1), new BN(1))).toBe(0); + }); + + it('supports negative original value', () => { + expect(util.getPercentageChange(new BN(-1), new BN(2))).toBe(300); + }); + + it('supports negative new value', () => { + expect(util.getPercentageChange(new BN(2), new BN(-1))).toBe(150); + }); + }); }); diff --git a/packages/transaction-controller/src/utils/utils.ts b/packages/transaction-controller/src/utils/utils.ts index 4c0a633faec..360f5260dcd 100644 --- a/packages/transaction-controller/src/utils/utils.ts +++ b/packages/transaction-controller/src/utils/utils.ts @@ -4,6 +4,7 @@ import { isStrictHexString, } from '@metamask/utils'; import type { Json } from '@metamask/utils'; +import BN from 'bn.js'; import { TransactionStatus } from '../types'; import type { @@ -183,3 +184,29 @@ export function padHexToEvenLength(hex: string) { return prefix + evenData; } + +/** + * Calculate the absolute percentage change between two values. + * + * @param originalValue - The first value. + * @param newValue - The second value. + * @returns The percentage change from the first value to the second value. + * If the original value is zero and the new value is not, returns 100. + */ +export function getPercentageChange(originalValue: BN, newValue: BN): number { + const precisionFactor = new BN(10).pow(new BN(18)); + const originalValuePrecision = originalValue.mul(precisionFactor); + const newValuePrecision = newValue.mul(precisionFactor); + + const difference = newValuePrecision.sub(originalValuePrecision); + + if (difference.isZero()) { + return 0; + } + + if (originalValuePrecision.isZero() && !newValuePrecision.isZero()) { + return 100; + } + + return difference.muln(100).div(originalValuePrecision).abs().toNumber(); +} diff --git a/packages/user-operation-controller/package.json b/packages/user-operation-controller/package.json index e7b140eca77..f648735945c 100644 --- a/packages/user-operation-controller/package.json +++ b/packages/user-operation-controller/package.json @@ -48,10 +48,10 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.0.1", - "@metamask/controller-utils": "^11.4.0", + "@metamask/base-controller": "^7.0.2", + "@metamask/controller-utils": "^11.4.2", "@metamask/eth-query": "^4.0.0", - "@metamask/polling-controller": "^12.0.0", + "@metamask/polling-controller": "^12.0.1", "@metamask/rpc-errors": "^7.0.1", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^10.0.0", @@ -61,12 +61,12 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/approval-controller": "^7.1.0", + "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", "@metamask/gas-fee-controller": "^22.0.0", - "@metamask/keyring-controller": "^17.3.0", - "@metamask/network-controller": "^22.0.0", - "@metamask/transaction-controller": "^38.0.0", + "@metamask/keyring-controller": "^17.3.1", + "@metamask/network-controller": "^22.0.1", + "@metamask/transaction-controller": "^38.1.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/user-operation-controller/src/utils/validation.test.ts b/packages/user-operation-controller/src/utils/validation.test.ts index cdb2f26fa87..180221394f3 100644 --- a/packages/user-operation-controller/src/utils/validation.test.ts +++ b/packages/user-operation-controller/src/utils/validation.test.ts @@ -1,5 +1,6 @@ /* eslint-disable jest/expect-expect */ +import { TransactionType } from '@metamask/transaction-controller'; import { cloneDeep } from 'lodash'; import type { @@ -341,7 +342,9 @@ describe('validation', () => { 'type', 'wrong type', 123, - 'Expected one of `"bridge","bridgeApproval","cancel","contractInteraction","contractDeployment","eth_decrypt","eth_getEncryptionPublicKey","incoming","personal_sign","retry","simpleSend","eth_signTypedData","smart","swap","swapAndSend","swapApproval","approve","safetransferfrom","transfer","transferfrom","setapprovalforall","increaseAllowance"`, but received: 123', + `Expected one of \`${Object.values(TransactionType) + .map((value) => `"${value as string}"`) + .join(',')}\`, but received: 123`, ], ])( 'throws if %s is %s', diff --git a/yarn.lock b/yarn.lock index 73cad45473c..9f7c47be2f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2027,16 +2027,16 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^18.2.2, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^18.2.3, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@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.0" + "@metamask/keyring-controller": "npm:^17.3.1" "@metamask/snaps-controllers": "npm:^9.7.0" "@metamask/snaps-sdk": "npm:^6.5.0" "@metamask/snaps-utils": "npm:^8.1.1" @@ -2074,8 +2074,8 @@ __metadata: resolution: "@metamask/address-book-controller@workspace:packages/address-book-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2092,7 +2092,7 @@ __metadata: resolution: "@metamask/announcement-controller@workspace:packages/announcement-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -2103,12 +2103,12 @@ __metadata: languageName: unknown linkType: soft -"@metamask/approval-controller@npm:^7.0.2, @metamask/approval-controller@npm:^7.1.0, @metamask/approval-controller@workspace:packages/approval-controller": +"@metamask/approval-controller@npm:^7.0.2, @metamask/approval-controller@npm:^7.1.1, @metamask/approval-controller@workspace:packages/approval-controller": version: 0.0.0-use.local resolution: "@metamask/approval-controller@workspace:packages/approval-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -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.2" - "@metamask/approval-controller": "npm:^7.1.0" + "@metamask/accounts-controller": "npm:^18.2.3" + "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/controller-utils": "npm:^11.4.2" "@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.0" + "@metamask/keyring-controller": "npm:^17.3.1" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^22.0.0" - "@metamask/polling-controller": "npm:^12.0.0" - "@metamask/preferences-controller": "npm:^13.1.0" + "@metamask/network-controller": "npm:^22.0.1" + "@metamask/polling-controller": "npm:^12.0.1" + "@metamask/preferences-controller": "npm:^13.2.0" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" @@ -2220,12 +2220,12 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^7.0.1, @metamask/base-controller@workspace:packages/base-controller": +"@metamask/base-controller@npm:^7.0.2, @metamask/base-controller@workspace:packages/base-controller": version: 0.0.0-use.local resolution: "@metamask/base-controller@workspace:packages/base-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" "@types/sinon": "npm:^9.0.10" @@ -2282,7 +2282,7 @@ __metadata: resolution: "@metamask/chain-controller@workspace:packages/chain-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/chain-api": "npm:^0.1.0" "@metamask/keyring-api": "npm:^8.1.3" "@metamask/snaps-controllers": "npm:^9.7.0" @@ -2306,8 +2306,8 @@ __metadata: resolution: "@metamask/composable-controller@workspace:packages/composable-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" immer: "npm:^9.0.6" @@ -2327,7 +2327,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.4.0, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.2, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -2367,8 +2367,8 @@ __metadata: "@metamask/eslint-config-nodejs": "npm:^12.1.0" "@metamask/eslint-config-typescript": "npm:^12.1.0" "@metamask/eth-block-tracker": "npm:^11.0.2" - "@metamask/eth-json-rpc-provider": "npm:^4.1.5" - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/eth-json-rpc-provider": "npm:^4.1.6" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/utils": "npm:^10.0.0" "@ts-bridge/cli": "npm:^0.5.1" "@types/jest": "npm:^27.4.1" @@ -2435,9 +2435,9 @@ __metadata: dependencies: "@ethersproject/providers": "npm:^5.7.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" - "@metamask/network-controller": "npm:^22.0.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/network-controller": "npm:^22.0.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2559,7 +2559,7 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-json-rpc-provider@npm:^4.1.5, @metamask/eth-json-rpc-provider@workspace:packages/eth-json-rpc-provider": +"@metamask/eth-json-rpc-provider@npm:^4.1.5, @metamask/eth-json-rpc-provider@npm:^4.1.6, @metamask/eth-json-rpc-provider@workspace:packages/eth-json-rpc-provider": version: 0.0.0-use.local resolution: "@metamask/eth-json-rpc-provider@workspace:packages/eth-json-rpc-provider" dependencies: @@ -2567,7 +2567,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-query": "npm:^0.5.3" - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^10.0.0" @@ -2802,8 +2802,8 @@ __metadata: resolution: "@metamask/example-controllers@workspace:examples/example-controllers" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2821,12 +2821,12 @@ __metadata: resolution: "@metamask/gas-fee-controller@workspace:packages/gas-fee-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-unit": "npm:^0.3.0" - "@metamask/network-controller": "npm:^22.0.0" - "@metamask/polling-controller": "npm:^12.0.0" + "@metamask/network-controller": "npm:^22.0.1" + "@metamask/polling-controller": "npm:^12.0.1" "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" "@types/jest": "npm:^27.4.1" @@ -2848,7 +2848,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": +"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.1, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" dependencies: @@ -2883,7 +2883,7 @@ __metadata: resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -2930,7 +2930,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^17.3.0, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^17.3.1, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -2941,13 +2941,13 @@ __metadata: "@keystonehq/metamask-airgapped-keyring": "npm:^0.14.1" "@lavamoat/allow-scripts": "npm:^3.0.4" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/eth-hd-keyring": "npm:^7.0.4" "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/eth-simple-keyring": "npm:^6.0.5" "@metamask/keyring-api": "npm:^8.1.3" - "@metamask/message-manager": "npm:^11.0.0" + "@metamask/message-manager": "npm:^11.0.1" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -2971,8 +2971,8 @@ __metadata: resolution: "@metamask/logging-controller@workspace:packages/logging-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -2984,13 +2984,13 @@ __metadata: languageName: unknown linkType: soft -"@metamask/message-manager@npm:^11.0.0, @metamask/message-manager@workspace:packages/message-manager": +"@metamask/message-manager@npm:^11.0.1, @metamask/message-manager@workspace:packages/message-manager": version: 0.0.0-use.local resolution: "@metamask/message-manager@workspace:packages/message-manager" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -3033,8 +3033,8 @@ __metadata: resolution: "@metamask/name-controller@workspace:packages/name-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" async-mutex: "npm:^0.5.0" @@ -3047,20 +3047,20 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^22.0.0, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@npm:^22.0.1, @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.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@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" - "@metamask/eth-json-rpc-provider": "npm:^4.1.5" + "@metamask/eth-json-rpc-provider": "npm:^4.1.6" "@metamask/eth-query": "npm:^4.0.0" - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/swappable-obj-proxy": "npm:^2.2.0" "@metamask/utils": "npm:^10.0.0" @@ -3103,7 +3103,7 @@ __metadata: resolution: "@metamask/notification-controller@workspace:packages/notification-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3123,9 +3123,9 @@ __metadata: "@contentful/rich-text-html-renderer": "npm:^16.5.2" "@lavamoat/allow-scripts": "npm:^3.0.4" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" - "@metamask/keyring-controller": "npm:^17.3.0" + "@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/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -3179,15 +3179,15 @@ __metadata: languageName: node linkType: hard -"@metamask/permission-controller@npm:^11.0.0, @metamask/permission-controller@npm:^11.0.2, @metamask/permission-controller@workspace:packages/permission-controller": +"@metamask/permission-controller@npm:^11.0.0, @metamask/permission-controller@npm:^11.0.3, @metamask/permission-controller@workspace:packages/permission-controller": version: 0.0.0-use.local resolution: "@metamask/permission-controller@workspace:packages/permission-controller" dependencies: - "@metamask/approval-controller": "npm:^7.1.0" + "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" "@types/deep-freeze-strict": "npm:^1.1.0" @@ -3211,8 +3211,8 @@ __metadata: resolution: "@metamask/permission-log-controller@workspace:packages/permission-log-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/utils": "npm:^10.0.0" "@types/deep-freeze-strict": "npm:^1.1.0" "@types/jest": "npm:^27.4.1" @@ -3232,8 +3232,8 @@ __metadata: resolution: "@metamask/phishing-controller@workspace:packages/phishing-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@noble/hashes": "npm:^1.4.0" "@types/jest": "npm:^27.4.1" "@types/punycode": "npm:^2.1.0" @@ -3251,14 +3251,14 @@ __metadata: languageName: unknown linkType: soft -"@metamask/polling-controller@npm:^12.0.0, @metamask/polling-controller@workspace:packages/polling-controller": +"@metamask/polling-controller@npm:^12.0.1, @metamask/polling-controller@workspace:packages/polling-controller": version: 0.0.0-use.local resolution: "@metamask/polling-controller@workspace:packages/polling-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" - "@metamask/network-controller": "npm:^22.0.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/network-controller": "npm:^22.0.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" "@types/uuid": "npm:^8.3.0" @@ -3286,14 +3286,14 @@ __metadata: languageName: node linkType: hard -"@metamask/preferences-controller@npm:^13.1.0, @metamask/preferences-controller@workspace:packages/preferences-controller": +"@metamask/preferences-controller@npm:^13.2.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.1" - "@metamask/controller-utils": "npm:^11.4.0" - "@metamask/keyring-controller": "npm:^17.3.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/keyring-controller": "npm:^17.3.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -3312,12 +3312,12 @@ __metadata: resolution: "@metamask/profile-sync-controller@workspace:packages/profile-sync-controller" dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" - "@metamask/accounts-controller": "npm:^18.2.2" + "@metamask/accounts-controller": "npm:^18.2.3" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/keyring-api": "npm:^8.1.3" - "@metamask/keyring-controller": "npm:^17.3.0" - "@metamask/network-controller": "npm:^22.0.0" + "@metamask/keyring-controller": "npm:^17.3.1" + "@metamask/network-controller": "npm:^22.0.1" "@metamask/snaps-controllers": "npm:^9.7.0" "@metamask/snaps-sdk": "npm:^6.5.0" "@metamask/snaps-utils": "npm:^8.1.1" @@ -3370,10 +3370,10 @@ __metadata: resolution: "@metamask/queued-request-controller@workspace:packages/queued-request-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" - "@metamask/json-rpc-engine": "npm:^10.0.0" - "@metamask/network-controller": "npm:^22.0.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/network-controller": "npm:^22.0.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/selected-network-controller": "npm:^19.0.0" "@metamask/swappable-obj-proxy": "npm:^2.2.0" @@ -3400,7 +3400,7 @@ __metadata: resolution: "@metamask/rate-limit-controller@workspace:packages/rate-limit-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -3455,10 +3455,10 @@ __metadata: resolution: "@metamask/selected-network-controller@workspace:packages/selected-network-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/json-rpc-engine": "npm:^10.0.0" - "@metamask/network-controller": "npm:^22.0.0" - "@metamask/permission-controller": "npm:^11.0.2" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/network-controller": "npm:^22.0.1" + "@metamask/permission-controller": "npm:^11.0.3" "@metamask/swappable-obj-proxy": "npm:^2.2.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -3482,14 +3482,14 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/signature-controller@workspace:packages/signature-controller" dependencies: - "@metamask/approval-controller": "npm:^7.1.0" + "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-sig-util": "npm:^8.0.0" - "@metamask/keyring-controller": "npm:^17.3.0" + "@metamask/keyring-controller": "npm:^17.3.1" "@metamask/logging-controller": "npm:^6.0.1" - "@metamask/network-controller": "npm:^22.0.0" + "@metamask/network-controller": "npm:^22.0.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3677,7 +3677,7 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^38.0.0, @metamask/transaction-controller@workspace:packages/transaction-controller": +"@metamask/transaction-controller@npm:^38.1.0, @metamask/transaction-controller@workspace:packages/transaction-controller": version: 0.0.0-use.local resolution: "@metamask/transaction-controller@workspace:packages/transaction-controller" dependencies: @@ -3688,18 +3688,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.2" - "@metamask/approval-controller": "npm:^7.1.0" + "@metamask/accounts-controller": "npm:^18.2.3" + "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" - "@metamask/eth-json-rpc-provider": "npm:^4.1.5" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" + "@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/keyring-api": "npm:^8.1.3" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^22.0.0" + "@metamask/network-controller": "npm:^22.0.1" "@metamask/nonce-tracker": "npm:^6.0.0" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" @@ -3734,18 +3734,18 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/user-operation-controller@workspace:packages/user-operation-controller" dependencies: - "@metamask/approval-controller": "npm:^7.1.0" + "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-query": "npm:^4.0.0" "@metamask/gas-fee-controller": "npm:^22.0.0" - "@metamask/keyring-controller": "npm:^17.3.0" - "@metamask/network-controller": "npm:^22.0.0" - "@metamask/polling-controller": "npm:^12.0.0" + "@metamask/keyring-controller": "npm:^17.3.1" + "@metamask/network-controller": "npm:^22.0.1" + "@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.0.0" + "@metamask/transaction-controller": "npm:^38.1.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" bn.js: "npm:^5.2.1"