From 75230dc8cade5f2039f220ad8d5220a04662f243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Van=20Eyck?= Date: Wed, 12 Feb 2025 10:25:06 +0100 Subject: [PATCH 01/20] feat: add new keyring type for oneKey (#5216) ## Explanation This PR adds support for a dedicated OneKey keyring (until now it was sharing the same keyring instance than Trezor) so it's considered as a standalone device and could get its own tag inside account list. There are two others PRs: - metamask-extension: https://github.com/MetaMask/metamask-extension/pull/29999 - eth-trezor-keyring: https://github.com/MetaMask/accounts/pull/175 ## References Fixes: https://github.com/MetaMask/accounts-planning/issues/793 ## Changelog ### `@metamask/accounts-controller` - **utils**: add OneKey keyring type - **tests**: update accounts controller unit test with OneKey keyring type ### `@metamask/keyring-controller` - **controller**: add OneKey keyring type ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/accounts-controller/src/AccountsController.test.ts | 1 + packages/accounts-controller/src/utils.ts | 3 +++ packages/keyring-controller/src/KeyringController.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 07c400f9a4e..9df36e855a1 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1875,6 +1875,7 @@ describe('AccountsController', () => { KeyringTypes.simple, KeyringTypes.hd, KeyringTypes.trezor, + KeyringTypes.oneKey, KeyringTypes.ledger, KeyringTypes.lattice, KeyringTypes.qr, diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index d3cb5aede23..0f8e47e1aeb 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -27,6 +27,9 @@ export function keyringTypeToName(keyringType: string): string { case KeyringTypes.trezor: { return 'Trezor'; } + case KeyringTypes.oneKey: { + return 'OneKey'; + } case KeyringTypes.ledger: { return 'Ledger'; } diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index b04582ee3ca..d28b07ce93c 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -56,6 +56,7 @@ export enum KeyringTypes { hd = 'HD Key Tree', qr = 'QR Hardware Wallet Device', trezor = 'Trezor Hardware', + oneKey = 'OneKey Hardware', ledger = 'Ledger Hardware', lattice = 'Lattice Hardware', snap = 'Snap Keyring', From 386e1b18c3889e65754e37b534cdc9b96758f3c5 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 12 Feb 2025 12:16:54 +0100 Subject: [PATCH 02/20] Release 298.0.0 (#5314) Release for the support of OneKey devices. Those devices were already supported, but they will use a new specific keyring `OneKeyKeyring` to distinguish them from normal Trezor devices. Both keyring shares the same logic and the same bridge logic too. --- package.json | 2 +- packages/accounts-controller/CHANGELOG.md | 9 +++++- packages/accounts-controller/package.json | 4 +-- packages/assets-controllers/package.json | 4 +-- packages/earn-controller/package.json | 2 +- packages/keyring-controller/CHANGELOG.md | 13 +++++++- packages/keyring-controller/package.json | 2 +- .../package.json | 4 +-- .../package.json | 2 +- packages/preferences-controller/package.json | 2 +- packages/profile-sync-controller/package.json | 4 +-- packages/signature-controller/package.json | 2 +- packages/transaction-controller/package.json | 2 +- .../user-operation-controller/package.json | 2 +- yarn.lock | 30 +++++++++---------- 15 files changed, 51 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 48ebc122150..db5e7384657 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "297.0.0", + "version": "298.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 88cb13fa5e4..a37b6fa4c12 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [23.1.0] + +### Added + +- Add new keyring type for OneKey ([#5216](https://github.com/MetaMask/core/pull/5216)) + ## [23.0.1] ### Changed @@ -438,7 +444,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@23.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@23.1.0...HEAD +[23.1.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@23.0.1...@metamask/accounts-controller@23.1.0 [23.0.1]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@23.0.0...@metamask/accounts-controller@23.0.1 [23.0.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@22.0.0...@metamask/accounts-controller@23.0.0 [22.0.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@21.0.2...@metamask/accounts-controller@22.0.0 diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 4cd97c0c651..2f140161fbb 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/accounts-controller", - "version": "23.0.1", + "version": "23.1.0", "description": "Manages internal accounts", "keywords": [ "MetaMask", @@ -62,7 +62,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@metamask/providers": "^18.1.1", "@metamask/snaps-controllers": "^9.19.0", "@types/jest": "^27.4.1", diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 594cbdd7354..fb14e6e63b2 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -77,11 +77,11 @@ }, "devDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^23.1.0", "@metamask/approval-controller": "^7.1.3", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@metamask/keyring-internal-api": "^4.0.1", "@metamask/keyring-snap-client": "^3.0.3", "@metamask/network-controller": "^22.2.1", diff --git a/packages/earn-controller/package.json b/packages/earn-controller/package.json index 1f1457bc23e..0acc8812145 100644 --- a/packages/earn-controller/package.json +++ b/packages/earn-controller/package.json @@ -53,7 +53,7 @@ "@metamask/stake-sdk": "^1.0.0" }, "devDependencies": { - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^23.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/network-controller": "^22.2.1", "@types/jest": "^27.4.1", diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 71590b4092e..1390ffdb22f 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [19.1.0] + +### Added + +- Add new keyring type for OneKey ([#5216](https://github.com/MetaMask/core/pull/5216)) + +### Changed + +- A specific error message is thrown when any operation is attempted while the controller is locked ([#5172](https://github.com/MetaMask/core/pull/5172)) + ## [19.0.7] ### Changed @@ -660,7 +670,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@19.0.7...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.1.0...HEAD +[19.1.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.7...@metamask/keyring-controller@19.1.0 [19.0.7]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.6...@metamask/keyring-controller@19.0.7 [19.0.6]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.5...@metamask/keyring-controller@19.0.6 [19.0.5]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.4...@metamask/keyring-controller@19.0.5 diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index a40aa895ca3..406e12d3ad8 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/keyring-controller", - "version": "19.0.7", + "version": "19.1.0", "description": "Stores identities seen in the wallet and manages interactions such as signing", "keywords": [ "MetaMask", diff --git a/packages/multichain-transactions-controller/package.json b/packages/multichain-transactions-controller/package.json index b62ac865df6..13702a68a90 100644 --- a/packages/multichain-transactions-controller/package.json +++ b/packages/multichain-transactions-controller/package.json @@ -61,9 +61,9 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^23.1.0", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index 1fb0a09080b..54af5356df3 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -112,7 +112,7 @@ "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@metamask/profile-sync-controller": "^7.0.1", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", diff --git a/packages/preferences-controller/package.json b/packages/preferences-controller/package.json index dade9b723a6..2bcf9830665 100644 --- a/packages/preferences-controller/package.json +++ b/packages/preferences-controller/package.json @@ -52,7 +52,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index a9f2b6dc5b8..8e3a1a2d4c2 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -102,7 +102,7 @@ "dependencies": { "@metamask/base-controller": "^8.0.0", "@metamask/keyring-api": "^17.0.0", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@metamask/network-controller": "^22.2.1", "@metamask/snaps-sdk": "^6.17.1", "@metamask/snaps-utils": "^8.10.0", @@ -115,7 +115,7 @@ "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/preinstall-always-fail": "^2.1.0", - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^23.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-internal-api": "^4.0.1", "@metamask/providers": "^18.1.1", diff --git a/packages/signature-controller/package.json b/packages/signature-controller/package.json index e32a9b4c631..5735de1ad9f 100644 --- a/packages/signature-controller/package.json +++ b/packages/signature-controller/package.json @@ -58,7 +58,7 @@ "devDependencies": { "@metamask/approval-controller": "^7.1.3", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@metamask/logging-controller": "^6.0.4", "@metamask/network-controller": "^22.2.1", "@types/jest": "^27.4.1", diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 74b65af67ed..bf16e5a4b8d 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -69,7 +69,7 @@ }, "devDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^23.1.0", "@metamask/approval-controller": "^7.1.3", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-block-tracker": "^11.0.3", diff --git a/packages/user-operation-controller/package.json b/packages/user-operation-controller/package.json index a68ef61a585..f1750769049 100644 --- a/packages/user-operation-controller/package.json +++ b/packages/user-operation-controller/package.json @@ -65,7 +65,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-block-tracker": "^11.0.3", "@metamask/gas-fee-controller": "^22.0.3", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@metamask/network-controller": "^22.2.1", "@metamask/transaction-controller": "^45.1.0", "@types/jest": "^27.4.1", diff --git a/yarn.lock b/yarn.lock index 9ca303c8fa9..f6a87dc72a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2340,7 +2340,7 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^23.0.1, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^23.1.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: @@ -2349,7 +2349,7 @@ __metadata: "@metamask/base-controller": "npm:^8.0.0" "@metamask/eth-snap-keyring": "npm:^10.0.0" "@metamask/keyring-api": "npm:^17.0.0" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/keyring-internal-api": "npm:^4.0.1" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.19.0" @@ -2460,7 +2460,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/accounts-controller": "npm:^23.0.1" + "@metamask/accounts-controller": "npm:^23.1.0" "@metamask/approval-controller": "npm:^7.1.3" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" @@ -2469,7 +2469,7 @@ __metadata: "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-provider-http": "npm:^0.3.0" "@metamask/keyring-api": "npm:^17.0.0" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/keyring-internal-api": "npm:^4.0.1" "@metamask/keyring-snap-client": "npm:^3.0.3" "@metamask/metamask-eth-abis": "npm:^3.1.1" @@ -2750,7 +2750,7 @@ __metadata: resolution: "@metamask/earn-controller@workspace:packages/earn-controller" dependencies: "@ethersproject/providers": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^23.0.1" + "@metamask/accounts-controller": "npm:^23.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" @@ -3299,7 +3299,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.0.7, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^19.1.0, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -3455,11 +3455,11 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller" dependencies: - "@metamask/accounts-controller": "npm:^23.0.1" + "@metamask/accounts-controller": "npm:^23.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/keyring-api": "npm:^17.0.0" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/keyring-internal-api": "npm:^4.0.1" "@metamask/keyring-snap-client": "npm:^3.0.3" "@metamask/polling-controller": "npm:^12.0.3" @@ -3595,7 +3595,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/profile-sync-controller": "npm:^7.0.1" "@metamask/utils": "npm:^11.1.0" "@types/jest": "npm:^27.4.1" @@ -3763,7 +3763,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -3783,11 +3783,11 @@ __metadata: dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" - "@metamask/accounts-controller": "npm:^23.0.1" + "@metamask/accounts-controller": "npm:^23.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/keyring-api": "npm:^17.0.0" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/keyring-internal-api": "npm:^4.0.1" "@metamask/network-controller": "npm:^22.2.1" "@metamask/providers": "npm:^18.1.1" @@ -3974,7 +3974,7 @@ __metadata: "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" "@metamask/eth-sig-util": "npm:^8.0.0" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/logging-controller": "npm:^6.0.4" "@metamask/network-controller": "npm:^22.2.1" "@metamask/utils": "npm:^11.1.0" @@ -4167,7 +4167,7 @@ __metadata: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^23.0.1" + "@metamask/accounts-controller": "npm:^23.1.0" "@metamask/approval-controller": "npm:^7.1.3" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" @@ -4221,7 +4221,7 @@ __metadata: "@metamask/eth-block-tracker": "npm:^11.0.3" "@metamask/eth-query": "npm:^4.0.0" "@metamask/gas-fee-controller": "npm:^22.0.3" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/network-controller": "npm:^22.2.1" "@metamask/polling-controller": "npm:^12.0.3" "@metamask/rpc-errors": "npm:^7.0.2" From 8513027636895f4b5a293759c92e1de6b4dc72a5 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 12 Feb 2025 13:49:22 +0100 Subject: [PATCH 03/20] feat: expose all user storage methods through messenger actions (#5311) ## Explanation This PR exposes all (but the dangerous `deleteAllFeatureEntries`) user storage methods ## References Fixes: - https://github.com/MetaMask/core/issues/4937 - https://consensyssoftware.atlassian.net/browse/IDENTITY-29 ## Changelog ### `@metamask/profile-sync-controller` - **ADDED**: Added `performBatchSetStorage`, `performDeleteStorage` and `performBatchDeleteStorage` to the exposed messenger actions ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../user-storage/UserStorageController.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index a41be03d2a5..66f82d47777 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -192,6 +192,9 @@ type ActionsObj = CreateActionsObj< | 'performGetStorage' | 'performGetStorageAllFeatureEntries' | 'performSetStorage' + | 'performBatchSetStorage' + | 'performDeleteStorage' + | 'performBatchDeleteStorage' | 'getStorageKey' | 'enableProfileSyncing' | 'disableProfileSyncing' @@ -211,6 +214,12 @@ export type UserStorageControllerPerformGetStorageAllFeatureEntries = ActionsObj['performGetStorageAllFeatureEntries']; export type UserStorageControllerPerformSetStorage = ActionsObj['performSetStorage']; +export type UserStorageControllerPerformBatchSetStorage = + ActionsObj['performBatchSetStorage']; +export type UserStorageControllerPerformDeleteStorage = + ActionsObj['performDeleteStorage']; +export type UserStorageControllerPerformBatchDeleteStorage = + ActionsObj['performBatchDeleteStorage']; export type UserStorageControllerGetStorageKey = ActionsObj['getStorageKey']; export type UserStorageControllerEnableProfileSyncing = ActionsObj['enableProfileSyncing']; @@ -424,6 +433,21 @@ export default class UserStorageController extends BaseController< this.performSetStorage.bind(this), ); + this.messagingSystem.registerActionHandler( + 'UserStorageController:performBatchSetStorage', + this.performBatchSetStorage.bind(this), + ); + + this.messagingSystem.registerActionHandler( + 'UserStorageController:performDeleteStorage', + this.performDeleteStorage.bind(this), + ); + + this.messagingSystem.registerActionHandler( + 'UserStorageController:performBatchDeleteStorage', + this.performBatchDeleteStorage.bind(this), + ); + this.messagingSystem.registerActionHandler( 'UserStorageController:getStorageKey', this.getStorageKey.bind(this), From 1f7e0b9134f274c11d9611f9ede37aa1383533d1 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 12 Feb 2025 13:58:43 +0100 Subject: [PATCH 04/20] Fix invalid type import path in `@metamask/multichain` (#5313) ## Explanation `@metamask/multichain` was importing `src/scope/types`, which does not work when importing the library in other projects. I've changed it to `../scope/types` instead. ## Changelog ### `@metamask/multichain` - **FIXED**: Fix invalid type import path ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/multichain/src/handlers/wallet-getSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain/src/handlers/wallet-getSession.ts b/packages/multichain/src/handlers/wallet-getSession.ts index bf343495091..5e04bf9ed03 100644 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -1,6 +1,5 @@ import type { Caveat } from '@metamask/permission-controller'; import type { JsonRpcRequest, JsonRpcSuccess } from '@metamask/utils'; -import type { NormalizedScopesObject } from 'src/scope/types'; import { getSessionScopes } from '../adapters/caip-permission-adapter-session-scopes'; import type { Caip25CaveatValue } from '../caip25Permission'; @@ -8,6 +7,7 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; +import type { NormalizedScopesObject } from '../scope/types'; /** * Handler for the `wallet_getSession` RPC method as specified by [CAIP-312](https://chainagnostic.org/CAIPs/caip-312). From e957d35229755e0cc962f6001526ce580a1746a5 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 12 Feb 2025 10:04:37 -0330 Subject: [PATCH 05/20] chore: Change ownership of SelectedNetworkController (#5312) ## Explanation The package `@metamask/selected-network-controller` is now owned solely by the Wallet API Platform team, rather than being shared between multiple teams. This team has the most context for what this code does, and it will in the future be increasingly less relevant to the rest of the wallet apart from supporting the EIP-1193 provider. ## References N/A ## Changelog N/A ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 10fcf481d94..159a42b2133 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -43,6 +43,7 @@ ## Wallet API Platform Team /packages/multichain @MetaMask/wallet-api-platform-engineers /packages/queued-request-controller @MetaMask/wallet-api-platform-engineers +/packages/selected-network-controller @MetaMask/wallet-api-platform-engineers ## Wallet Framework Team /packages/base-controller @MetaMask/wallet-framework-engineers @@ -62,7 +63,6 @@ /packages/network-controller @MetaMask/wallet-framework-engineers @MetaMask/metamask-assets /packages/permission-controller @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers @MetaMask/snaps-devs /packages/permission-log-controller @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers -/packages/selected-network-controller @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers @MetaMask/metamask-assets /packages/profile-sync-controller @MetaMask/notifications @MetaMask/identity ## Package Release related From 8acf5d78c1f7147536c57dfe32001f045b81750b Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Wed, 12 Feb 2025 09:43:13 -0700 Subject: [PATCH 06/20] RpcService: Regard node-fetch errors as retriable (#5298) Currently we have tests for middleware to verify that requests get retried when certain errors are thrown. In a future commit we will replace the retry logic in these middleware with RpcService, and when this happens the aforementioned tests will break. This is because we are using Nock to force the request to throw errors, and in tests we use `node-fetch` to polyfill the `fetch` function, and `node-fetch` errors are not regarded as retriable by RpcService. This commit adjusts RpcService to treat `node-fetch` errors as retriable (but making sure to exclude errors from Nock itself, since they will also manifest as `node-fetch` errors). --- packages/network-controller/package.json | 2 + .../src/rpc-service/rpc-service.test.ts | 210 +++++++++++++++++- .../src/rpc-service/rpc-service.ts | 95 ++++++-- yarn.lock | 14 +- 4 files changed, 293 insertions(+), 28 deletions(-) diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 6fd54838ce4..3f8449348e6 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -72,11 +72,13 @@ "@types/jest": "^27.4.1", "@types/jest-when": "^2.7.3", "@types/lodash": "^4.14.191", + "@types/node-fetch": "^2.6.12", "deepmerge": "^4.2.2", "jest": "^27.5.1", "jest-when": "^3.4.2", "lodash": "^4.17.21", "nock": "^13.3.1", + "node-fetch": "^2.7.0", "sinon": "^9.2.4", "ts-jest": "^27.1.4", "typedoc": "^0.24.8", diff --git a/packages/network-controller/src/rpc-service/rpc-service.test.ts b/packages/network-controller/src/rpc-service/rpc-service.test.ts index ffc148c4a4a..3919a2623ab 100644 --- a/packages/network-controller/src/rpc-service/rpc-service.test.ts +++ b/packages/network-controller/src/rpc-service/rpc-service.test.ts @@ -3,11 +3,12 @@ import { rpcErrors } from '@metamask/rpc-errors'; import nock from 'nock'; +import { FetchError } from 'node-fetch'; import { useFakeTimers } from 'sinon'; import type { SinonFakeTimers } from 'sinon'; import type { AbstractRpcService } from './abstract-rpc-service'; -import { NETWORK_UNREACHABLE_ERRORS, RpcService } from './rpc-service'; +import { RpcService } from './rpc-service'; import { DEFAULT_CIRCUIT_BREAK_DURATION } from '../../../controller-utils/src/create-service-policy'; describe('RpcService', () => { @@ -22,10 +23,58 @@ describe('RpcService', () => { }); describe('request', () => { - describe.each([...NETWORK_UNREACHABLE_ERRORS].slice(0, 1))( - `if making the request throws a "%s" error (as a "network unreachable" error)`, - (errorMessage) => { - const error = new TypeError(errorMessage); + // NOTE: Keep this list synced with CONNECTION_ERRORS + describe.each([ + { + constructorName: 'TypeError', + message: 'network error', + }, + { + constructorName: 'TypeError', + message: 'Failed to fetch', + }, + { + constructorName: 'TypeError', + message: 'NetworkError when attempting to fetch resource.', + }, + { + constructorName: 'TypeError', + message: 'The Internet connection appears to be offline.', + }, + { + constructorName: 'TypeError', + message: 'Load failed', + }, + { + constructorName: 'TypeError', + message: 'Network request failed', + }, + { + constructorName: 'FetchError', + message: 'request to https://foo.com failed', + }, + { + constructorName: 'TypeError', + message: 'fetch failed', + }, + { + constructorName: 'TypeError', + message: 'terminated', + }, + ])( + `if making the request throws the $message error`, + ({ constructorName, message }) => { + let error; + switch (constructorName) { + case 'FetchError': + error = new FetchError(message, 'system'); + break; + case 'TypeError': + error = new TypeError(message); + break; + default: + throw new Error(`Unknown constructor ${constructorName}`); + } testsForRetriableFetchErrors({ getClock: () => clock, producedError: error, @@ -59,6 +108,145 @@ describe('RpcService', () => { }, ); + describe('if the endpoint URL was not mocked via Nock', () => { + it('re-throws the error without retrying the request', async () => { + const service = new RpcService({ + fetch, + btoa, + endpointUrl: 'https://rpc.example.chain', + }); + + const promise = service.request({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }); + await expect(promise).rejects.toThrow('Nock: Disallowed net connect'); + }); + + it('does not forward the request to a failover service if given one', async () => { + const failoverService = buildMockRpcService(); + const service = new RpcService({ + fetch, + btoa, + endpointUrl: 'https://rpc.example.chain', + failoverService, + }); + + const jsonRpcRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'eth_chainId', + params: [], + }; + await ignoreRejection(service.request(jsonRpcRequest)); + expect(failoverService.request).not.toHaveBeenCalled(); + }); + + it('does not call onBreak', async () => { + const onBreakListener = jest.fn(); + const service = new RpcService({ + fetch, + btoa, + endpointUrl: 'https://rpc.example.chain', + }); + service.onBreak(onBreakListener); + + const promise = service.request({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }); + await ignoreRejection(promise); + expect(onBreakListener).not.toHaveBeenCalled(); + }); + }); + + describe('if the endpoint URL was mocked via Nock, but not the RPC method', () => { + it('re-throws the error without retrying the request', async () => { + const endpointUrl = 'https://rpc.example.chain'; + nock(endpointUrl) + .post('/', { + id: 1, + jsonrpc: '2.0', + method: 'eth_incorrectMethod', + params: [], + }) + .reply(500); + const service = new RpcService({ + fetch, + btoa, + endpointUrl, + }); + + const promise = service.request({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }); + await expect(promise).rejects.toThrow('Nock: No match for request'); + }); + + it('does not forward the request to a failover service if given one', async () => { + const endpointUrl = 'https://rpc.example.chain'; + nock(endpointUrl) + .post('/', { + id: 1, + jsonrpc: '2.0', + method: 'eth_incorrectMethod', + params: [], + }) + .reply(500); + const failoverService = buildMockRpcService(); + const service = new RpcService({ + fetch, + btoa, + endpointUrl, + failoverService, + }); + + const jsonRpcRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'eth_chainId', + params: [], + }; + await ignoreRejection(service.request(jsonRpcRequest)); + expect(failoverService.request).not.toHaveBeenCalled(); + }); + + it('does not call onBreak', async () => { + const endpointUrl = 'https://rpc.example.chain'; + nock(endpointUrl) + .post('/', { + id: 1, + jsonrpc: '2.0', + method: 'eth_incorrectMethod', + params: [], + }) + .reply(500); + const onBreakListener = jest.fn(); + const service = new RpcService({ + fetch, + btoa, + endpointUrl, + }); + service.onBreak(onBreakListener); + + const promise = service.request({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }); + await ignoreRejection(promise); + expect(onBreakListener).not.toHaveBeenCalled(); + }); + }); + describe('if making the request throws an unknown error', () => { it('re-throws the error without retrying the request', async () => { const error = new Error('oops'); @@ -129,7 +317,7 @@ describe('RpcService', () => { }); describe.each([503, 504])( - 'if the endpoint consistently has a %d response', + 'if the endpoint has a %d response', (httpStatus) => { testsForRetriableResponses({ getClock: () => clock, @@ -191,7 +379,7 @@ describe('RpcService', () => { const jsonRpcRequest = { id: 1, jsonrpc: '2.0' as const, - method: 'eth_chainId', + method: 'eth_unknownMethod', params: [], }; await ignoreRejection(service.request(jsonRpcRequest)); @@ -219,7 +407,7 @@ describe('RpcService', () => { const promise = service.request({ id: 1, jsonrpc: '2.0', - method: 'eth_chainId', + method: 'eth_unknownMethod', params: [], }); await ignoreRejection(promise); @@ -259,7 +447,7 @@ describe('RpcService', () => { .post('/', { id: 1, jsonrpc: '2.0', - method: 'eth_unknownMethod', + method: 'eth_chainId', params: [], }) .reply(429); @@ -287,7 +475,7 @@ describe('RpcService', () => { .post('/', { id: 1, jsonrpc: '2.0', - method: 'eth_unknownMethod', + method: 'eth_chainId', params: [], }) .reply(429); @@ -355,7 +543,7 @@ describe('RpcService', () => { .post('/', { id: 1, jsonrpc: '2.0', - method: 'eth_unknownMethod', + method: 'eth_chainId', params: [], }) .reply(500, { diff --git a/packages/network-controller/src/rpc-service/rpc-service.ts b/packages/network-controller/src/rpc-service/rpc-service.ts index 69998aa6b0c..3ed12715ff6 100644 --- a/packages/network-controller/src/rpc-service/rpc-service.ts +++ b/packages/network-controller/src/rpc-service/rpc-service.ts @@ -18,21 +18,58 @@ import type { AbstractRpcService } from './abstract-rpc-service'; import type { AddToCockatielEventData, FetchOptions } from './shared'; /** - * The list of error messages that represent a failure to reach the network. + * The list of error messages that represent a failure to connect to the network. * * This list was derived from Sindre Sorhus's `is-network-error` package: * */ -export const NETWORK_UNREACHABLE_ERRORS = new Set([ - 'network error', // Chrome - 'Failed to fetch', // Chrome - 'NetworkError when attempting to fetch resource.', // Firefox - 'The Internet connection appears to be offline.', // Safari 16 - 'Load failed', // Safari 17+ - 'Network request failed', // `cross-fetch` - 'fetch failed', // Undici (Node.js) - 'terminated', // Undici (Node.js) -]); +export const CONNECTION_ERRORS = [ + // Chrome + { + constructorName: 'TypeError', + pattern: /network error/u, + }, + // Chrome + { + constructorName: 'TypeError', + pattern: /Failed to fetch/u, + }, + // Firefox + { + constructorName: 'TypeError', + pattern: /NetworkError when attempting to fetch resource\./u, + }, + // Safari 16 + { + constructorName: 'TypeError', + pattern: /The Internet connection appears to be offline\./u, + }, + // Safari 17+ + { + constructorName: 'TypeError', + pattern: /Load failed/u, + }, + // `cross-fetch` + { + constructorName: 'TypeError', + pattern: /Network request failed/u, + }, + // `node-fetch` + { + constructorName: 'FetchError', + pattern: /request to (.+) failed/u, + }, + // Undici (Node.js) + { + constructorName: 'TypeError', + pattern: /fetch failed/u, + }, + // Undici (Node.js) + { + constructorName: 'TypeError', + pattern: /terminated/u, + }, +]; /** * Determines whether the given error represents a failure to reach the network @@ -43,15 +80,41 @@ export const NETWORK_UNREACHABLE_ERRORS = new Set([ * particular scenario, and we need to account for this. * * @param error - The error. - * @returns True if the error indicates that the network is unreachable, and - * false otherwise. + * @returns True if the error indicates that the network cannot be connected to, + * and false otherwise. */ -export default function isNetworkUnreachableError(error: unknown) { +export default function isConnectionError(error: unknown) { + if (!(typeof error === 'object' && error !== null && 'message' in error)) { + return false; + } + + const { message } = error; + return ( - error instanceof TypeError && NETWORK_UNREACHABLE_ERRORS.has(error.message) + typeof message === 'string' && + !isNockError(message) && + CONNECTION_ERRORS.some(({ constructorName, pattern }) => { + return ( + error.constructor.name === constructorName && pattern.test(message) + ); + }) ); } +/** + * Determines whether the given error message refers to a Nock error. + * + * It's important that if we failed to mock a request in a test, the resulting + * error does not cause the request to be retried so that we can see it right + * away. + * + * @param message - The error message to test. + * @returns True if the message indicates a missing Nock mock, false otherwise. + */ +function isNockError(message: string) { + return message.includes('Nock:'); +} + /** * Guarantees a URL, even given a string. This is useful for checking components * of that URL. @@ -145,7 +208,7 @@ export class RpcService implements AbstractRpcService { retryFilterPolicy: handleWhen((error) => { return ( // Ignore errors where the request failed to establish - isNetworkUnreachableError(error) || + isConnectionError(error) || // Ignore server sent HTML error pages or truncated JSON responses error.message.includes('not valid JSON') || // Ignore server overload errors diff --git a/yarn.lock b/yarn.lock index f6a87dc72a9..774f0b6fe8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3553,6 +3553,7 @@ __metadata: "@types/jest": "npm:^27.4.1" "@types/jest-when": "npm:^2.7.3" "@types/lodash": "npm:^4.14.191" + "@types/node-fetch": "npm:^2.6.12" async-mutex: "npm:^0.5.0" deepmerge: "npm:^4.2.2" fast-deep-equal: "npm:^3.1.3" @@ -3562,6 +3563,7 @@ __metadata: lodash: "npm:^4.17.21" loglevel: "npm:^1.8.1" nock: "npm:^13.3.1" + node-fetch: "npm:^2.7.0" reselect: "npm:^5.1.1" sinon: "npm:^9.2.4" ts-jest: "npm:^27.1.4" @@ -5028,6 +5030,16 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.6.12": + version: 2.6.12 + resolution: "@types/node-fetch@npm:2.6.12" + dependencies: + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: 10/8107c479da83a3114fcbfa882eba95ee5175cccb5e4dd53f737a96f2559ae6262f662176b8457c1656de09ec393cc7b20a266c077e4bfb21e929976e1cf4d0f9 + languageName: node + linkType: hard + "@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": version: 22.5.0 resolution: "@types/node@npm:22.5.0" @@ -11018,7 +11030,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.1": +"node-fetch@npm:^2.6.1, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: From ce1248513358d2651919d4e55d6e3fd9678a61f3 Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Wed, 12 Feb 2025 11:20:35 -0800 Subject: [PATCH 07/20] feat: Add `MultichainNetworkController` to handle both EVM and non-EVM network and account switching (#5215) ## Explanation This PR updates both the MultichainNetworkController and AccountsController to handle network switching as well as account switching. The logic handles the following logic: - Switching accounts on AccountsController will notify MultichainNetworkController to update if the account belongs to evm vs non-evm network (MultichainNetworkController subscribes to AccountsController event) - Switching between networks on MultichainNetworkController will notify AccountsController to update accounts based on which network the account belongs to (AccountsController subscribes to MultichainNetworkController event) ## References Fixes https://github.com/MetaMask/accounts-planning/issues/804 ## Changelog ### `@metamask/accounts-controller` - **BREAKING**: - Added `MultichainNetworkController:networkDidChange` to allowed events. This is used to subscribe to the `setActiveNetwork` event from the `MultichainNetworkController` and is responsible for updating selected account based on network changes (both EVM and non-EVM). ### `@metamask/multichain-network-controller` - **ADDED**: - Allowed actions - `NetworkControllerGetStateAction` | `NetworkControllerSetActiveNetworkAction`. The `MultichainNetworkController` acts as a proxy for the `NetworkController` and will update it based on EVM network changes. - Allowed events - `AccountsControllerSelectedAccountChangeEvent` to allowed events. This is used to subscribe to the `selectedAccountChange` event from the `AccountsController` and is responsible for updating active network based on account changes (both EVM and non-EVM). ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: gantunesr <17601467+gantunesr@users.noreply.github.com> Co-authored-by: tommasini Co-authored-by: Elliot Winkler Co-authored-by: Charly Chevalier --- README.md | 16 + eslint-warning-thresholds.json | 3 - packages/accounts-controller/package.json | 4 +- .../src/AccountsController.test.ts | 156 +++++--- .../src/AccountsController.ts | 174 +++++---- .../accounts-controller/src/tests/mocks.ts | 8 +- packages/accounts-controller/src/types.ts | 10 + packages/accounts-controller/src/utils.ts | 3 + .../accounts-controller/tsconfig.build.json | 3 +- packages/accounts-controller/tsconfig.json | 5 +- .../CHANGELOG.md | 10 + .../multichain-network-controller/LICENSE | 20 + .../multichain-network-controller/README.md | 15 + .../jest.config.js | 26 ++ .../package.json | 80 ++++ .../src/MultichainNetworkController.test.ts | 358 ++++++++++++++++++ .../src/MultichainNetworkController.ts | 204 ++++++++++ .../src/constants.ts | 74 ++++ .../src/index.ts | 24 ++ .../src/types.ts | 178 +++++++++ .../src/utils.test.ts | 114 ++++++ .../src/utils.ts | 93 +++++ .../tests/utils.ts | 98 +++++ .../tsconfig.build.json | 14 + .../tsconfig.json | 12 + .../typedoc.json | 7 + teams.json | 1 + tsconfig.build.json | 3 +- tsconfig.json | 1 + yarn.lock | 113 +++++- 30 files changed, 1688 insertions(+), 139 deletions(-) create mode 100644 packages/accounts-controller/src/types.ts create mode 100644 packages/multichain-network-controller/CHANGELOG.md create mode 100644 packages/multichain-network-controller/LICENSE create mode 100644 packages/multichain-network-controller/README.md create mode 100644 packages/multichain-network-controller/jest.config.js create mode 100644 packages/multichain-network-controller/package.json create mode 100644 packages/multichain-network-controller/src/MultichainNetworkController.test.ts create mode 100644 packages/multichain-network-controller/src/MultichainNetworkController.ts create mode 100644 packages/multichain-network-controller/src/constants.ts create mode 100644 packages/multichain-network-controller/src/index.ts create mode 100644 packages/multichain-network-controller/src/types.ts create mode 100644 packages/multichain-network-controller/src/utils.test.ts create mode 100644 packages/multichain-network-controller/src/utils.ts create mode 100644 packages/multichain-network-controller/tests/utils.ts create mode 100644 packages/multichain-network-controller/tsconfig.build.json create mode 100644 packages/multichain-network-controller/tsconfig.json create mode 100644 packages/multichain-network-controller/typedoc.json diff --git a/README.md b/README.md index 9645d28513e..6ffab5f882b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/logging-controller`](packages/logging-controller) - [`@metamask/message-manager`](packages/message-manager) - [`@metamask/multichain`](packages/multichain) +- [`@metamask/multichain-network-controller`](packages/multichain-network-controller) - [`@metamask/multichain-transactions-controller`](packages/multichain-transactions-controller) - [`@metamask/name-controller`](packages/name-controller) - [`@metamask/network-controller`](packages/network-controller) @@ -85,6 +86,7 @@ linkStyle default opacity:0.5 logging_controller(["@metamask/logging-controller"]); message_manager(["@metamask/message-manager"]); multichain(["@metamask/multichain"]); + multichain_network_controller(["@metamask/multichain-network-controller"]); multichain_transactions_controller(["@metamask/multichain-transactions-controller"]); name_controller(["@metamask/name-controller"]); network_controller(["@metamask/network-controller"]); @@ -105,6 +107,7 @@ linkStyle default opacity:0.5 user_operation_controller(["@metamask/user-operation-controller"]); accounts_controller --> base_controller; accounts_controller --> keyring_controller; + accounts_controller --> network_controller; address_book_controller --> base_controller; address_book_controller --> controller_utils; announcement_controller --> base_controller; @@ -116,10 +119,15 @@ linkStyle default opacity:0.5 assets_controllers --> approval_controller; assets_controllers --> keyring_controller; assets_controllers --> network_controller; + assets_controllers --> permission_controller; assets_controllers --> preferences_controller; base_controller --> json_rpc_engine; composable_controller --> base_controller; composable_controller --> json_rpc_engine; + earn_controller --> base_controller; + earn_controller --> controller_utils; + earn_controller --> accounts_controller; + earn_controller --> network_controller; ens_controller --> base_controller; ens_controller --> controller_utils; ens_controller --> network_controller; @@ -136,8 +144,15 @@ linkStyle default opacity:0.5 message_manager --> base_controller; message_manager --> controller_utils; multichain --> controller_utils; + multichain --> json_rpc_engine; multichain --> network_controller; multichain --> permission_controller; + multichain_network_controller --> base_controller; + multichain_network_controller --> keyring_controller; + multichain_transactions_controller --> base_controller; + multichain_transactions_controller --> polling_controller; + multichain_transactions_controller --> accounts_controller; + multichain_transactions_controller --> keyring_controller; name_controller --> base_controller; name_controller --> controller_utils; network_controller --> base_controller; @@ -184,6 +199,7 @@ linkStyle default opacity:0.5 signature_controller --> keyring_controller; signature_controller --> logging_controller; signature_controller --> network_controller; + token_search_discovery_controller --> base_controller; transaction_controller --> base_controller; transaction_controller --> controller_utils; transaction_controller --> accounts_controller; diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index e777270690c..4b387a4f112 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -16,9 +16,6 @@ "packages/accounts-controller/src/AccountsController.test.ts": { "import-x/namespace": 1 }, - "packages/accounts-controller/src/utils.ts": { - "jsdoc/tag-lines": 3 - }, "packages/address-book-controller/src/AddressBookController.ts": { "jsdoc/check-tag-names": 13 }, diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 2f140161fbb..9260adcd998 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -63,6 +63,7 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.1.0", + "@metamask/network-controller": "^22.2.1", "@metamask/providers": "^18.1.1", "@metamask/snaps-controllers": "^9.19.0", "@types/jest": "^27.4.1", @@ -75,7 +76,8 @@ "webextension-polyfill": "^0.12.0" }, "peerDependencies": { - "@metamask/keyring-controller": "^19.0.0", + "@metamask/keyring-controller": "^19.1.0", + "@metamask/network-controller": "^22.2.1", "@metamask/providers": "^18.1.0", "@metamask/snaps-controllers": "^9.19.0", "webextension-polyfill": "^0.10.0 || ^0.11.0 || ^0.12.0" diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 9df36e855a1..ad80a09febf 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1,4 +1,5 @@ import { Messenger } from '@metamask/base-controller'; +import { InfuraNetworkType } from '@metamask/controller-utils'; import type { AccountAssetListUpdatedEventPayload, AccountBalancesUpdatedEventPayload, @@ -17,6 +18,7 @@ import type { InternalAccount, InternalAccountType, } from '@metamask/keyring-internal-api'; +import type { NetworkClientId } from '@metamask/network-controller'; import type { SnapControllerState } from '@metamask/snaps-controllers'; import { SnapStatus } from '@metamask/snaps-utils'; import type { CaipChainId } from '@metamask/utils'; @@ -307,6 +309,7 @@ function buildAccountsControllerMessenger(messenger = buildMessenger()) { 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', + 'MultichainNetworkController:networkDidChange', ], allowedActions: [ 'KeyringController:getAccounts', @@ -339,6 +342,7 @@ function setupAccountsController({ AccountsControllerActions | AllowedActions, AccountsControllerEvents | AllowedEvents >; + triggerMultichainNetworkChange: (id: NetworkClientId | CaipChainId) => void; } { const accountsControllerMessenger = buildAccountsControllerMessenger(messenger); @@ -347,10 +351,37 @@ function setupAccountsController({ messenger: accountsControllerMessenger, state: { ...defaultState, ...initialState }, }); - return { accountsController, messenger }; + + const triggerMultichainNetworkChange = (id: NetworkClientId | CaipChainId) => + messenger.publish('MultichainNetworkController:networkDidChange', id); + + return { accountsController, messenger, triggerMultichainNetworkChange }; } describe('AccountsController', () => { + const mockBtcAccount = createExpectedInternalAccount({ + id: 'mock-non-evm', + name: 'non-evm', + address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', + keyringType: KeyringTypes.snap, + type: BtcAccountType.P2wpkh, + }); + + const mockOlderEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-1', + name: 'mock account 1', + address: 'mock-address-1', + keyringType: KeyringTypes.hd, + lastSelected: 11111, + }); + const mockNewerEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-2', + name: 'mock account 2', + address: 'mock-address-2', + keyringType: KeyringTypes.hd, + lastSelected: 22222, + }); + describe('onSnapStateChange', () => { it('be used enable an account if the Snap is enabled and not blocked', async () => { const messenger = buildMessenger(); @@ -1514,6 +1545,59 @@ describe('AccountsController', () => { }); }); + describe('handle MultichainNetworkController:networkDidChange event', () => { + it('should update selected account to non-EVM account when switching to non-EVM network', () => { + const messenger = buildMessenger(); + const { accountsController, triggerMultichainNetworkChange } = + setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockOlderEvmAccount.id]: mockOlderEvmAccount, + [mockNewerEvmAccount.id]: mockNewerEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, + }, + selectedAccount: mockNewerEvmAccount.id, + }, + }, + messenger, + }); + + // Triggered from network switch to Bitcoin mainnet + triggerMultichainNetworkChange(BtcScope.Mainnet); + + // BTC account is now selected + expect(accountsController.state.internalAccounts.selectedAccount).toBe( + mockBtcAccount.id, + ); + }); + + it('should update selected account to EVM account when switching to EVM network', () => { + const messenger = buildMessenger(); + const { accountsController, triggerMultichainNetworkChange } = + setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockOlderEvmAccount.id]: mockOlderEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, + }, + selectedAccount: mockBtcAccount.id, + }, + }, + messenger, + }); + + // Triggered from network switch to Bitcoin mainnet + triggerMultichainNetworkChange(InfuraNetworkType.mainnet); + + // ETH mainnet account is now selected + expect(accountsController.state.internalAccounts.selectedAccount).toBe( + mockOlderEvmAccount.id, + ); + }); + }); + describe('updateAccounts', () => { const mockAddress1 = '0x123'; const mockAddress2 = '0x456'; @@ -2145,29 +2229,6 @@ describe('AccountsController', () => { }); describe('getSelectedAccount', () => { - const mockNonEvmAccount = createExpectedInternalAccount({ - id: 'mock-non-evm', - name: 'non-evm', - address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', - keyringType: KeyringTypes.snap, - type: BtcAccountType.P2wpkh, - }); - - const mockOlderEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-1', - name: 'mock account 1', - address: 'mock-address-1', - keyringType: KeyringTypes.hd, - lastSelected: 11111, - }); - const mockNewerEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-2', - name: 'mock account 2', - address: 'mock-address-2', - keyringType: KeyringTypes.hd, - lastSelected: 22222, - }); - it.each([ { lastSelectedAccount: mockNewerEvmAccount, @@ -2178,7 +2239,7 @@ describe('AccountsController', () => { expected: mockOlderEvmAccount, }, { - lastSelectedAccount: mockNonEvmAccount, + lastSelectedAccount: mockBtcAccount, expected: mockNewerEvmAccount, }, ])( @@ -2190,7 +2251,7 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: lastSelectedAccount.id, }, @@ -2206,9 +2267,9 @@ describe('AccountsController', () => { initialState: { internalAccounts: { accounts: { - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, - selectedAccount: mockNonEvmAccount.id, + selectedAccount: mockBtcAccount.id, }, }, }); @@ -2235,29 +2296,6 @@ describe('AccountsController', () => { }); describe('getSelectedMultichainAccount', () => { - const mockNonEvmAccount = createExpectedInternalAccount({ - id: 'mock-non-evm', - name: 'non-evm', - address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', - keyringType: KeyringTypes.snap, - type: BtcAccountType.P2wpkh, - }); - - const mockOlderEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-1', - name: 'mock account 1', - address: 'mock-address-1', - keyringType: KeyringTypes.hd, - lastSelected: 11111, - }); - const mockNewerEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-2', - name: 'mock account 2', - address: 'mock-address-2', - keyringType: KeyringTypes.hd, - lastSelected: 22222, - }); - it.each([ { chainId: undefined, @@ -2266,18 +2304,18 @@ describe('AccountsController', () => { }, { chainId: undefined, - selectedAccount: mockNonEvmAccount, - expected: mockNonEvmAccount, + selectedAccount: mockBtcAccount, + expected: mockBtcAccount, }, { chainId: 'eip155:1', - selectedAccount: mockNonEvmAccount, + selectedAccount: mockBtcAccount, expected: mockNewerEvmAccount, }, { chainId: 'bip122:000000000019d6689c085ae165831e93', - selectedAccount: mockNonEvmAccount, - expected: mockNonEvmAccount, + selectedAccount: mockBtcAccount, + expected: mockBtcAccount, }, ])( "chainId $chainId with selectedAccount '$selectedAccount.id' should return $expected.id", @@ -2288,7 +2326,7 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: selectedAccount.id, }, @@ -2313,9 +2351,9 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, - selectedAccount: mockNonEvmAccount.id, + selectedAccount: mockBtcAccount.id, }, }, }); diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index f81f77565b1..4c582b03ab1 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -1,46 +1,47 @@ -import type { - ControllerGetStateAction, - ControllerStateChangeEvent, - ExtractEventPayload, - RestrictedMessenger, +import { + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type ExtractEventPayload, + type RestrictedMessenger, + BaseController, } from '@metamask/base-controller'; -import { BaseController } from '@metamask/base-controller'; -import type { - SnapKeyringAccountAssetListUpdatedEvent, - SnapKeyringAccountBalancesUpdatedEvent, - SnapKeyringAccountTransactionsUpdatedEvent, +import { + type SnapKeyringAccountAssetListUpdatedEvent, + type SnapKeyringAccountBalancesUpdatedEvent, + type SnapKeyringAccountTransactionsUpdatedEvent, + SnapKeyring, } from '@metamask/eth-snap-keyring'; -import { SnapKeyring } from '@metamask/eth-snap-keyring'; import { EthAccountType, EthMethod, EthScope, isEvmAccountType, } from '@metamask/keyring-api'; -import { KeyringTypes } from '@metamask/keyring-controller'; -import type { - KeyringControllerState, - KeyringControllerGetKeyringForAccountAction, - KeyringControllerGetKeyringsByTypeAction, - KeyringControllerGetAccountsAction, - KeyringControllerStateChangeEvent, +import { + type KeyringControllerState, + type KeyringControllerGetKeyringForAccountAction, + type KeyringControllerGetKeyringsByTypeAction, + type KeyringControllerGetAccountsAction, + type KeyringControllerStateChangeEvent, + KeyringTypes, } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { NetworkClientId } from '@metamask/network-controller'; import type { SnapControllerState, SnapStateChange, } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import type { Snap } from '@metamask/snaps-utils'; -import type { CaipChainId } from '@metamask/utils'; import { type Keyring, type Json, + type CaipChainId, isCaipChainId, parseCaipChainId, } from '@metamask/utils'; -import type { Draft } from 'immer'; +import type { MultichainNetworkControllerNetworkDidChangeEvent } from './types'; import { getUUIDFromAddressOfNormalAccount, isNormalKeyringType, @@ -187,7 +188,8 @@ export type AllowedEvents = | KeyringControllerStateChangeEvent | SnapKeyringAccountAssetListUpdatedEvent | SnapKeyringAccountBalancesUpdatedEvent - | SnapKeyringAccountTransactionsUpdatedEvent; + | SnapKeyringAccountTransactionsUpdatedEvent + | MultichainNetworkControllerNetworkDidChangeEvent; export type AccountsControllerEvents = | AccountsControllerChangeEvent @@ -280,43 +282,7 @@ export class AccountsController extends BaseController< }, }); - this.messagingSystem.subscribe( - 'SnapController:stateChange', - (snapStateState) => this.#handleOnSnapStateChange(snapStateState), - ); - - this.messagingSystem.subscribe( - 'KeyringController:stateChange', - (keyringState) => this.#handleOnKeyringStateChange(keyringState), - ); - - this.messagingSystem.subscribe( - 'SnapKeyring:accountAssetListUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountAssetListUpdated', - snapAccountEvent, - ), - ); - - this.messagingSystem.subscribe( - 'SnapKeyring:accountBalancesUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountBalancesUpdated', - snapAccountEvent, - ), - ); - - this.messagingSystem.subscribe( - 'SnapKeyring:accountTransactionsUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountTransactionsUpdated', - snapAccountEvent, - ), - ); - + this.#subscribeToMessageEvents(); this.#registerMessageHandlers(); } @@ -460,7 +426,7 @@ export class AccountsController extends BaseController< setSelectedAccount(accountId: string): void { const account = this.getAccountExpect(accountId); - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts.accounts[account.id].metadata.lastSelected = Date.now(); currentState.internalAccounts.selectedAccount = account.id; @@ -508,7 +474,7 @@ export class AccountsController extends BaseController< throw new Error('Account name already exists'); } - this.update((currentState: Draft) => { + this.update((currentState) => { const internalAccount = { ...account, metadata: { ...account.metadata, ...metadata }, @@ -584,7 +550,7 @@ export class AccountsController extends BaseController< {} as Record, ); - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts.accounts = accounts; if ( @@ -618,7 +584,7 @@ export class AccountsController extends BaseController< */ loadBackup(backup: AccountsControllerState): void { if (backup.internalAccounts) { - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts = backup.internalAccounts; }); } @@ -866,7 +832,7 @@ export class AccountsController extends BaseController< } } - this.update((currentState: Draft) => { + this.update((currentState) => { if (deletedAccounts.length > 0) { for (const account of deletedAccounts) { currentState.internalAccounts.accounts = this.#handleAccountRemoved( @@ -928,7 +894,7 @@ export class AccountsController extends BaseController< (account) => account.metadata.snap, ); - this.update((currentState: Draft) => { + this.update((currentState) => { accounts.forEach((account) => { const currentAccount = currentState.internalAccounts.accounts[account.id]; @@ -1160,6 +1126,36 @@ export class AccountsController extends BaseController< return accountsState; } + /** + * Handles the change in multichain network by updating the selected account. + * + * @param id - The EVM client ID or non-EVM chain ID that changed. + */ + readonly #handleMultichainNetworkChange = ( + id: NetworkClientId | CaipChainId, + ) => { + let accountId: string; + + // We only support non-EVM Caip chain IDs at the moment. Ex Solana and Bitcoin + // MultichainNetworkController will handle throwing an error if the Caip chain ID is not supported + if (isCaipChainId(id)) { + // Update selected account to non evm account + const lastSelectedNonEvmAccount = this.getSelectedMultichainAccount(id); + // @ts-expect-error - This should never be undefined, otherwise it's a bug that should be handled + accountId = lastSelectedNonEvmAccount.id; + } else { + // Update selected account to evm account + const lastSelectedEvmAccount = this.getSelectedAccount(); + accountId = lastSelectedEvmAccount.id; + } + + this.update((currentState) => { + currentState.internalAccounts.accounts[accountId].metadata.lastSelected = + Date.now(); + currentState.internalAccounts.selectedAccount = accountId; + }); + }; + /** * Retrieves the value of a specific metadata key for an existing account. * @@ -1169,7 +1165,6 @@ export class AccountsController extends BaseController< * @returns The value of the specified metadata key, or undefined if the account or metadata key does not exist. */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention #populateExistingMetadata( accountId: string, metadataKey: T, @@ -1179,9 +1174,56 @@ export class AccountsController extends BaseController< return internalAccount ? internalAccount.metadata[metadataKey] : undefined; } + /** + * Subscribes to message events. + */ + #subscribeToMessageEvents() { + this.messagingSystem.subscribe( + 'SnapController:stateChange', + (snapStateState) => this.#handleOnSnapStateChange(snapStateState), + ); + + this.messagingSystem.subscribe( + 'KeyringController:stateChange', + (keyringState) => this.#handleOnKeyringStateChange(keyringState), + ); + + this.messagingSystem.subscribe( + 'SnapKeyring:accountAssetListUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountAssetListUpdated', + snapAccountEvent, + ), + ); + + this.messagingSystem.subscribe( + 'SnapKeyring:accountBalancesUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountBalancesUpdated', + snapAccountEvent, + ), + ); + + this.messagingSystem.subscribe( + 'SnapKeyring:accountTransactionsUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountTransactionsUpdated', + snapAccountEvent, + ), + ); + + // Handle account change when multichain network is changed + this.messagingSystem.subscribe( + 'MultichainNetworkController:networkDidChange', + this.#handleMultichainNetworkChange, + ); + } + /** * Registers message handlers for the AccountsController. - * */ #registerMessageHandlers() { this.messagingSystem.registerActionHandler( diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index c5224ab0be4..ab7e55eca81 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -4,11 +4,9 @@ import { BtcMethod, EthMethod, } from '@metamask/keyring-api'; +import type { KeyringAccountType } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; -import type { - InternalAccount, - InternalAccountType, -} from '@metamask/keyring-internal-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { v4 } from 'uuid'; export const createMockInternalAccount = ({ @@ -23,7 +21,7 @@ export const createMockInternalAccount = ({ }: { id?: string; address?: string; - type?: InternalAccountType; + type?: KeyringAccountType; name?: string; keyringType?: KeyringTypes; snap?: { diff --git a/packages/accounts-controller/src/types.ts b/packages/accounts-controller/src/types.ts new file mode 100644 index 00000000000..1ee9421ec42 --- /dev/null +++ b/packages/accounts-controller/src/types.ts @@ -0,0 +1,10 @@ +// This file contains duplicate code from MultichainNetworkController.ts to avoid circular dependencies +// It should be refactored to avoid duplication + +import type { CaipChainId } from '@metamask/keyring-api'; +import type { NetworkClientId } from '@metamask/network-controller'; + +export type MultichainNetworkControllerNetworkDidChangeEvent = { + type: `MultichainNetworkController:networkDidChange`; + payload: [NetworkClientId | CaipChainId]; +}; diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index 0f8e47e1aeb..3562df9b566 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -50,6 +50,7 @@ export function keyringTypeToName(keyringType: string): string { /** * Generates a UUID v4 options from a given Ethereum address. + * * @param address - The Ethereum address to generate the UUID from. * @returns The UUID v4 options. */ @@ -65,6 +66,7 @@ export function getUUIDOptionsFromAddressOfNormalAccount( /** * Generates a UUID from a given Ethereum address. + * * @param address - The Ethereum address to generate the UUID from. * @returns The generated UUID. */ @@ -74,6 +76,7 @@ export function getUUIDFromAddressOfNormalAccount(address: string): string { /** * Check if a keyring type is considered a "normal" keyring. + * * @param keyringType - The account's keyring type. * @returns True if the keyring type is considered a "normal" keyring, false otherwise. */ diff --git a/packages/accounts-controller/tsconfig.build.json b/packages/accounts-controller/tsconfig.build.json index b4fbdd4821c..2ccd968d36d 100644 --- a/packages/accounts-controller/tsconfig.build.json +++ b/packages/accounts-controller/tsconfig.build.json @@ -10,7 +10,8 @@ { "path": "../base-controller/tsconfig.build.json" }, - { "path": "../keyring-controller/tsconfig.build.json" } + { "path": "../keyring-controller/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/accounts-controller/tsconfig.json b/packages/accounts-controller/tsconfig.json index 7263c934b6b..12cd20ecb5c 100644 --- a/packages/accounts-controller/tsconfig.json +++ b/packages/accounts-controller/tsconfig.json @@ -9,7 +9,8 @@ }, { "path": "../keyring-controller" - } + }, + { "path": "../network-controller" } ], - "include": ["../../types", "./src"] + "include": ["../../types", "./src", "src/tests"] } diff --git a/packages/multichain-network-controller/CHANGELOG.md b/packages/multichain-network-controller/CHANGELOG.md new file mode 100644 index 00000000000..b518709c7b8 --- /dev/null +++ b/packages/multichain-network-controller/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/multichain-network-controller/LICENSE b/packages/multichain-network-controller/LICENSE new file mode 100644 index 00000000000..7d002dced3a --- /dev/null +++ b/packages/multichain-network-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/multichain-network-controller/README.md b/packages/multichain-network-controller/README.md new file mode 100644 index 00000000000..6bdb2c13233 --- /dev/null +++ b/packages/multichain-network-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/multichain-network-controller` + +... + +## Installation + +`yarn add @metamask/multichain-network-controller` + +or + +`npm install @metamask/multichain-network-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/multichain-network-controller/jest.config.js b/packages/multichain-network-controller/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/multichain-network-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json new file mode 100644 index 00000000000..b22fb7a372e --- /dev/null +++ b/packages/multichain-network-controller/package.json @@ -0,0 +1,80 @@ +{ + "name": "@metamask/multichain-network-controller", + "version": "0.0.0", + "description": "Multichain network controller", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/multichain-network-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/multichain-network-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/multichain-network-controller", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", + "publish:preview": "yarn npm publish --tag preview" + }, + "dependencies": { + "@metamask/base-controller": "^8.0.0", + "@metamask/keyring-api": "^17.0.0", + "@metamask/utils": "^11.1.0", + "@solana/addresses": "^2.0.0" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@metamask/keyring-controller": "^19.1.0", + "@types/jest": "^27.4.1", + "@types/uuid": "^8.3.0", + "deepmerge": "^4.2.2", + "immer": "^9.0.6", + "jest": "^27.5.1", + "nock": "^13.3.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2" + }, + "peerDependencies": { + "@metamask/accounts-controller": "^23.0.0", + "@metamask/network-controller": "^22.1.1" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts new file mode 100644 index 00000000000..f5dc5beedf4 --- /dev/null +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -0,0 +1,358 @@ +import { Messenger } from '@metamask/base-controller'; +import { InfuraNetworkType } from '@metamask/controller-utils'; +import { + BtcScope, + SolScope, + EthAccountType, + BtcAccountType, + SolAccountType, + type KeyringAccountType, + type CaipChainId, +} from '@metamask/keyring-api'; +import type { + NetworkControllerGetStateAction, + NetworkControllerSetActiveNetworkAction, +} from '@metamask/network-controller'; + +import { getDefaultMultichainNetworkControllerState } from './constants'; +import { MultichainNetworkController } from './MultichainNetworkController'; +import { + type AllowedActions, + type AllowedEvents, + type MultichainNetworkControllerAllowedActions, + type MultichainNetworkControllerAllowedEvents, + MULTICHAIN_NETWORK_CONTROLLER_NAME, +} from './types'; +import { createMockInternalAccount } from '../tests/utils'; + +/** + * Setup a test controller instance. + * + * @param args - Arguments to this function. + * @param args.options - The constructor options for the controller. + * @param args.getNetworkState - Mock for NetworkController:getState action. + * @param args.setActiveNetwork - Mock for NetworkController:setActiveNetwork action. + * @returns A collection of test controllers and mocks. + */ +function setupController({ + options = {}, + getNetworkState, + setActiveNetwork, +}: { + options?: Partial< + ConstructorParameters[0] + >; + getNetworkState?: jest.Mock< + ReturnType, + Parameters + >; + setActiveNetwork?: jest.Mock< + ReturnType, + Parameters + >; +} = {}) { + const messenger = new Messenger< + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents + >(); + + const publishSpy = jest.spyOn(messenger, 'publish'); + + // Register action handlers + const mockGetNetworkState = + getNetworkState ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'NetworkController:getState', + mockGetNetworkState, + ); + + const mockSetActiveNetwork = + setActiveNetwork ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'NetworkController:setActiveNetwork', + mockSetActiveNetwork, + ); + + const controllerMessenger = messenger.getRestricted< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + AllowedActions['type'], + AllowedEvents['type'] + >({ + name: MULTICHAIN_NETWORK_CONTROLLER_NAME, + allowedActions: [ + 'NetworkController:setActiveNetwork', + 'NetworkController:getState', + ], + allowedEvents: ['AccountsController:selectedAccountChange'], + }); + + // Default state to use Solana network with EVM as active network + const controller = new MultichainNetworkController({ + messenger: options.messenger || controllerMessenger, + state: { + selectedMultichainNetworkChainId: SolScope.Mainnet, + isEvmSelected: true, + ...options.state, + }, + }); + + const triggerSelectedAccountChange = (accountType: KeyringAccountType) => { + const mockAccountAddressByAccountType: Record = + { + [EthAccountType.Eoa]: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', + [EthAccountType.Erc4337]: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', + [SolAccountType.DataAccount]: + 'So11111111111111111111111111111111111111112', + [BtcAccountType.P2wpkh]: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + }; + const mockAccountAddress = mockAccountAddressByAccountType[accountType]; + + const mockAccount = createMockInternalAccount({ + type: accountType, + address: mockAccountAddress, + }); + messenger.publish('AccountsController:selectedAccountChange', mockAccount); + }; + + return { + messenger, + controller, + mockGetNetworkState, + mockSetActiveNetwork, + publishSpy, + triggerSelectedAccountChange, + }; +} + +describe('MultichainNetworkController', () => { + describe('constructor', () => { + it('should set default state', () => { + const { controller } = setupController({ + options: { state: getDefaultMultichainNetworkControllerState() }, + }); + expect(controller.state).toStrictEqual( + getDefaultMultichainNetworkControllerState(), + ); + }); + }); + + describe('setActiveNetwork', () => { + it('should set non-EVM network when same non-EVM chain ID is active', async () => { + // By default, Solana is selected but is NOT active (aka EVM network is active) + const { controller, publishSpy } = setupController(); + + // Set active network to Solana + await controller.setActiveNetwork(SolScope.Mainnet); + + // Check that the Solana is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + + // Check that the a non evm network is now active + expect(controller.state.isEvmSelected).toBe(false); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:networkDidChange', + SolScope.Mainnet, + ); + }); + + it('should throw error when unsupported non-EVM chainId is provided', async () => { + const { controller } = setupController(); + const unsupportedChainId = 'eip155:1' as CaipChainId; + + await expect( + controller.setActiveNetwork(unsupportedChainId), + ).rejects.toThrow(`Unsupported Caip chain ID: ${unsupportedChainId}`); + }); + + it('should do nothing when same non-EVM chain ID is set and active', async () => { + // By default, Solana is selected and active + const { controller, publishSpy } = setupController({ + options: { state: { isEvmSelected: false } }, + }); + + // Set active network to Solana + await controller.setActiveNetwork(SolScope.Mainnet); + + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + + expect(controller.state.isEvmSelected).toBe(false); + + // Check that the messenger published the correct event + expect(publishSpy).not.toHaveBeenCalled(); + }); + + it('should set non-EVM network when different non-EVM chain ID is active', async () => { + // By default, Solana is selected but is NOT active (aka EVM network is active) + const { controller, publishSpy } = setupController({ + options: { state: { isEvmSelected: false } }, + }); + + // Set active network to Bitcoin + await controller.setActiveNetwork(BtcScope.Mainnet); + + // Check that the Solana is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + BtcScope.Mainnet, + ); + + // Check that BTC network is now active + expect(controller.state.isEvmSelected).toBe(false); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:networkDidChange', + BtcScope.Mainnet, + ); + }); + + it('should set EVM network and call NetworkController:setActiveNetwork when same EVM network is selected', async () => { + const selectedNetworkClientId = InfuraNetworkType.mainnet; + + const { controller, mockSetActiveNetwork, publishSpy } = setupController({ + getNetworkState: jest.fn().mockImplementation(() => ({ + selectedNetworkClientId, + })), + }); + + await controller.setActiveNetwork(selectedNetworkClientId); + + // Check that EVM network is selected + expect(controller.state.isEvmSelected).toBe(true); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:networkDidChange', + selectedNetworkClientId, + ); + + // Check that NetworkController:setActiveNetwork was not called + expect(mockSetActiveNetwork).not.toHaveBeenCalled(); + }); + + it('should set EVM network and call NetworkController:setActiveNetwork when different EVM network is selected', async () => { + const { controller, mockSetActiveNetwork, publishSpy } = setupController({ + getNetworkState: jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: InfuraNetworkType.mainnet, + })), + }); + const evmNetworkClientId = 'linea'; + + await controller.setActiveNetwork(evmNetworkClientId); + + // Check that EVM network is selected + expect(controller.state.isEvmSelected).toBe(true); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:networkDidChange', + evmNetworkClientId, + ); + + // Check that NetworkController:setActiveNetwork was not called + expect(mockSetActiveNetwork).toHaveBeenCalledWith(evmNetworkClientId); + }); + }); + + describe('handle AccountsController:selectedAccountChange event', () => { + it('isEvmSelected should be true when both switching to EVM account and EVM network is already active', async () => { + // By default, Solana is selected but EVM network is active + const { controller, triggerSelectedAccountChange } = setupController(); + + // EVM network is currently active + expect(controller.state.isEvmSelected).toBe(true); + + // Switching to EVM account + triggerSelectedAccountChange(EthAccountType.Eoa); + + // EVM network is still active + expect(controller.state.isEvmSelected).toBe(true); + }); + + it('should switch to EVM network if non-EVM network is previously active', async () => { + // By default, Solana is selected and active + const { controller, triggerSelectedAccountChange } = setupController({ + options: { state: { isEvmSelected: false } }, + getNetworkState: jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: InfuraNetworkType.mainnet, + })), + }); + + // non-EVM network is currently active + expect(controller.state.isEvmSelected).toBe(false); + + // Switching to EVM account + triggerSelectedAccountChange(EthAccountType.Eoa); + + // EVM network is now active + expect(controller.state.isEvmSelected).toBe(true); + }); + it('non-EVM network should be active when switching to account of same selected non-EVM network', async () => { + // By default, Solana is selected and active + const { controller, triggerSelectedAccountChange } = setupController({ + options: { + state: { + isEvmSelected: true, + selectedMultichainNetworkChainId: SolScope.Mainnet, + }, + }, + }); + + // EVM network is currently active + expect(controller.state.isEvmSelected).toBe(true); + + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + + // Switching to Solana account + triggerSelectedAccountChange(SolAccountType.DataAccount); + + // Solana is still the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + expect(controller.state.isEvmSelected).toBe(false); + }); + + it('non-EVM network should change when switching to account on different non-EVM network', async () => { + // By default, Solana is selected and active + const { controller, triggerSelectedAccountChange } = setupController({ + options: { + state: { + isEvmSelected: false, + selectedMultichainNetworkChainId: SolScope.Mainnet, + }, + }, + }); + + // Solana is currently active + expect(controller.state.isEvmSelected).toBe(false); + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + + // Switching to Bitcoin account + triggerSelectedAccountChange(BtcAccountType.P2wpkh); + + // Bitcoin is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + BtcScope.Mainnet, + ); + expect(controller.state.isEvmSelected).toBe(false); + }); + }); +}); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts new file mode 100644 index 00000000000..572dfa6d12c --- /dev/null +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -0,0 +1,204 @@ +import { BaseController } from '@metamask/base-controller'; +import { isEvmAccountType } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { NetworkClientId } from '@metamask/network-controller'; +import { isCaipChainId } from '@metamask/utils'; + +import { + MULTICHAIN_NETWORK_CONTROLLER_METADATA, + getDefaultMultichainNetworkControllerState, +} from './constants'; +import { + MULTICHAIN_NETWORK_CONTROLLER_NAME, + type MultichainNetworkControllerState, + type MultichainNetworkControllerMessenger, + type SupportedCaipChainId, +} from './types'; +import { + checkIfSupportedCaipChainId, + getChainIdForNonEvmAddress, +} from './utils'; + +/** + * The MultichainNetworkController is responsible for fetching and caching account + * balances. + */ +export class MultichainNetworkController extends BaseController< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerState, + MultichainNetworkControllerMessenger +> { + constructor({ + messenger, + state, + }: { + messenger: MultichainNetworkControllerMessenger; + state?: Omit< + Partial, + 'multichainNetworkConfigurationsByChainId' + >; + }) { + super({ + messenger, + name: MULTICHAIN_NETWORK_CONTROLLER_NAME, + metadata: MULTICHAIN_NETWORK_CONTROLLER_METADATA, + state: { + ...getDefaultMultichainNetworkControllerState(), + ...state, + }, + }); + + this.#subscribeToMessageEvents(); + this.#registerMessageHandlers(); + } + + /** + * Sets the active EVM network. + * + * @param id - The client ID of the EVM network to set active. + */ + async #setActiveEvmNetwork(id: NetworkClientId): Promise { + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + id, + ); + + // Indicate that the non-EVM network is not selected + this.update((state) => { + state.isEvmSelected = true; + }); + + // Prevent setting same network + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + + if (id === selectedNetworkClientId) { + // EVM network is already selected, no need to update NetworkController + return; + } + + // Update evm active network + await this.messagingSystem.call('NetworkController:setActiveNetwork', id); + } + + /** + * Sets the active non-EVM network. + * + * @param id - The chain ID of the non-EVM network to set active. + */ + #setActiveNonEvmNetwork(id: SupportedCaipChainId): void { + if (id === this.state.selectedMultichainNetworkChainId) { + if (!this.state.isEvmSelected) { + // Same non-EVM network is already selected, no need to update + return; + } + + // Indicate that the non-EVM network is selected + this.update((state) => { + state.isEvmSelected = false; + }); + + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + id, + ); + } + + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + id, + ); + + this.update((state) => { + state.selectedMultichainNetworkChainId = id; + state.isEvmSelected = false; + }); + } + + /** + * Sets the active network. + * + * @param id - The non-EVM Caip chain ID or EVM client ID of the network to set active. + * @returns - A promise that resolves when the network is set active. + */ + async setActiveNetwork( + id: SupportedCaipChainId | NetworkClientId, + ): Promise { + if (isCaipChainId(id)) { + const isSupportedCaipChainId = checkIfSupportedCaipChainId(id); + if (!isSupportedCaipChainId) { + throw new Error(`Unsupported Caip chain ID: ${String(id)}`); + } + return this.#setActiveNonEvmNetwork(id); + } + + return await this.#setActiveEvmNetwork(id); + } + + /** + * Handles switching between EVM and non-EVM networks when an account is changed + * + * @param account - The account that was changed + */ + readonly #handleSelectedAccountChange = (account: InternalAccount) => { + const { type: accountType, address: accountAddress } = account; + const isEvmAccount = isEvmAccountType(accountType); + + // Handle switching to EVM network + if (isEvmAccount) { + if (this.state.isEvmSelected) { + // No need to update if already on evm network + return; + } + + // Make EVM network active + this.update((state) => { + state.isEvmSelected = true; + }); + return; + } + + // Handle switching to non-EVM network + const nonEvmChainId = getChainIdForNonEvmAddress(accountAddress); + const isSameNonEvmNetwork = + nonEvmChainId === this.state.selectedMultichainNetworkChainId; + + if (isSameNonEvmNetwork) { + // No need to update if already on the same non-EVM network + this.update((state) => { + state.isEvmSelected = false; + }); + return; + } + + this.update((state) => { + state.selectedMultichainNetworkChainId = nonEvmChainId; + state.isEvmSelected = false; + }); + }; + + /** + * Subscribes to message events. + */ + #subscribeToMessageEvents() { + // Handle network switch when account is changed + this.messagingSystem.subscribe( + 'AccountsController:selectedAccountChange', + this.#handleSelectedAccountChange, + ); + } + + /** + * Registers message handlers. + */ + #registerMessageHandlers() { + this.messagingSystem.registerActionHandler( + 'MultichainNetworkController:setActiveNetwork', + this.setActiveNetwork.bind(this), + ); + } +} diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts new file mode 100644 index 00000000000..0f84e75f9b1 --- /dev/null +++ b/packages/multichain-network-controller/src/constants.ts @@ -0,0 +1,74 @@ +import { type StateMetadata } from '@metamask/base-controller'; +import { BtcScope, SolScope } from '@metamask/keyring-api'; +import { NetworkStatus } from '@metamask/network-controller'; + +import type { + MultichainNetworkConfiguration, + MultichainNetworkControllerState, + MultichainNetworkMetadata, + SupportedCaipChainId, +} from './types'; + +export const BTC_NATIVE_ASSET = `${BtcScope.Mainnet}/slip44:0`; +export const SOL_NATIVE_ASSET = `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; + +/** + * Supported networks by the MultichainNetworkController + */ +export const AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS: Record< + SupportedCaipChainId, + MultichainNetworkConfiguration +> = { + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, + name: 'Bitcoin Mainnet', + nativeCurrency: BTC_NATIVE_ASSET, + isEvm: false, + }, + [SolScope.Mainnet]: { + chainId: SolScope.Mainnet, + name: 'Solana Mainnet', + nativeCurrency: SOL_NATIVE_ASSET, + isEvm: false, + }, +}; + +/** + * Metadata for the supported networks. + */ +export const NETWORKS_METADATA: Record = { + [BtcScope.Mainnet]: { + features: [], + status: NetworkStatus.Available, + }, + [SolScope.Mainnet]: { + features: [], + status: NetworkStatus.Available, + }, +}; + +/** + * Default state of the {@link MultichainNetworkController}. + * + * @returns The default state of the {@link MultichainNetworkController}. + */ +export const getDefaultMultichainNetworkControllerState = + (): MultichainNetworkControllerState => ({ + multichainNetworkConfigurationsByChainId: + AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS, + selectedMultichainNetworkChainId: SolScope.Mainnet, + isEvmSelected: true, + }); + +/** + * {@link MultichainNetworkController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +export const MULTICHAIN_NETWORK_CONTROLLER_METADATA = { + multichainNetworkConfigurationsByChainId: { persist: true, anonymous: true }, + selectedMultichainNetworkChainId: { persist: true, anonymous: true }, + isEvmSelected: { persist: true, anonymous: true }, +} satisfies StateMetadata; diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts new file mode 100644 index 00000000000..eaf8accddf0 --- /dev/null +++ b/packages/multichain-network-controller/src/index.ts @@ -0,0 +1,24 @@ +export { MultichainNetworkController } from './MultichainNetworkController'; +export { getDefaultMultichainNetworkControllerState } from './constants'; +export type { + MultichainNetworkMetadata, + SupportedCaipChainId, + CommonNetworkConfiguration, + NonEvmNetworkConfiguration, + EvmNetworkConfiguration, + MultichainNetworkConfiguration, + MultichainNetworkControllerState, + MultichainNetworkControllerGetStateAction, + MultichainNetworkControllerSetActiveNetworkAction, + MultichainNetworkControllerStateChange, + MultichainNetworkControllerNetworkDidChangeEvent, + MultichainNetworkControllerActions, + MultichainNetworkControllerEvents, + MultichainNetworkControllerMessenger, +} from './types'; +export { + checkIfSupportedCaipChainId, + toMultichainNetworkConfiguration, + toMultichainNetworkConfigurationsByChainId, + toEvmCaipChainId, +} from './utils'; diff --git a/packages/multichain-network-controller/src/types.ts b/packages/multichain-network-controller/src/types.ts new file mode 100644 index 00000000000..5eb1215da2a --- /dev/null +++ b/packages/multichain-network-controller/src/types.ts @@ -0,0 +1,178 @@ +import { + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedMessenger, +} from '@metamask/base-controller'; +import type { BtcScope, CaipChainId, SolScope } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { + NetworkStatus, + NetworkControllerSetActiveNetworkAction, + NetworkControllerGetStateAction, + NetworkClientId, +} from '@metamask/network-controller'; +import { type CaipAssetType } from '@metamask/utils'; + +export const MULTICHAIN_NETWORK_CONTROLLER_NAME = 'MultichainNetworkController'; + +export type MultichainNetworkMetadata = { + features: string[]; + status: NetworkStatus; +}; + +export type SupportedCaipChainId = SolScope.Mainnet | BtcScope.Mainnet; + +export type CommonNetworkConfiguration = { + /** + * EVM network flag. + */ + isEvm: boolean; + /** + * The chain ID of the network. + */ + chainId: CaipChainId; + /** + * The name of the network. + */ + name: string; +}; + +export type NonEvmNetworkConfiguration = CommonNetworkConfiguration & { + /** + * EVM network flag. + */ + isEvm: false; + /** + * The native asset type of the network. + */ + nativeCurrency: CaipAssetType; +}; + +// TODO: The controller only supports non-EVM network configurations at the moment +// Once we support Caip chain IDs for EVM networks, we can re-enable EVM network configurations +export type EvmNetworkConfiguration = CommonNetworkConfiguration & { + /** + * EVM network flag. + */ + isEvm: true; + /** + * The native asset type of the network. + * For EVM, this is the network ticker since there is no standard between + * tickers and Caip IDs. + */ + nativeCurrency: string; + /** + * The block explorers of the network. + */ + blockExplorerUrls: string[]; + /** + * The index of the default block explorer URL. + */ + defaultBlockExplorerUrlIndex: number; +}; + +export type MultichainNetworkConfiguration = + | EvmNetworkConfiguration + | NonEvmNetworkConfiguration; + +/** + * State used by the {@link MultichainNetworkController} to cache network configurations. + */ +export type MultichainNetworkControllerState = { + /** + * The network configurations by chain ID. + */ + multichainNetworkConfigurationsByChainId: Record< + CaipChainId, + MultichainNetworkConfiguration + >; + /** + * The chain ID of the selected network. + */ + selectedMultichainNetworkChainId: SupportedCaipChainId; + /** + * Whether EVM or non-EVM network is selected + */ + isEvmSelected: boolean; +}; + +/** + * Returns the state of the {@link MultichainNetworkController}. + */ +export type MultichainNetworkControllerGetStateAction = + ControllerGetStateAction< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerState + >; + +export type SetActiveNetworkMethod = ( + id: SupportedCaipChainId | NetworkClientId, +) => Promise; + +export type MultichainNetworkControllerSetActiveNetworkAction = { + type: `${typeof MULTICHAIN_NETWORK_CONTROLLER_NAME}:setActiveNetwork`; + handler: SetActiveNetworkMethod; +}; + +/** + * Event emitted when the state of the {@link MultichainNetworkController} changes. + */ +export type MultichainNetworkControllerStateChange = ControllerStateChangeEvent< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerState +>; + +export type MultichainNetworkControllerNetworkDidChangeEvent = { + type: `${typeof MULTICHAIN_NETWORK_CONTROLLER_NAME}:networkDidChange`; + payload: [NetworkClientId | SupportedCaipChainId]; +}; + +/** + * Actions exposed by the {@link MultichainNetworkController}. + */ +export type MultichainNetworkControllerActions = + | MultichainNetworkControllerGetStateAction + | MultichainNetworkControllerSetActiveNetworkAction; + +/** + * Events emitted by {@link MultichainNetworkController}. + */ +export type MultichainNetworkControllerEvents = + MultichainNetworkControllerNetworkDidChangeEvent; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | NetworkControllerGetStateAction + | NetworkControllerSetActiveNetworkAction; + +// Re-define event here to avoid circular dependency with AccountsController +export type AccountsControllerSelectedAccountChangeEvent = { + type: `AccountsController:selectedAccountChange`; + payload: [InternalAccount]; +}; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = AccountsControllerSelectedAccountChangeEvent; + +export type MultichainNetworkControllerAllowedActions = + | MultichainNetworkControllerActions + | AllowedActions; + +export type MultichainNetworkControllerAllowedEvents = + | MultichainNetworkControllerEvents + | AllowedEvents; + +/** + * Messenger type for the MultichainNetworkController. + */ +export type MultichainNetworkControllerMessenger = RestrictedMessenger< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; diff --git a/packages/multichain-network-controller/src/utils.test.ts b/packages/multichain-network-controller/src/utils.test.ts new file mode 100644 index 00000000000..dbf6e5e5322 --- /dev/null +++ b/packages/multichain-network-controller/src/utils.test.ts @@ -0,0 +1,114 @@ +import { BtcScope, SolScope, type CaipChainId } from '@metamask/keyring-api'; +import { type NetworkConfiguration } from '@metamask/network-controller'; + +import { + toEvmCaipChainId, + getChainIdForNonEvmAddress, + checkIfSupportedCaipChainId, + toMultichainNetworkConfiguration, + toMultichainNetworkConfigurationsByChainId, +} from './utils'; + +describe('utils', () => { + describe('getChainIdForNonEvmAddress', () => { + it('returns Solana chain ID for Solana addresses', () => { + const solanaAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + expect(getChainIdForNonEvmAddress(solanaAddress)).toBe(SolScope.Mainnet); + }); + + it('returns Bitcoin chain ID for non-Solana addresses', () => { + const bitcoinAddress = 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6'; + expect(getChainIdForNonEvmAddress(bitcoinAddress)).toBe(BtcScope.Mainnet); + }); + }); + + describe('checkIfSupportedCaipChainId', () => { + it('returns true for supported CAIP chain IDs', () => { + expect(checkIfSupportedCaipChainId(SolScope.Mainnet)).toBe(true); + expect(checkIfSupportedCaipChainId(BtcScope.Mainnet)).toBe(true); + }); + + it('returns false for non-CAIP IDs', () => { + expect(checkIfSupportedCaipChainId('mainnet' as CaipChainId)).toBe(false); + }); + + it('returns false for unsupported CAIP chain IDs', () => { + expect(checkIfSupportedCaipChainId('eip155:1')).toBe(false); + }); + }); + + describe('toMultichainNetworkConfiguration', () => { + it('updates the network configuration for a single EVM network', () => { + const network: NetworkConfiguration = { + chainId: '0x1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [], + defaultRpcEndpointIndex: 0, + }; + expect(toMultichainNetworkConfiguration(network)).toStrictEqual({ + chainId: 'eip155:1', + isEvm: true, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + }); + }); + }); + + describe('toMultichainNetworkConfigurationsByChainId', () => { + it('updates the network configurations for multiple EVM networks', () => { + const networks: Record = { + '0x1': { + chainId: '0x1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [], + defaultRpcEndpointIndex: 0, + }, + '0xe708': { + chainId: '0xe708', + name: 'Linea', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://lineascan.build'], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [], + defaultRpcEndpointIndex: 0, + }, + }; + expect( + toMultichainNetworkConfigurationsByChainId(networks), + ).toStrictEqual({ + 'eip155:1': { + chainId: 'eip155:1', + isEvm: true, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + }, + 'eip155:59144': { + chainId: 'eip155:59144', + isEvm: true, + name: 'Linea', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://lineascan.build'], + defaultBlockExplorerUrlIndex: 0, + }, + }); + }); + }); + + describe('toEvmCaipChainId', () => { + it('converts a hex chain ID to a CAIP chain ID', () => { + expect(toEvmCaipChainId('0x1')).toBe('eip155:1'); + expect(toEvmCaipChainId('0xe708')).toBe('eip155:59144'); + expect(toEvmCaipChainId('0x539')).toBe('eip155:1337'); + }); + }); +}); diff --git a/packages/multichain-network-controller/src/utils.ts b/packages/multichain-network-controller/src/utils.ts new file mode 100644 index 00000000000..d6a00d7160e --- /dev/null +++ b/packages/multichain-network-controller/src/utils.ts @@ -0,0 +1,93 @@ +import { BtcScope, SolScope } from '@metamask/keyring-api'; +import type { NetworkConfiguration } from '@metamask/network-controller'; +import { + type Hex, + type CaipChainId, + KnownCaipNamespace, + toCaipChainId, + hexToNumber, +} from '@metamask/utils'; +import { isAddress as isSolanaAddress } from '@solana/addresses'; + +import { AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; +import type { + SupportedCaipChainId, + MultichainNetworkConfiguration, +} from './types'; + +/** + * Returns the chain id of the non-EVM network based on the account address. + * + * @param address - The address to check. + * @returns The caip chain id of the non-EVM network. + */ +export function getChainIdForNonEvmAddress( + address: string, +): SupportedCaipChainId { + // This condition is not the most robust. Once we support more networks, we will need to update this logic. + if (isSolanaAddress(address)) { + return SolScope.Mainnet; + } + return BtcScope.Mainnet; +} + +/** + * Checks if the Caip chain ID is supported. + * + * @param id - The Caip chain IDto check. + * @returns Whether the chain ID is supported. + */ +export function checkIfSupportedCaipChainId( + id: CaipChainId, +): id is SupportedCaipChainId { + // Check if the chain id is supported + return Object.keys(AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS).includes(id); +} + +/** + * Converts a hex chain ID to a Caip chain ID. + * + * @param chainId - The hex chain ID to convert. + * @returns The Caip chain ID. + */ +export const toEvmCaipChainId = (chainId: Hex): CaipChainId => + toCaipChainId(KnownCaipNamespace.Eip155, hexToNumber(chainId).toString()); + +/** + * Updates a network configuration to the format used by the MultichainNetworkController. + * This method is exclusive for EVM networks with hex identifiers from the NetworkController. + * + * @param network - The network configuration to update. + * @returns The updated network configuration. + */ +export const toMultichainNetworkConfiguration = ( + network: NetworkConfiguration, +): MultichainNetworkConfiguration => { + return { + chainId: toEvmCaipChainId(network.chainId), + isEvm: true, + name: network.name, + nativeCurrency: network.nativeCurrency, + blockExplorerUrls: network.blockExplorerUrls, + defaultBlockExplorerUrlIndex: network.defaultBlockExplorerUrlIndex || 0, + }; +}; + +/** + * Updates a record of network configurations to the format used by the MultichainNetworkController. + * This method is exclusive for EVM networks with hex identifiers from the NetworkController. + * + * @param networkConfigurationsByChainId - The network configurations to update. + * @returns The updated network configurations. + */ +export const toMultichainNetworkConfigurationsByChainId = ( + networkConfigurationsByChainId: Record, +): Record => + Object.entries(networkConfigurationsByChainId).reduce( + (acc, [, network]) => ({ + ...acc, + [toEvmCaipChainId(network.chainId)]: + toMultichainNetworkConfiguration(network), + }), + {}, + ); diff --git a/packages/multichain-network-controller/tests/utils.ts b/packages/multichain-network-controller/tests/utils.ts new file mode 100644 index 00000000000..141f6f29f9e --- /dev/null +++ b/packages/multichain-network-controller/tests/utils.ts @@ -0,0 +1,98 @@ +import { + BtcAccountType, + EthAccountType, + SolAccountType, + BtcMethod, + EthMethod, + SolMethod, + type KeyringAccountType, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +/** + * Creates a mock internal account. This is a duplicated function from the accounts-controller package + * This exists here to prevent circular dependencies with the accounts-controller package + * + * @param args - Arguments to this function. + * @param args.id - The ID of the account. + * @param args.address - The address of the account. + * @param args.type - The type of the account. + * @param args.name - The name of the account. + * @param args.keyringType - The keyring type of the account. + * @param args.snap - The snap of the account. + * @param args.snap.id - The ID of the snap. + * @param args.snap.enabled - Whether the snap is enabled. + * @param args.snap.name - The name of the snap. + * @param args.importTime - The import time of the account. + * @param args.lastSelected - The last selected time of the account. + * @returns A mock internal account. + */ +export const createMockInternalAccount = ({ + id = 'dummy-id', + address = '0x2990079bcdee240329a520d2444386fc119da21a', + type = EthAccountType.Eoa, + name = 'Account 1', + keyringType = KeyringTypes.hd, + snap, + importTime = Date.now(), + lastSelected = Date.now(), +}: { + id?: string; + address?: string; + type?: KeyringAccountType; + name?: string; + keyringType?: KeyringTypes; + snap?: { + id: string; + enabled: boolean; + name: string; + }; + importTime?: number; + lastSelected?: number; +} = {}): InternalAccount => { + let methods; + + switch (type) { + case EthAccountType.Eoa: + methods = [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ]; + break; + case EthAccountType.Erc4337: + methods = [ + EthMethod.PatchUserOperation, + EthMethod.PrepareUserOperation, + EthMethod.SignUserOperation, + ]; + break; + case BtcAccountType.P2wpkh: + methods = [BtcMethod.SendBitcoin]; + break; + case SolAccountType.DataAccount: + methods = [SolMethod.SendAndConfirmTransaction]; + break; + default: + throw new Error(`Unknown account type: ${type as string}`); + } + + return { + id, + address, + options: {}, + methods, + type, + metadata: { + name, + keyring: { type: keyringType }, + importTime, + lastSelected, + snap, + }, + } as InternalAccount; +}; diff --git a/packages/multichain-network-controller/tsconfig.build.json b/packages/multichain-network-controller/tsconfig.build.json new file mode 100644 index 00000000000..41c2d082d3d --- /dev/null +++ b/packages/multichain-network-controller/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain-network-controller/tsconfig.json b/packages/multichain-network-controller/tsconfig.json new file mode 100644 index 00000000000..e5ff777b642 --- /dev/null +++ b/packages/multichain-network-controller/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../network-controller" }, + { "path": "../keyring-controller" } + ], + "include": ["../../types", "./src", "./tests"] +} diff --git a/packages/multichain-network-controller/typedoc.json b/packages/multichain-network-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/multichain-network-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/teams.json b/teams.json index 7060b959eaf..eabdb25af76 100644 --- a/teams.json +++ b/teams.json @@ -17,6 +17,7 @@ "metamask/logging-controller": "team-confirmations", "metamask/message-manager": "team-confirmations", "metamask/multichain": "team-wallet-api-platform", + "metamask/multichain-network-controller": "team-wallet-api-platform", "metamask/name-controller": "team-confirmations", "metamask/network-controller": "team-wallet-framework,team-assets", "metamask/notification-controller": "team-snaps-platform", diff --git a/tsconfig.build.json b/tsconfig.build.json index eea2f56d062..a091abb09e7 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -42,7 +42,8 @@ "path": "./packages/token-search-discovery-controller/tsconfig.build.json" }, { "path": "./packages/transaction-controller/tsconfig.build.json" }, - { "path": "./packages/user-operation-controller/tsconfig.build.json" } + { "path": "./packages/user-operation-controller/tsconfig.build.json" }, + { "path": "./packages/multichain-network-controller/tsconfig.build.json" } ], "files": [], "include": [] diff --git a/tsconfig.json b/tsconfig.json index c9b7f0715ec..489ba07d2a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ { "path": "./packages/message-manager" }, { "path": "./packages/multichain" }, { "path": "./packages/multichain-transactions-controller" }, + { "path": "./packages/multichain-network-controller" }, { "path": "./packages/name-controller" }, { "path": "./packages/network-controller" }, { "path": "./packages/notification-services-controller" }, diff --git a/yarn.lock b/yarn.lock index 774f0b6fe8d..57dcd4b6f07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2351,6 +2351,7 @@ __metadata: "@metamask/keyring-api": "npm:^17.0.0" "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/keyring-internal-api": "npm:^4.0.1" + "@metamask/network-controller": "npm:^22.2.1" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.19.0" "@metamask/snaps-sdk": "npm:^6.17.1" @@ -2369,7 +2370,8 @@ __metadata: uuid: "npm:^8.3.2" webextension-polyfill: "npm:^0.12.0" peerDependencies: - "@metamask/keyring-controller": ^19.0.0 + "@metamask/keyring-controller": ^19.1.0 + "@metamask/network-controller": ^22.2.1 "@metamask/providers": ^18.1.0 "@metamask/snaps-controllers": ^9.19.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 @@ -3451,6 +3453,32 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": + version: 0.0.0-use.local + resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/keyring-api": "npm:^17.0.0" + "@metamask/keyring-controller": "npm:^19.1.0" + "@metamask/utils": "npm:^11.1.0" + "@solana/addresses": "npm:^2.0.0" + "@types/jest": "npm:^27.4.1" + "@types/uuid": "npm:^8.3.0" + deepmerge: "npm:^4.2.2" + immer: "npm:^9.0.6" + jest: "npm:^27.5.1" + nock: "npm:^13.3.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + peerDependencies: + "@metamask/accounts-controller": ^23.0.0 + "@metamask/network-controller": ^22.1.1 + languageName: unknown + linkType: soft + "@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller" @@ -4689,6 +4717,82 @@ __metadata: languageName: node linkType: hard +"@solana/addresses@npm:^2.0.0": + version: 2.0.0 + resolution: "@solana/addresses@npm:2.0.0" + dependencies: + "@solana/assertions": "npm:2.0.0" + "@solana/codecs-core": "npm:2.0.0" + "@solana/codecs-strings": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/f99d09c72046c73858aa8b7bc323e634a60b1023a4d280036bc94489e431075c7f29d2889e8787e33a04cfdecbe77cd8ca26c31ded73f735dc98e49c3151cc17 + languageName: node + linkType: hard + +"@solana/assertions@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/assertions@npm:2.0.0" + dependencies: + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/c1af37ae1bd79b1657395d9315ac261dabc9908a64af6ed80e3b7e5140909cd8c8c757f0c41fff084e26fbb4d32f091c89c092a8c1ed5e6f4565dfe7426c0979 + languageName: node + linkType: hard + +"@solana/codecs-core@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-core@npm:2.0.0" + dependencies: + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/e58a72e67bee3e5da60201eecda345c604b49138d5298e39b8e7d4d57a4dee47be3b0ecc8fc3429a2a60a42c952eaf860d43d3df1eb2b1d857e35368eca9c820 + languageName: node + linkType: hard + +"@solana/codecs-numbers@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-numbers@npm:2.0.0" + dependencies: + "@solana/codecs-core": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/500144d549ea0292c2f672300610df9054339a31cb6a4e61b29623308ef3b14f15eb587ee6139cf3334d2e0f29db1da053522da244b12184bb8fbdb097b7102b + languageName: node + linkType: hard + +"@solana/codecs-strings@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-strings@npm:2.0.0" + dependencies: + "@solana/codecs-core": "npm:2.0.0" + "@solana/codecs-numbers": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ">=5" + checksum: 10/4380136e2603c2cee12a28438817beb34b0fe45da222b8c38342c5b3680f02086ec7868cde0bb7b4e5dd459af5988613af1d97230c6a193db3be1c45122aba39 + languageName: node + linkType: hard + +"@solana/errors@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/errors@npm:2.0.0" + dependencies: + chalk: "npm:^5.3.0" + commander: "npm:^12.1.0" + peerDependencies: + typescript: ">=5" + bin: + errors: bin/cli.mjs + checksum: 10/4191f96cad47c64266ec501ae1911a6245fd02b2f68a2c53c3dabbc63eb7c5462f170a765b584348b195da2387e7ca02096d792c67352c2c30a4f3a3cc7e4270 + languageName: node + linkType: hard + "@spruceid/siwe-parser@npm:2.1.0": version: 2.1.0 resolution: "@spruceid/siwe-parser@npm:2.1.0" @@ -7066,6 +7170,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93 + languageName: node + linkType: hard + "commander@npm:^9.0.0": version: 9.5.0 resolution: "commander@npm:9.5.0" From 19e242a0711dbfc72c7bc200e0425ea438495283 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 13 Feb 2025 11:40:24 +0100 Subject: [PATCH 08/20] feat: Re-simulate active transactions every 3000 ms (#5189) ## Explanation This PR adds `ResimulateHelper`, which focuses on `transactionMeta.isActive` property and re-simulates transaction depending on that value. In order to capsulate re-simulation logic, this PR also relocates other utility functions under the new created `ResimulationHelper.ts` file. ## References Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3922 Extension PR: https://github.com/MetaMask/metamask-extension/pull/29878 ## Changelog ### `@metamask/transaction-controller` - **ADDED**: Adds ability of re-simulating transaction depending on the `isActive` property on `transactionMeta` - `isActive` property is expected to set by client. - Re-simulation of transactions will occur every 3 seconds if `isActive` is `true`. - Adds `setTransactionActive` function to update the `isActive` property on `transactionMeta`. ## Checklist - [X] I've updated the test suite for new or updated code as appropriate - [X] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [X] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [X] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Elliot Winkler --- eslint-warning-thresholds.json | 8 - packages/transaction-controller/CHANGELOG.md | 7 + .../transaction-controller/jest.config.js | 2 +- .../src/TransactionController.test.ts | 46 ++++- .../src/TransactionController.ts | 47 ++++- .../ResimulateHelper.test.ts} | 160 ++++++++++++++++-- .../ResimulateHelper.ts} | 125 +++++++++++++- packages/transaction-controller/src/types.ts | 5 + 8 files changed, 369 insertions(+), 31 deletions(-) rename packages/transaction-controller/src/{utils/resimulate.test.ts => helpers/ResimulateHelper.test.ts} (70%) rename packages/transaction-controller/src/{utils/resimulate.ts => helpers/ResimulateHelper.ts} (70%) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 4b387a4f112..19a192a361b 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -565,7 +565,6 @@ "jsdoc/tag-lines": 4 }, "packages/transaction-controller/src/TransactionController.test.ts": { - "@typescript-eslint/no-unused-vars": 1, "import-x/namespace": 1, "import-x/order": 4, "jsdoc/tag-lines": 1, @@ -701,13 +700,6 @@ "packages/transaction-controller/src/utils/nonce.test.ts": { "import-x/order": 1 }, - "packages/transaction-controller/src/utils/resimulate.test.ts": { - "import-x/order": 2 - }, - "packages/transaction-controller/src/utils/resimulate.ts": { - "import-x/order": 1, - "jsdoc/tag-lines": 7 - }, "packages/transaction-controller/src/utils/retry.test.ts": { "import-x/order": 1 }, diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 1ce358fd59f..01428f01bce 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Adds ability of re-simulating transaction depending on the `isActive` property on `transactionMeta` ([#5189](https://github.com/MetaMask/core/pull/5189)) + - `isActive` property is expected to set by client. + - Re-simulation of transactions will occur every 3 seconds if `isActive` is `true`. +- Adds `setTransactionActive` function to update the `isActive` property on `transactionMeta`. ([#5189](https://github.com/MetaMask/core/pull/5189)) + ## [45.1.0] ### Added diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index 8072dc420d7..3ae6e5e21b6 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -18,7 +18,7 @@ module.exports = merge(baseConfig, { coverageThreshold: { global: { branches: 91.76, - functions: 94.62, + functions: 94.57, lines: 96.83, statements: 96.82, }, diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index d788cf471d4..4571fbe2014 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -50,6 +50,7 @@ import { IncomingTransactionHelper } from './helpers/IncomingTransactionHelper'; import { MethodDataHelper } from './helpers/MethodDataHelper'; import { MultichainTrackingHelper } from './helpers/MultichainTrackingHelper'; import { PendingTransactionTracker } from './helpers/PendingTransactionTracker'; +import { shouldResimulate } from './helpers/ResimulateHelper'; import type { AllowedActions, AllowedEvents, @@ -86,7 +87,6 @@ import { getTransactionLayer1GasFee, updateTransactionLayer1GasFee, } from './utils/layer1-gas-fee-flow'; -import { shouldResimulate } from './utils/resimulate'; import { getSimulationData } from './utils/simulation'; import { updatePostTransactionBalance, @@ -115,7 +115,10 @@ jest.mock('./utils/gas'); jest.mock('./utils/gas-fees'); jest.mock('./utils/gas-flow'); jest.mock('./utils/layer1-gas-fee-flow'); -jest.mock('./utils/resimulate'); +jest.mock('./helpers/ResimulateHelper', () => ({ + ...jest.requireActual('./helpers/ResimulateHelper'), + shouldResimulate: jest.fn(), +})); jest.mock('./utils/simulation'); jest.mock('./utils/swaps'); jest.mock('uuid'); @@ -2354,7 +2357,7 @@ describe('TransactionController', () => { try { await result; - } catch (error) { + } catch { // Ignore user rejected error as it is expected } await finishedPromise; @@ -6075,4 +6078,41 @@ describe('TransactionController', () => { ); }); }); + + describe('setTransactionActive', () => { + it('throws if transaction does not exist', async () => { + const { controller } = setupController(); + expect(() => controller.setTransactionActive('123', true)).toThrow( + 'Transaction with id 123 not found', + ); + }); + + it('updates the isActive state of a transaction', async () => { + const transactionId = '123'; + const { controller } = setupController({ + options: { + state: { + transactions: [ + { + id: transactionId, + status: TransactionStatus.unapproved, + history: [{}], + txParams: { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + } as unknown as TransactionMeta, + ], + }, + }, + updateToInitialState: true, + }); + + controller.setTransactionActive(transactionId, true); + + const transaction = controller.state.transactions[0]; + + expect(transaction?.isActive).toBe(true); + }); + }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 54e763c1077..60ffcb37e83 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -65,6 +65,12 @@ import { IncomingTransactionHelper } from './helpers/IncomingTransactionHelper'; import { MethodDataHelper } from './helpers/MethodDataHelper'; import { MultichainTrackingHelper } from './helpers/MultichainTrackingHelper'; import { PendingTransactionTracker } from './helpers/PendingTransactionTracker'; +import type { ResimulateResponse } from './helpers/ResimulateHelper'; +import { + ResimulateHelper, + hasSimulationDataChanged, + shouldResimulate, +} from './helpers/ResimulateHelper'; import { projectLogger as log } from './logger'; import type { DappSuggestedGasFees, @@ -110,8 +116,6 @@ import { getNextNonce, } from './utils/nonce'; import { prepareTransaction, serializeTransaction } from './utils/prepare'; -import type { ResimulateResponse } from './utils/resimulate'; -import { hasSimulationDataChanged, shouldResimulate } from './utils/resimulate'; import { getTransactionParamsWithIncreasedGasFee } from './utils/retry'; import { getSimulationData } from './utils/simulation'; import { @@ -926,6 +930,18 @@ export class TransactionController extends BaseController< this.#checkForPendingTransactionAndStartPolling, ); + new ResimulateHelper({ + simulateTransaction: this.#updateSimulationData.bind(this), + onTransactionsUpdate: (listener) => { + this.messagingSystem.subscribe( + 'TransactionController:stateChange', + listener, + (controllerState) => controllerState.transactions, + ); + }, + getTransactions: () => this.state.transactions, + }); + this.onBootCleanup(); this.#checkForPendingTransactionAndStartPolling(); } @@ -1862,6 +1878,33 @@ export class TransactionController extends BaseController< return this.getTransaction(txId); } + /** + * Update the isActive state of a transaction. + * + * @param transactionId - The ID of the transaction to update. + * @param isActive - The active state. + */ + setTransactionActive(transactionId: string, isActive: boolean) { + const transactionMeta = this.getTransaction(transactionId); + + if (!transactionMeta) { + throw new Error(`Transaction with id ${transactionId} not found`); + } + + this.#updateTransactionInternal( + { + transactionId, + note: 'TransactionController#setTransactionActive - Transaction isActive updated', + skipHistory: true, + skipValidation: true, + skipResimulateCheck: true, + }, + (updatedTransactionMeta) => { + updatedTransactionMeta.isActive = isActive; + }, + ); + } + /** * Signs and returns the raw transaction data for provided transaction params list. * diff --git a/packages/transaction-controller/src/utils/resimulate.test.ts b/packages/transaction-controller/src/helpers/ResimulateHelper.test.ts similarity index 70% rename from packages/transaction-controller/src/utils/resimulate.test.ts rename to packages/transaction-controller/src/helpers/ResimulateHelper.test.ts index f27e19498cd..9c372dd4264 100644 --- a/packages/transaction-controller/src/utils/resimulate.test.ts +++ b/packages/transaction-controller/src/helpers/ResimulateHelper.test.ts @@ -1,26 +1,27 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import { NetworkType } from '@metamask/controller-utils'; +import type { NetworkClientId } from '@metamask/network-controller'; import { BN } from 'bn.js'; -import { CHAIN_IDS } from '../constants'; -import type { - SecurityAlertResponse, - SimulationData, - SimulationTokenBalanceChange, - TransactionMeta, -} from '../types'; -import { SimulationTokenStandard, TransactionStatus } from '../types'; import { + type ResimulateHelperOptions, + ResimulateHelper, 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'); + RESIMULATE_INTERVAL_MS, +} from './ResimulateHelper'; +import { CHAIN_IDS } from '../constants'; +import type { + TransactionMeta, + SecurityAlertResponse, + SimulationData, + SimulationTokenBalanceChange, +} from '../types'; +import { TransactionStatus, SimulationTokenStandard } from '../types'; +import { getPercentageChange } from '../utils/utils'; const CURRENT_TIME_MOCK = 1234567890; const CURRENT_TIME_SECONDS_MOCK = 1234567; @@ -74,6 +75,139 @@ const TRANSACTION_META_MOCK: TransactionMeta = { }, }; +const mockTransactionMeta = { + id: '1', + networkClientId: 'network1' as NetworkClientId, + isActive: true, + status: TransactionStatus.unapproved, +} as TransactionMeta; + +jest.mock('../utils/utils'); + +describe('ResimulateHelper', () => { + let getTransactionsMock: jest.Mock<() => TransactionMeta[]>; + let simulateTransactionMock: jest.Mock< + (transactionMeta: TransactionMeta) => Promise + >; + let onTransactionsUpdateMock: jest.Mock<(listener: () => void) => void>; + + /** + * Triggers onStateChange callback + */ + function triggerStateChange() { + onTransactionsUpdateMock.mock.calls[0][0](); + } + + /** + * Mocks getTransactions to return given transactions argument + * + * @param transactions - Transactions to be returned + */ + function mockGetTransactionsOnce(transactions: TransactionMeta[]) { + getTransactionsMock.mockReturnValueOnce( + transactions as unknown as ResimulateHelperOptions['getTransactions'], + ); + } + + beforeEach(() => { + jest.useFakeTimers(); + getTransactionsMock = jest.fn(); + onTransactionsUpdateMock = jest.fn(); + simulateTransactionMock = jest.fn().mockResolvedValue(undefined); + + new ResimulateHelper({ + getTransactions: getTransactionsMock, + onTransactionsUpdate: onTransactionsUpdateMock, + simulateTransaction: simulateTransactionMock, + } as unknown as ResimulateHelperOptions); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it(`resimulates unapproved active transaction every ${RESIMULATE_INTERVAL_MS} milliseconds`, async () => { + mockGetTransactionsOnce([mockTransactionMeta]); + triggerStateChange(); + + jest.advanceTimersByTime(RESIMULATE_INTERVAL_MS); + await Promise.resolve(); + + jest.advanceTimersByTime(RESIMULATE_INTERVAL_MS); + await Promise.resolve(); + + jest.runAllTimers(); + + expect(simulateTransactionMock).toHaveBeenCalledWith(mockTransactionMeta); + expect(simulateTransactionMock).toHaveBeenCalledTimes(2); + }); + + it(`does not resimulate twice the same transaction even if state change is triggered twice`, async () => { + mockGetTransactionsOnce([mockTransactionMeta]); + triggerStateChange(); + + // Halfway through the interval + jest.advanceTimersByTime(RESIMULATE_INTERVAL_MS / 2); + + // Assume state change is triggered again + mockGetTransactionsOnce([mockTransactionMeta]); + triggerStateChange(); + + // Halfway through the interval + jest.advanceTimersByTime(RESIMULATE_INTERVAL_MS / 2); + + expect(simulateTransactionMock).toHaveBeenCalledTimes(1); + }); + + it('does not resimulate a transaction that is no longer active', () => { + mockGetTransactionsOnce([mockTransactionMeta]); + triggerStateChange(); + + // Halfway through the interval + jest.advanceTimersByTime(RESIMULATE_INTERVAL_MS / 2); + + const inactiveTransactionMeta = { + ...mockTransactionMeta, + isActive: false, + } as TransactionMeta; + + mockGetTransactionsOnce([inactiveTransactionMeta]); + triggerStateChange(); + + jest.advanceTimersByTime(RESIMULATE_INTERVAL_MS / 2); + + expect(simulateTransactionMock).toHaveBeenCalledTimes(0); + }); + + it('does not resimulate a transaction that is not active', () => { + const inactiveTransactionMeta = { + ...mockTransactionMeta, + isActive: false, + } as TransactionMeta; + + mockGetTransactionsOnce([inactiveTransactionMeta]); + triggerStateChange(); + + jest.advanceTimersByTime(2 * RESIMULATE_INTERVAL_MS); + + expect(simulateTransactionMock).toHaveBeenCalledTimes(0); + }); + + it('stops resimulating a transaction that is no longer in the transaction list', () => { + mockGetTransactionsOnce([mockTransactionMeta]); + triggerStateChange(); + + jest.advanceTimersByTime(RESIMULATE_INTERVAL_MS); + + mockGetTransactionsOnce([]); + triggerStateChange(); + + jest.advanceTimersByTime(RESIMULATE_INTERVAL_MS); + + expect(simulateTransactionMock).toHaveBeenCalledTimes(1); + }); +}); + describe('Resimulate Utils', () => { const getPercentageChangeMock = jest.mocked(getPercentageChange); diff --git a/packages/transaction-controller/src/utils/resimulate.ts b/packages/transaction-controller/src/helpers/ResimulateHelper.ts similarity index 70% rename from packages/transaction-controller/src/utils/resimulate.ts rename to packages/transaction-controller/src/helpers/ResimulateHelper.ts index b339356c7a2..6bb1a51c23c 100644 --- a/packages/transaction-controller/src/utils/resimulate.ts +++ b/packages/transaction-controller/src/helpers/ResimulateHelper.ts @@ -1,31 +1,142 @@ import type { Hex } from '@metamask/utils'; -import { createModuleLogger, remove0x } from '@metamask/utils'; +import { remove0x } from '@metamask/utils'; import { BN } from 'bn.js'; import { isEqual } from 'lodash'; -import { projectLogger } from '../logger'; +import { createModuleLogger, projectLogger } from '../logger'; +import { TransactionStatus } from '../types'; import type { SimulationBalanceChange, SimulationData, TransactionMeta, TransactionParams, } from '../types'; -import { getPercentageChange } from './utils'; +import { getPercentageChange } from '../utils/utils'; -const log = createModuleLogger(projectLogger, 'resimulate'); +const log = createModuleLogger(projectLogger, 'resimulate-helper'); 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 const RESIMULATE_INTERVAL_MS = 3000; export type ResimulateResponse = { blockTime?: number; resimulate: boolean; }; +export type ResimulateHelperOptions = { + getTransactions: () => TransactionMeta[]; + onTransactionsUpdate: (listener: () => void) => void; + simulateTransaction: (transactionMeta: TransactionMeta) => Promise; +}; + +export class ResimulateHelper { + // Map of transactionId <=> timeoutId + readonly #timeoutIds: Map = new Map(); + + readonly #getTransactions: () => TransactionMeta[]; + + readonly #simulateTransaction: ( + transactionMeta: TransactionMeta, + ) => Promise; + + constructor({ + getTransactions, + simulateTransaction, + onTransactionsUpdate, + }: ResimulateHelperOptions) { + this.#getTransactions = getTransactions; + this.#simulateTransaction = simulateTransaction; + + onTransactionsUpdate(this.#onTransactionsUpdate.bind(this)); + } + + #onTransactionsUpdate() { + const unapprovedTransactions = this.#getTransactions().filter( + (tx) => tx.status === TransactionStatus.unapproved, + ); + + const unapprovedTransactionIds = new Set( + unapprovedTransactions.map((tx) => tx.id), + ); + + // Combine unapproved transaction IDs and currently active resimulations + const allTransactionIds = new Set([ + ...unapprovedTransactionIds, + ...this.#timeoutIds.keys(), + ]); + + allTransactionIds.forEach((transactionId) => { + const transactionMeta = unapprovedTransactions.find( + (tx) => tx.id === transactionId, + ) as TransactionMeta; + + if (transactionMeta?.isActive) { + this.#start(transactionMeta); + } else { + this.#stop(transactionId); + } + }); + } + + #start(transactionMeta: TransactionMeta) { + const { id: transactionId } = transactionMeta; + if (this.#timeoutIds.has(transactionId)) { + return; + } + + const listener = () => { + // eslint-disable-next-line promise/catch-or-return + this.#simulateTransaction(transactionMeta) + .catch((error) => { + /* istanbul ignore next */ + log('Error during transaction resimulation', error); + }) + .finally(() => { + // Schedule the next execution + if (this.#timeoutIds.has(transactionId)) { + this.#queueUpdate(transactionId, listener); + } + }); + }; + + // Start the first execution + this.#queueUpdate(transactionId, listener); + log( + `Started resimulating transaction ${transactionId} every ${RESIMULATE_INTERVAL_MS} milliseconds`, + ); + } + + #queueUpdate(transactionId: string, listener: () => void) { + const timeoutId = setTimeout(listener, RESIMULATE_INTERVAL_MS); + this.#timeoutIds.set(transactionId, timeoutId); + } + + #stop(transactionId: string) { + if (!this.#timeoutIds.has(transactionId)) { + return; + } + + this.#removeListener(transactionId); + log( + `Stopped resimulating transaction ${transactionId} every ${RESIMULATE_INTERVAL_MS} milliseconds`, + ); + } + + #removeListener(id: string) { + const timeoutId = this.#timeoutIds.get(id); + if (timeoutId) { + clearTimeout(timeoutId); + this.#timeoutIds.delete(id); + } + } +} + /** * 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. @@ -79,6 +190,7 @@ export function shouldResimulate( /** * 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. @@ -141,6 +253,7 @@ export function hasSimulationDataChanged( /** * 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. @@ -174,6 +287,7 @@ function isParametersUpdated( /** * 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. @@ -205,6 +319,7 @@ function hasNewSecurityAlert( /** * 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. @@ -240,6 +355,7 @@ function hasValueAndNativeBalanceMismatch( /** * 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. @@ -258,6 +374,7 @@ function isBalanceChangeUpdated( /** * 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. diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 5d7ec9d5895..33eecc9a9c0 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -171,6 +171,11 @@ type TransactionMetaBase = { */ firstRetryBlockNumber?: string; + /** + * Whether the transaction is active. + */ + isActive?: boolean; + /** * Whether the transaction is the first time interaction. */ From a7718b8a2aa86f07bdff73444ee0bb3a9a55af80 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 13 Feb 2025 13:11:15 +0100 Subject: [PATCH 09/20] fix: change maxNumberOfAccountsToAdd default value from 100 to Infinity (#5322) ## Explanation This PR changes the default value for account syncing's `maxNumberOfAccountsToAdd` from `100` to `Infinity`. It will become the client's responsibility to set this number to a specific value. If not specified, account sync will synchronize every account. ## References Related to: https://consensyssoftware.atlassian.net/browse/IDENTITY-28 ## Changelog ### `@metamask/profile-sync-controller` - **CHANGED**: change `maxNumberOfAccountsToAdd` default value from `100` to `Infinity` ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../user-storage/account-syncing/controller-integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts index 53c150aef9f..d8b9cae7293 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts @@ -123,7 +123,7 @@ export async function syncInternalAccountsWithUserStorage( } const { - maxNumberOfAccountsToAdd = 100, + maxNumberOfAccountsToAdd = Infinity, onAccountAdded, onAccountNameUpdated, onAccountSyncErroneousSituation, From e94aede449794f2311ee30162814c09f2fe8162c Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Thu, 13 Feb 2025 13:29:43 +0000 Subject: [PATCH 10/20] feat: better handle notification account tracking on wallet unlock (#5323) ## Explanation Our constructor fired account calls even when the wallet was locked. We added similar logic as push notifications to ensure we only make the initialise call after the wallet is unlocked. ## References N/A ## Changelog ### `@metamask/notification-services-controller` - **ADDED**: lock conditional checks when initialising accounts inside the NotificationServicesController - **ADDED**: accounts initialise call when the wallet is unlocked ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../NotificationServicesController.test.ts | 166 ++++++++++++++++-- .../NotificationServicesController.ts | 17 +- 2 files changed, 161 insertions(+), 22 deletions(-) diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts index c48711f6509..586c990a157 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts @@ -2,6 +2,7 @@ import { Messenger } from '@metamask/base-controller'; import * as ControllerUtils from '@metamask/controller-utils'; import type { KeyringControllerGetAccountsAction, + KeyringControllerGetStateAction, KeyringControllerState, } from '@metamask/keyring-controller'; import type { UserStorageController } from '@metamask/profile-sync-controller'; @@ -39,6 +40,7 @@ import type { NotificationServicesPushControllerUpdateTriggerPushNotifications, NotificationServicesControllerMessenger, NotificationServicesControllerState, + NotificationServicesPushControllerSubscribeToNotifications, } from './NotificationServicesController'; import { processFeatureAnnouncement } from './processors'; import { processNotification } from './processors/process-notifications'; @@ -186,40 +188,152 @@ describe('metamask-notifications - constructor()', () => { }); }); - it('initializes push notifications', async () => { - const { messenger, mockEnablePushNotifications } = arrangeMocks(); + const arrangeActInitialisePushNotifications = ( + modifications?: (mocks: ReturnType) => void, + ) => { + // Arrange + const mocks = arrangeMocks(); + modifications?.(mocks); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _controller = new NotificationServicesController({ - messenger, + // Act + new NotificationServicesController({ + messenger: mocks.messenger, env: { featureAnnouncements: featureAnnouncementsEnv }, state: { isNotificationServicesEnabled: true }, }); + return mocks; + }; + + it('initializes push notifications', async () => { + const { mockEnablePushNotifications } = + arrangeActInitialisePushNotifications(); + await waitFor(() => { expect(mockEnablePushNotifications).toHaveBeenCalled(); }); }); - it('fails to initialize push notifications', async () => { - const { messenger, mockPerformGetStorage, mockEnablePushNotifications } = - arrangeMocks(); + it('does not initialise push notifications if the wallet is locked', async () => { + const { mockEnablePushNotifications, mockSubscribeToPushNotifications } = + arrangeActInitialisePushNotifications((mocks) => { + mocks.mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: false, // Wallet Locked + } as MockVar); + }); - // test when user storage is empty - mockPerformGetStorage.mockResolvedValue(null); + await waitFor(() => { + expect(mockEnablePushNotifications).not.toHaveBeenCalled(); + }); + await waitFor(() => { + expect(mockSubscribeToPushNotifications).toHaveBeenCalled(); + }); + }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _controller = new NotificationServicesController({ - messenger, - env: { featureAnnouncements: featureAnnouncementsEnv }, - state: { isNotificationServicesEnabled: true }, + it('should re-initialise push notifications if wallet was locked, and then is unlocked', async () => { + // Test Wallet Lock + const { + globalMessenger, + mockEnablePushNotifications, + mockSubscribeToPushNotifications, + } = arrangeActInitialisePushNotifications((mocks) => { + mocks.mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: false, // Wallet Locked + } as MockVar); }); + await waitFor(() => { + expect(mockEnablePushNotifications).not.toHaveBeenCalled(); + }); + await waitFor(() => { + expect(mockSubscribeToPushNotifications).toHaveBeenCalled(); + }); + + // Test Wallet Unlock + jest.clearAllMocks(); + await globalMessenger.publish('KeyringController:unlock'); + await waitFor(() => { + expect(mockEnablePushNotifications).toHaveBeenCalled(); + }); + await waitFor(() => { + expect(mockSubscribeToPushNotifications).not.toHaveBeenCalled(); + }); + }); + + it('bails push notification initialisation if fails to get notification storage', async () => { + const { mockPerformGetStorage, mockEnablePushNotifications } = + arrangeActInitialisePushNotifications((mocks) => { + // test when user storage is empty + mocks.mockPerformGetStorage.mockResolvedValue(null); + }); + await waitFor(() => { expect(mockPerformGetStorage).toHaveBeenCalled(); }); - expect(mockEnablePushNotifications).not.toHaveBeenCalled(); + await waitFor(() => { + expect(mockEnablePushNotifications).not.toHaveBeenCalled(); + }); + }); + + const arrangeActInitialiseNotificationAccountTracking = ( + modifications?: (mocks: ReturnType) => void, + ) => { + // Arrange + const mocks = arrangeMocks(); + modifications?.(mocks); + + // Act + new NotificationServicesController({ + messenger: mocks.messenger, + env: { + featureAnnouncements: featureAnnouncementsEnv, + isPushIntegrated: false, + }, + state: { isNotificationServicesEnabled: true }, + }); + + return mocks; + }; + + it('should initialse accounts to track notifications on', async () => { + const { mockListAccounts } = + arrangeActInitialiseNotificationAccountTracking(); + await waitFor(() => { + expect(mockListAccounts).toHaveBeenCalled(); + }); + }); + + it('should not initialise accounts if wallet is locked', async () => { + const { mockListAccounts } = + arrangeActInitialiseNotificationAccountTracking((mocks) => { + mocks.mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: false, + } as MockVar); + }); + await waitFor(() => { + expect(mockListAccounts).not.toHaveBeenCalled(); + }); + }); + + it('should re-initialise if the wallet was locked, and then unlocked', async () => { + // Test Wallet Locked + const { globalMessenger, mockListAccounts } = + arrangeActInitialiseNotificationAccountTracking((mocks) => { + mocks.mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: false, + } as MockVar); + }); + await waitFor(() => { + expect(mockListAccounts).not.toHaveBeenCalled(); + }); + + // Test Wallet Unlock + jest.clearAllMocks(); + await globalMessenger.publish('KeyringController:unlock'); + await waitFor(() => { + expect(mockListAccounts).toHaveBeenCalled(); + }); }); }); @@ -976,6 +1090,7 @@ function mockNotificationMessenger() { 'NotificationServicesPushController:disablePushNotifications', 'NotificationServicesPushController:enablePushNotifications', 'NotificationServicesPushController:updateTriggerPushNotifications', + 'NotificationServicesPushController:subscribeToPushNotifications', 'UserStorageController:getStorageKey', 'UserStorageController:performGetStorage', 'UserStorageController:performSetStorage', @@ -1015,6 +1130,9 @@ function mockNotificationMessenger() { const mockUpdateTriggerPushNotifications = typedMockAction(); + const mockSubscribeToPushNotifications = + typedMockAction(); + const mockGetStorageKey = typedMockAction().mockResolvedValue( 'MOCK_STORAGE_KEY', @@ -1028,6 +1146,11 @@ function mockNotificationMessenger() { const mockPerformSetStorage = typedMockAction(); + const mockKeyringControllerGetState = + typedMockAction().mockReturnValue({ + isUnlocked: true, + } as MockVar); + jest.spyOn(messenger, 'call').mockImplementation((...args) => { const [actionType] = args; @@ -1040,7 +1163,7 @@ function mockNotificationMessenger() { } if (actionType === 'KeyringController:getState') { - return { isUnlocked: true } as MockVar; + return mockKeyringControllerGetState(); } if (actionType === 'AuthenticationController:getBearerToken') { @@ -1077,6 +1200,13 @@ function mockNotificationMessenger() { return mockUpdateTriggerPushNotifications(params[0]); } + if ( + actionType === + 'NotificationServicesPushController:subscribeToPushNotifications' + ) { + return mockSubscribeToPushNotifications(); + } + if (actionType === 'UserStorageController:getStorageKey') { return mockGetStorageKey(); } @@ -1104,9 +1234,11 @@ function mockNotificationMessenger() { mockDisablePushNotifications, mockEnablePushNotifications, mockUpdateTriggerPushNotifications, + mockSubscribeToPushNotifications, mockGetStorageKey, mockPerformGetStorage, mockPerformSetStorage, + mockKeyringControllerGetState, }; } diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts index 37346ecf408..b925fb9463e 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts @@ -445,6 +445,9 @@ export default class NotificationServicesController extends BaseController< }; readonly #accounts = { + // Flag to ensure we only setup once + isNotificationAccountsSetup: false, + /** * Used to get list of addresses from keyring (wallet addresses) * @@ -492,8 +495,11 @@ export default class NotificationServicesController extends BaseController< * * @returns result from list accounts */ - initialize: () => { - return this.#accounts.listAccounts(); + initialize: async (): Promise => { + if (this.#isUnlocked && !this.#accounts.isNotificationAccountsSetup) { + await this.#accounts.listAccounts(); + this.#accounts.isNotificationAccountsSetup = true; + } }, /** @@ -562,9 +568,10 @@ export default class NotificationServicesController extends BaseController< this.#registerMessageHandlers(); this.#clearLoadingStates(); - this.#keyringController.setupLockedStateSubscriptions( - this.#pushNotifications.initializePushNotifications, - ); + this.#keyringController.setupLockedStateSubscriptions(async () => { + await this.#accounts.initialize(); + await this.#pushNotifications.initializePushNotifications(); + }); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.#accounts.initialize(); // eslint-disable-next-line @typescript-eslint/no-floating-promises From 13ca53857773b955b1f6b8852195f9e440e65c5e Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 13 Feb 2025 15:49:25 +0100 Subject: [PATCH 11/20] fix: remove unused events from `UserStorageController` (#5324) ## Explanation This PR removes unused event types from `UserStorageController` ## References ## Changelog ### `@metamask/profile-sync-controller` - **REMOVED**: removed unused events from `UserStorageController` ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../user-storage/UserStorageController.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index 66f82d47777..f9108a81a22 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -256,23 +256,11 @@ export type UserStorageControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, UserStorageControllerState >; -export type UserStorageControllerAccountSyncingInProgress = { - type: `${typeof controllerName}:accountSyncingInProgress`; - payload: [boolean]; -}; -export type UserStorageControllerAccountSyncingComplete = { - type: `${typeof controllerName}:accountSyncingComplete`; - payload: [boolean]; -}; -export type Events = - | UserStorageControllerStateChangeEvent - | UserStorageControllerAccountSyncingInProgress - | UserStorageControllerAccountSyncingComplete; + +export type Events = UserStorageControllerStateChangeEvent; export type AllowedEvents = | UserStorageControllerStateChangeEvent - | UserStorageControllerAccountSyncingInProgress - | UserStorageControllerAccountSyncingComplete | KeyringControllerLockEvent | KeyringControllerUnlockEvent // Account Syncing Events From 422ab57dccddef1873d674ad1e20621bd665f8e4 Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Thu, 13 Feb 2025 17:25:35 +0100 Subject: [PATCH 12/20] fix: fix account controller legacy polling (#5321) ## Explanation Changes were made to accountTrackerController inside the @metamask/assets-controllers package. The account tracker polling was executed on the constructor with the initialisation. This caused additional requests to be sent by the app, leading to performance issues. Additionally, the accountTracker was still using legacy polling via the poll function. This function has now been removed, and a test has been added to prevent the creation of multiple polling instances. These changes improve efficiency by reducing unnecessary network requests and ensuring the polling mechanism is properly controlled. ## References ## Changelog ### `@metamask/assets-controllers` - **CHANGED**: Removed legacy poll function to prevent redundant polling. - **FIXED**: ensure that the polling is not triggered on the constructor with the initialisation ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes Co-authored-by: sahar-fehri --- eslint-warning-thresholds.json | 3 +- .../src/AccountTrackerController.test.ts | 52 +++++++++++++------ .../src/AccountTrackerController.ts | 29 ----------- 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 19a192a361b..ae34e6428f5 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -32,8 +32,7 @@ "no-shadow": 2 }, "packages/assets-controllers/src/AccountTrackerController.test.ts": { - "import-x/namespace": 2, - "import-x/order": 2 + "import-x/namespace": 2 }, "packages/assets-controllers/src/AccountTrackerController.ts": { "jsdoc/check-tag-names": 5, diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index 227d4cb30c0..78787be4e79 100644 --- a/packages/assets-controllers/src/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -9,6 +9,12 @@ import { import { getDefaultPreferencesState } from '@metamask/preferences-controller'; import * as sinon from 'sinon'; +import type { + AccountTrackerControllerMessenger, + AllowedActions, + AllowedEvents, +} from './AccountTrackerController'; +import { AccountTrackerController } from './AccountTrackerController'; import { advanceTime } from '../../../tests/helpers'; import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import type { @@ -19,12 +25,6 @@ import { buildCustomNetworkClientConfiguration, buildMockGetNetworkClientById, } from '../../network-controller/tests/helpers'; -import type { - AccountTrackerControllerMessenger, - AllowedActions, - AllowedEvents, -} from './AccountTrackerController'; -import { AccountTrackerController } from './AccountTrackerController'; jest.mock('@metamask/controller-utils', () => { return { @@ -95,12 +95,6 @@ describe('AccountTrackerController', () => { }); describe('refresh', () => { - beforeEach(() => { - jest - .spyOn(AccountTrackerController.prototype, 'poll') - .mockImplementationOnce(async () => Promise.resolve()); - }); - describe('without networkClientId', () => { it('should sync addresses', async () => { const mockAddress1 = '0xbabe9bbeab5f83a755ac92c7a09b9ab3ff527f8c'; @@ -787,8 +781,11 @@ describe('AccountTrackerController', () => { }); }); - it('should call refresh every interval on legacy polling', async () => { - const pollSpy = jest.spyOn(AccountTrackerController.prototype, 'poll'); + it('should call refresh every interval on polling', async () => { + const pollSpy = jest.spyOn( + AccountTrackerController.prototype, + '_executePoll', + ); await withController( { options: { interval: 100 }, @@ -799,6 +796,11 @@ describe('AccountTrackerController', () => { async ({ controller }) => { jest.spyOn(controller, 'refresh').mockResolvedValue(); + await controller.startPolling({ + networkClientId: 'networkClientId1', + }); + await advanceTime({ clock, duration: 1 }); + expect(pollSpy).toHaveBeenCalledTimes(1); await advanceTime({ clock, duration: 50 }); @@ -813,7 +815,6 @@ describe('AccountTrackerController', () => { }); it('should call refresh every interval for each networkClientId being polled', async () => { - jest.spyOn(AccountTrackerController.prototype, 'poll').mockResolvedValue(); const networkClientId1 = 'networkClientId1'; const networkClientId2 = 'networkClientId2'; await withController( @@ -867,6 +868,27 @@ describe('AccountTrackerController', () => { }, ); }); + + it('should not call polling twice', async () => { + await withController( + { + options: { interval: 100 }, + }, + async ({ controller }) => { + const refreshSpy = jest + .spyOn(controller, 'refresh') + .mockResolvedValue(); + + expect(refreshSpy).not.toHaveBeenCalled(); + controller.startPolling({ + networkClientId: 'networkClientId1', + }); + + await advanceTime({ clock, duration: 1 }); + expect(refreshSpy).toHaveBeenCalledTimes(1); + }, + ); + }); }); type WithControllerCallback = ({ diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index 38b37c5e326..a0133623d02 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -146,8 +146,6 @@ export class AccountTrackerController extends StaticIntervalPollingController; - /** * Creates an AccountTracker instance. * @@ -198,10 +196,6 @@ export class AccountTrackerController extends StaticIntervalPollingController { - if (interval) { - this.setIntervalLength(interval); - } - - if (this.#handle) { - clearTimeout(this.#handle); - } - - await this.refresh(); - - this.#handle = setTimeout(() => { - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.poll(this.getIntervalLength()); - }, this.getIntervalLength()); - } - /** * Refreshes the balances of the accounts using the networkClientId * From fe244b1f1fa300cb9ad98e2ac229a31d1cb7f4a2 Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Thu, 13 Feb 2025 09:50:03 -0800 Subject: [PATCH 13/20] Follow up comments in MultichainNetworkController PR (#5320) ## Explanation Addresses post PR comments from @ccharly in - https://github.com/MetaMask/core/pull/5215/files ## References ## Changelog ### `@metamask/multichain-network-controller` - Add `@metamask/network-controller` `^22.2.1` as a missing dependency ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../src/AccountsController.ts | 10 +-- .../package.json | 4 +- .../src/MultichainNetworkController.test.ts | 24 ++++++ .../src/MultichainNetworkController.ts | 84 ++++++++++--------- yarn.lock | 4 +- 5 files changed, 76 insertions(+), 50 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 4c582b03ab1..84ea1a113ad 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -1131,9 +1131,7 @@ export class AccountsController extends BaseController< * * @param id - The EVM client ID or non-EVM chain ID that changed. */ - readonly #handleMultichainNetworkChange = ( - id: NetworkClientId | CaipChainId, - ) => { + #handleOnMultichainNetworkDidChange(id: NetworkClientId | CaipChainId) { let accountId: string; // We only support non-EVM Caip chain IDs at the moment. Ex Solana and Bitcoin @@ -1154,7 +1152,9 @@ export class AccountsController extends BaseController< Date.now(); currentState.internalAccounts.selectedAccount = accountId; }); - }; + + // DO NOT publish AccountsController:setSelectedAccount to prevent circular listener loops + } /** * Retrieves the value of a specific metadata key for an existing account. @@ -1218,7 +1218,7 @@ export class AccountsController extends BaseController< // Handle account change when multichain network is changed this.messagingSystem.subscribe( 'MultichainNetworkController:networkDidChange', - this.#handleMultichainNetworkChange, + (id) => this.#handleOnMultichainNetworkDidChange(id), ); } diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index b22fb7a372e..b8ba81573d9 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -55,6 +55,7 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.1.0", + "@metamask/network-controller": "^22.2.1", "@types/jest": "^27.4.1", "@types/uuid": "^8.3.0", "deepmerge": "^4.2.2", @@ -67,8 +68,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^23.0.0", - "@metamask/network-controller": "^22.1.1" + "@metamask/network-controller": "^22.2.1" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index f5dc5beedf4..5f4728054f0 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -226,8 +226,12 @@ describe('MultichainNetworkController', () => { getNetworkState: jest.fn().mockImplementation(() => ({ selectedNetworkClientId, })), + options: { state: { isEvmSelected: false } }, }); + // Check that EVM network is not selected + expect(controller.state.isEvmSelected).toBe(false); + await controller.setActiveNetwork(selectedNetworkClientId); // Check that EVM network is selected @@ -265,6 +269,26 @@ describe('MultichainNetworkController', () => { // Check that NetworkController:setActiveNetwork was not called expect(mockSetActiveNetwork).toHaveBeenCalledWith(evmNetworkClientId); }); + + it('should not do anything when same EVM network is set and active', async () => { + const { controller, publishSpy } = setupController({ + getNetworkState: jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: InfuraNetworkType.mainnet, + })), + options: { state: { isEvmSelected: true } }, + }); + + // EVM network is already active + expect(controller.state.isEvmSelected).toBe(true); + + await controller.setActiveNetwork(InfuraNetworkType.mainnet); + + // EVM network is still active + expect(controller.state.isEvmSelected).toBe(true); + + // Check that the messenger published the correct event + expect(publishSpy).not.toHaveBeenCalled(); + }); }); describe('handle AccountsController:selectedAccountChange event', () => { diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 572dfa6d12c..b9c3d5f441b 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -58,65 +58,63 @@ export class MultichainNetworkController extends BaseController< * @param id - The client ID of the EVM network to set active. */ async #setActiveEvmNetwork(id: NetworkClientId): Promise { - // Notify listeners that setActiveNetwork was called - this.messagingSystem.publish( - 'MultichainNetworkController:networkDidChange', - id, - ); - - // Indicate that the non-EVM network is not selected - this.update((state) => { - state.isEvmSelected = true; - }); - - // Prevent setting same network const { selectedNetworkClientId } = this.messagingSystem.call( 'NetworkController:getState', ); - if (id === selectedNetworkClientId) { - // EVM network is already selected, no need to update NetworkController + const shouldSetEvmActive = !this.state.isEvmSelected; + const shouldNotifyNetworkChange = id !== selectedNetworkClientId; + + // No changes needed if EVM is active and network is already selected + if (!shouldSetEvmActive && !shouldNotifyNetworkChange) { return; } - // Update evm active network - await this.messagingSystem.call('NetworkController:setActiveNetwork', id); - } - - /** - * Sets the active non-EVM network. - * - * @param id - The chain ID of the non-EVM network to set active. - */ - #setActiveNonEvmNetwork(id: SupportedCaipChainId): void { - if (id === this.state.selectedMultichainNetworkChainId) { - if (!this.state.isEvmSelected) { - // Same non-EVM network is already selected, no need to update - return; - } - - // Indicate that the non-EVM network is selected + // Update EVM selection state if needed + if (shouldSetEvmActive) { this.update((state) => { - state.isEvmSelected = false; + state.isEvmSelected = true; }); + } - // Notify listeners that setActiveNetwork was called + // Only notify the network controller if the selected evm network is different + if (shouldNotifyNetworkChange) { + await this.messagingSystem.call('NetworkController:setActiveNetwork', id); + } + + // Only publish the networkDidChange event if either the EVM network is different or we're switching between EVM and non-EVM networks + if (shouldSetEvmActive || shouldNotifyNetworkChange) { this.messagingSystem.publish( 'MultichainNetworkController:networkDidChange', id, ); } + } - // Notify listeners that setActiveNetwork was called - this.messagingSystem.publish( - 'MultichainNetworkController:networkDidChange', - id, - ); + /** + * Sets the active non-EVM network. + * + * @param id - The chain ID of the non-EVM network to set active. + */ + #setActiveNonEvmNetwork(id: SupportedCaipChainId): void { + if ( + id === this.state.selectedMultichainNetworkChainId && + !this.state.isEvmSelected + ) { + // Same non-EVM network is already selected, no need to update + return; + } this.update((state) => { state.selectedMultichainNetworkChainId = id; state.isEvmSelected = false; }); + + // Notify listeners that the network changed + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + id, + ); } /** @@ -144,7 +142,7 @@ export class MultichainNetworkController extends BaseController< * * @param account - The account that was changed */ - readonly #handleSelectedAccountChange = (account: InternalAccount) => { + #handleOnSelectedAccountChange(account: InternalAccount) { const { type: accountType, address: accountAddress } = account; const isEvmAccount = isEvmAccountType(accountType); @@ -159,6 +157,7 @@ export class MultichainNetworkController extends BaseController< this.update((state) => { state.isEvmSelected = true; }); + return; } @@ -179,7 +178,10 @@ export class MultichainNetworkController extends BaseController< state.selectedMultichainNetworkChainId = nonEvmChainId; state.isEvmSelected = false; }); - }; + + // No need to publish NetworkController:setActiveNetwork because EVM accounts falls back to use the last selected EVM network + // DO NOT publish MultichainNetworkController:networkDidChange to prevent circular listener loops + } /** * Subscribes to message events. @@ -188,7 +190,7 @@ export class MultichainNetworkController extends BaseController< // Handle network switch when account is changed this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', - this.#handleSelectedAccountChange, + (account) => this.#handleOnSelectedAccountChange(account), ); } diff --git a/yarn.lock b/yarn.lock index 57dcd4b6f07..aa807d6b2b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3461,6 +3461,7 @@ __metadata: "@metamask/base-controller": "npm:^8.0.0" "@metamask/keyring-api": "npm:^17.0.0" "@metamask/keyring-controller": "npm:^19.1.0" + "@metamask/network-controller": "npm:^22.2.1" "@metamask/utils": "npm:^11.1.0" "@solana/addresses": "npm:^2.0.0" "@types/jest": "npm:^27.4.1" @@ -3474,8 +3475,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/accounts-controller": ^23.0.0 - "@metamask/network-controller": ^22.1.1 + "@metamask/network-controller": ^22.2.1 languageName: unknown linkType: soft From d2d180059a62b8a4a07dc4ca22a4759c2e7546ea Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 13 Feb 2025 22:56:38 +0100 Subject: [PATCH 14/20] fix: fix peer deps for `@metamask/{accounts,multichain-network}-controller` (#5327) ## Explanation The 2 controllers depend on each other, however we cannot really express this with peer deps. So for now, we only declare this "relation" on the new controller `mutltichain-network-controller`. Also downgrading some peer dep to use the major version. ## References N/A ## Changelog N/A ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Cal-L --- packages/accounts-controller/package.json | 6 +++--- packages/multichain-network-controller/package.json | 3 ++- yarn.lock | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 9260adcd998..0c6c3f7ac29 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -52,6 +52,7 @@ "@metamask/eth-snap-keyring": "^10.0.0", "@metamask/keyring-api": "^17.0.0", "@metamask/keyring-internal-api": "^4.0.1", + "@metamask/network-controller": "^22.2.1", "@metamask/snaps-sdk": "^6.17.1", "@metamask/snaps-utils": "^8.10.0", "@metamask/utils": "^11.1.0", @@ -63,7 +64,6 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.1.0", - "@metamask/network-controller": "^22.2.1", "@metamask/providers": "^18.1.1", "@metamask/snaps-controllers": "^9.19.0", "@types/jest": "^27.4.1", @@ -76,8 +76,8 @@ "webextension-polyfill": "^0.12.0" }, "peerDependencies": { - "@metamask/keyring-controller": "^19.1.0", - "@metamask/network-controller": "^22.2.1", + "@metamask/keyring-controller": "^19.0.0", + "@metamask/network-controller": "^22.0.0", "@metamask/providers": "^18.1.0", "@metamask/snaps-controllers": "^9.19.0", "webextension-polyfill": "^0.10.0 || ^0.11.0 || ^0.12.0" diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index b8ba81573d9..6fd4275f479 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -68,7 +68,8 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/network-controller": "^22.2.1" + "@metamask/accounts-controller": "^23.0.0", + "@metamask/network-controller": "^22.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/yarn.lock b/yarn.lock index aa807d6b2b7..dc8e1e4cfac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2370,8 +2370,8 @@ __metadata: uuid: "npm:^8.3.2" webextension-polyfill: "npm:^0.12.0" peerDependencies: - "@metamask/keyring-controller": ^19.1.0 - "@metamask/network-controller": ^22.2.1 + "@metamask/keyring-controller": ^19.0.0 + "@metamask/network-controller": ^22.0.0 "@metamask/providers": ^18.1.0 "@metamask/snaps-controllers": ^9.19.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 @@ -3475,7 +3475,8 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/network-controller": ^22.2.1 + "@metamask/accounts-controller": ^23.0.0 + "@metamask/network-controller": ^22.0.0 languageName: unknown linkType: soft From d931ffa83f80fc4001e459754209a41e09702c93 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 14 Feb 2025 07:28:42 +0900 Subject: [PATCH 15/20] feat: bridge controller (#5276) ## Explanation This PR adds a new controller: `BridgeController`. This controller handles the quote fetching and polling from the Bridge API. ## References This is a port of the `BridgeController` from Extension: https://github.com/MetaMask/metamask-extension/tree/main/app/scripts/controllers/bridge Some minor changes were needed to fill in the missing functions and variables from Extension. This package will be consumed initially by the Metamask Mobile application first. Eventually, we wish to migrate the Extension to use this `core/bridge-controller` package. ## Changelog ### `@metamask/bridge-controller` - ****: New `BridgeController`! ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Elliot Winkler --- .github/CODEOWNERS | 6 +- .gitignore | 2 +- README.md | 1 + packages/bridge-controller/CHANGELOG.md | 14 + packages/bridge-controller/LICENSE | 20 + packages/bridge-controller/README.md | 15 + packages/bridge-controller/jest.config.js | 26 + packages/bridge-controller/package.json | 87 ++ .../src/bridge-controller.test.ts | 830 ++++++++++++++++ .../src/bridge-controller.ts | 375 ++++++++ .../bridge-controller/src/constants/bridge.ts | 68 ++ .../bridge-controller/src/constants/chains.ts | 173 ++++ .../bridge-controller/src/constants/swaps.ts | 1 + .../bridge-controller/src/constants/tokens.ts | 144 +++ packages/bridge-controller/src/index.ts | 61 ++ packages/bridge-controller/src/types.ts | 274 ++++++ .../src/utils/balance.test.ts | 249 +++++ .../bridge-controller/src/utils/balance.ts | 51 + .../src/utils/bridge.test.ts | 170 ++++ .../bridge-controller/src/utils/bridge.ts | 101 ++ .../bridge-controller/src/utils/fetch.test.ts | 350 +++++++ packages/bridge-controller/src/utils/fetch.ts | 201 ++++ packages/bridge-controller/src/utils/quote.ts | 36 + .../bridge-controller/src/utils/validators.ts | 162 ++++ .../tests/mock-quotes-erc20-erc20.json | 248 +++++ .../tests/mock-quotes-erc20-native.json | 894 ++++++++++++++++++ .../tests/mock-quotes-native-erc20-eth.json | 258 +++++ .../tests/mock-quotes-native-erc20.json | 294 ++++++ .../bridge-controller/tsconfig.build.json | 17 + packages/bridge-controller/tsconfig.json | 16 + packages/bridge-controller/typedoc.json | 7 + teams.json | 1 + yarn.lock | 33 + 33 files changed, 5183 insertions(+), 2 deletions(-) create mode 100644 packages/bridge-controller/CHANGELOG.md create mode 100644 packages/bridge-controller/LICENSE create mode 100644 packages/bridge-controller/README.md create mode 100644 packages/bridge-controller/jest.config.js create mode 100644 packages/bridge-controller/package.json create mode 100644 packages/bridge-controller/src/bridge-controller.test.ts create mode 100644 packages/bridge-controller/src/bridge-controller.ts create mode 100644 packages/bridge-controller/src/constants/bridge.ts create mode 100644 packages/bridge-controller/src/constants/chains.ts create mode 100644 packages/bridge-controller/src/constants/swaps.ts create mode 100644 packages/bridge-controller/src/constants/tokens.ts create mode 100644 packages/bridge-controller/src/index.ts create mode 100644 packages/bridge-controller/src/types.ts create mode 100644 packages/bridge-controller/src/utils/balance.test.ts create mode 100644 packages/bridge-controller/src/utils/balance.ts create mode 100644 packages/bridge-controller/src/utils/bridge.test.ts create mode 100644 packages/bridge-controller/src/utils/bridge.ts create mode 100644 packages/bridge-controller/src/utils/fetch.test.ts create mode 100644 packages/bridge-controller/src/utils/fetch.ts create mode 100644 packages/bridge-controller/src/utils/quote.ts create mode 100644 packages/bridge-controller/src/utils/validators.ts create mode 100644 packages/bridge-controller/tests/mock-quotes-erc20-erc20.json create mode 100644 packages/bridge-controller/tests/mock-quotes-erc20-native.json create mode 100644 packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json create mode 100644 packages/bridge-controller/tests/mock-quotes-native-erc20.json create mode 100644 packages/bridge-controller/tsconfig.build.json create mode 100644 packages/bridge-controller/tsconfig.json create mode 100644 packages/bridge-controller/typedoc.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 159a42b2133..fcfb57e5fde 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -37,6 +37,9 @@ ## Snaps Team /packages/rate-limit-controller @MetaMask/snaps-devs +## Swaps-Bridge Team +/packages/bridge-controller @MetaMask/swaps-engineers + ## Portfolio Team /packages/token-search-discovery-controller @MetaMask/portfolio @@ -112,4 +115,5 @@ /packages/multichain-transactions-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/token-search-discovery-controller/package.json @MetaMask/portfolio @MetaMask/wallet-framework-engineers /packages/token-search-discovery-controller/CHANGELOG.md @MetaMask/portfolio @MetaMask/wallet-framework-engineers - +/packages/bridge-controller/package.json @MetaMask/swaps-engineers @MetaMask/wallet-framework-engineers +/packages/bridge-controller/CHANGELOG.md @MetaMask/swaps-engineers @MetaMask/wallet-framework-engineers \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5043addaa41..6c1e52eb80d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ scripts/coverage !.yarn/versions # typescript -packages/*/*.tsbuildinfo +packages/*/*.tsbuildinfo \ No newline at end of file diff --git a/README.md b/README.md index 6ffab5f882b..d35222d2543 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/approval-controller`](packages/approval-controller) - [`@metamask/assets-controllers`](packages/assets-controllers) - [`@metamask/base-controller`](packages/base-controller) +- [`@metamask/bridge-controller`](packages/bridge-controller) - [`@metamask/build-utils`](packages/build-utils) - [`@metamask/composable-controller`](packages/composable-controller) - [`@metamask/controller-utils`](packages/controller-utils) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md new file mode 100644 index 00000000000..8fcf72c699c --- /dev/null +++ b/packages/bridge-controller/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial release + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/bridge-controller/LICENSE b/packages/bridge-controller/LICENSE new file mode 100644 index 00000000000..7d002dced3a --- /dev/null +++ b/packages/bridge-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/bridge-controller/README.md b/packages/bridge-controller/README.md new file mode 100644 index 00000000000..adb050aedec --- /dev/null +++ b/packages/bridge-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/bridge-controller` + +Manages bridge-related quote fetching functionality for MetaMask. + +## Installation + +`yarn add @metamask/bridge-controller` + +or + +`npm install @metamask/bridge-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/bridge-controller/jest.config.js b/packages/bridge-controller/jest.config.js new file mode 100644 index 00000000000..d67e30322b8 --- /dev/null +++ b/packages/bridge-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 93, + functions: 98, + lines: 99, + statements: 99, + }, + }, +}); diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json new file mode 100644 index 00000000000..7b4a0b7571a --- /dev/null +++ b/packages/bridge-controller/package.json @@ -0,0 +1,87 @@ +{ + "name": "@metamask/bridge-controller", + "version": "0.0.0", + "description": "Manages bridge-related quote fetching functionality for MetaMask", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/bridge-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/bridge-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/bridge-controller", + "publish:preview": "yarn npm publish --tag preview", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^8.0.0", + "@metamask/controller-utils": "^11.5.0", + "@metamask/metamask-eth-abis": "^3.1.1", + "@metamask/polling-controller": "^12.0.3", + "@metamask/utils": "^11.1.0", + "ethers": "^6.12.0" + }, + "devDependencies": { + "@metamask/accounts-controller": "^23.1.0", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eth-json-rpc-provider": "^4.1.8", + "@metamask/json-rpc-engine": "^10.0.3", + "@metamask/network-controller": "^22.2.1", + "@metamask/transaction-controller": "^45.1.0", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "lodash": "^4.17.21", + "nock": "^13.3.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2" + }, + "peerDependencies": { + "@metamask/accounts-controller": "^23.0.0", + "@metamask/network-controller": "^22.0.0", + "@metamask/transaction-controller": "^45.0.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts new file mode 100644 index 00000000000..3bc04fcc973 --- /dev/null +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -0,0 +1,830 @@ +import type { Hex } from '@metamask/utils'; +import { bigIntToHex } from '@metamask/utils'; +import { Contract } from 'ethers'; +import nock from 'nock'; + +import { BridgeController } from './bridge-controller'; +import { + BridgeClientId, + DEFAULT_BRIDGE_CONTROLLER_STATE, +} from './constants/bridge'; +import { CHAIN_IDS } from './constants/chains'; +import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; +import type { BridgeControllerMessenger, QuoteResponse } from './types'; +import * as balanceUtils from './utils/balance'; +import { getBridgeApiBaseUrl } from './utils/bridge'; +import * as fetchUtils from './utils/fetch'; +import { flushPromises } from '../../../tests/helpers'; +import { handleFetch } from '../../controller-utils/src'; +import mockBridgeQuotesErc20Native from '../tests/mock-quotes-erc20-native.json'; +import mockBridgeQuotesNativeErc20Eth from '../tests/mock-quotes-native-erc20-eth.json'; +import mockBridgeQuotesNativeErc20 from '../tests/mock-quotes-native-erc20.json'; + +const EMPTY_INIT_STATE = { + bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, +}; + +const messengerMock = { + call: jest.fn(), + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + publish: jest.fn(), +} as unknown as jest.Mocked; + +jest.mock('ethers', () => { + return { + ...jest.requireActual('ethers'), + Contract: jest.fn(), + BrowserProvider: jest.fn(), + }; +}); +const getLayer1GasFeeMock = jest.fn(); +const mockFetchFn = handleFetch; + +describe('BridgeController', function () { + let bridgeController: BridgeController; + + beforeAll(function () { + bridgeController = new BridgeController({ + messenger: messengerMock, + getLayer1GasFee: getLayer1GasFeeMock, + clientId: BridgeClientId.EXTENSION, + fetchFn: mockFetchFn, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + + nock(getBridgeApiBaseUrl()) + .get('/getAllFeatureFlags') + .reply(200, { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 3, + support: true, + chains: { + '10': { + isActiveSrc: true, + isActiveDest: false, + }, + '534352': { + isActiveSrc: true, + isActiveDest: false, + }, + '137': { + isActiveSrc: false, + isActiveDest: true, + }, + '42161': { + isActiveSrc: false, + isActiveDest: true, + }, + }, + }, + 'approval-gas-multiplier': { + '137': 1.1, + '42161': 1.2, + '10': 1.3, + '534352': 1.4, + }, + 'bridge-gas-multiplier': { + '137': 2.1, + '42161': 2.2, + '10': 2.3, + '534352': 2.4, + }, + }); + nock(getBridgeApiBaseUrl()) + .get('/getTokens?chainId=10') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + aggregators: ['lifl', 'socket'], + }, + { + address: '0x1291478912', + symbol: 'DEF', + decimals: 16, + }, + ]); + nock(SWAPS_API_V2_BASE_URL) + .get('/networks/10/topAssets') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); + bridgeController.resetState(); + }); + + it('constructor should setup correctly', function () { + expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); + }); + + it('setBridgeFeatureFlags should fetch and set the bridge feature flags', async function () { + const expectedFeatureFlagsResponse = { + extensionConfig: { + maxRefreshCount: 3, + refreshRate: 3, + support: true, + chains: { + [CHAIN_IDS.OPTIMISM]: { isActiveSrc: true, isActiveDest: false }, + [CHAIN_IDS.SCROLL]: { isActiveSrc: true, isActiveDest: false }, + [CHAIN_IDS.POLYGON]: { isActiveSrc: false, isActiveDest: true }, + [CHAIN_IDS.ARBITRUM]: { isActiveSrc: false, isActiveDest: true }, + }, + }, + }; + expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); + + const setIntervalLengthSpy = jest.spyOn( + bridgeController, + 'setIntervalLength', + ); + + await bridgeController.setBridgeFeatureFlags(); + expect(bridgeController.state.bridgeState.bridgeFeatureFlags).toStrictEqual( + expectedFeatureFlagsResponse, + ); + expect(setIntervalLengthSpy).toHaveBeenCalledTimes(1); + expect(setIntervalLengthSpy).toHaveBeenCalledWith(3); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + bridgeFeatureFlags: expectedFeatureFlagsResponse, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + }); + + it('updateBridgeQuoteRequestParams should update the quoteRequest state', async function () { + await bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + await bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: 10, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + await bridgeController.updateBridgeQuoteRequestParams({ + destChainId: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: undefined, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + await bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: undefined, + walletAddress: undefined, + }); + + await bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + await bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: '0x2ABC', + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x2ABC', + walletAddress: undefined, + }); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + }); + + it('updateBridgeQuoteRequestParams should trigger quote polling if request is valid', async function () { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(true); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve(mockBridgeQuotesNativeErc20Eth as never); + }, 5000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([ + ...mockBridgeQuotesNativeErc20Eth, + ...mockBridgeQuotesNativeErc20Eth, + ] as never); + }, 10000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementationOnce(async () => { + return await new Promise((_resolve, reject) => { + return setTimeout(() => { + reject(new Error('Network error')); + }, 10000); + }); + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledWith({ + networkClientId: expect.anything(), + updatedQuoteRequest: { + ...quoteRequest, + insufficientBal: false, + }, + }); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // Loading state + jest.advanceTimersByTime(1000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( + { + ...quoteRequest, + insufficientBal: false, + }, + expect.any(AbortSignal), + BridgeClientId.EXTENSION, + mockFetchFn, + ); + expect( + bridgeController.state.bridgeState.quotesLastFetched, + ).toBeUndefined(); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: false }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(10000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: false }, + quotes: mockBridgeQuotesNativeErc20Eth, + quotesLoadingStatus: 1, + }), + ); + const firstFetchTime = bridgeController.state.bridgeState.quotesLastFetched; + expect(firstFetchTime).toBeGreaterThan(0); + + // After 2nd fetch + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: false }, + quotes: [ + ...mockBridgeQuotesNativeErc20Eth, + ...mockBridgeQuotesNativeErc20Eth, + ], + quotesLoadingStatus: 1, + quoteFetchError: undefined, + quotesRefreshCount: 2, + }), + ); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(2); + const secondFetchTime = + bridgeController.state.bridgeState.quotesLastFetched; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(secondFetchTime).toBeGreaterThan(firstFetchTime!); + + // After 3nd fetch throws an error + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(3); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: false }, + quotes: [ + ...mockBridgeQuotesNativeErc20Eth, + ...mockBridgeQuotesNativeErc20Eth, + ], + quotesLoadingStatus: 2, + quoteFetchError: 'Network error', + quotesRefreshCount: 3, + }), + ); + expect( + bridgeController.state.bridgeState.quotesLastFetched, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ).toBeGreaterThan(secondFetchTime!); + + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); + }); + + it('updateBridgeQuoteRequestParams should only poll once if insufficientBal=true', async function () { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(false); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve(mockBridgeQuotesNativeErc20Eth as never); + }, 5000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementation(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([ + ...mockBridgeQuotesNativeErc20Eth, + ...mockBridgeQuotesNativeErc20Eth, + ] as never); + }, 10000); + }); + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledWith({ + networkClientId: expect.anything(), + updatedQuoteRequest: { + ...quoteRequest, + insufficientBal: true, + }, + }); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesInitialLoadTime: undefined, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // Loading state + jest.advanceTimersByTime(1000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( + { + ...quoteRequest, + insufficientBal: true, + }, + expect.any(AbortSignal), + BridgeClientId.EXTENSION, + mockFetchFn, + ); + expect( + bridgeController.state.bridgeState.quotesLastFetched, + ).toBeUndefined(); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(10000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: mockBridgeQuotesNativeErc20Eth, + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + quotesInitialLoadTime: 11000, + }), + ); + const firstFetchTime = bridgeController.state.bridgeState.quotesLastFetched; + expect(firstFetchTime).toBeGreaterThan(0); + + // After 2nd fetch + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: mockBridgeQuotesNativeErc20Eth, + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + quotesInitialLoadTime: 11000, + }), + ); + const secondFetchTime = + bridgeController.state.bridgeState.quotesLastFetched; + expect(secondFetchTime).toStrictEqual(firstFetchTime); + expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); + }); + + it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', async function () { + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + await bridgeController.updateBridgeQuoteRequestParams({ + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + }); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).not.toHaveBeenCalled(); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + destChainId: 10, + destTokenAddress: '0x123', + }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + }); + + describe('getBridgeERC20Allowance', () => { + it('should return the atomic allowance of the ERC20 token contract', async () => { + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + allowance: jest.fn(() => '100000000000000000000'), + })); + + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const allowance = await bridgeController.getBridgeERC20Allowance( + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + '0xa', + ); + expect(allowance).toBe('100000000000000000000'); + }); + + it('should throw an error when no provider is found', async () => { + // Setup + const mockMessenger = { + call: jest.fn().mockImplementation((methodName) => { + // eslint-disable-next-line jest/no-conditional-in-test + if (methodName === 'NetworkController:getNetworkClientById') { + return { provider: null }; + } + // eslint-disable-next-line jest/no-conditional-in-test + if (methodName === 'NetworkController:getState') { + return { selectedNetworkClientId: 'testNetworkClientId' }; + } + return undefined; + }), + registerActionHandler: jest.fn(), + publish: jest.fn(), + registerInitialEventPayload: jest.fn(), + } as unknown as jest.Mocked; + + const controller = new BridgeController({ + messenger: mockMessenger, + clientId: BridgeClientId.EXTENSION, + getLayer1GasFee: jest.fn(), + fetchFn: mockFetchFn, + }); + + // Test + await expect( + controller.getBridgeERC20Allowance('0xContractAddress', '0x1'), + ).rejects.toThrow('No provider found'); + }); + }); + + it.each([ + [ + 'should append l1GasFees if srcChain is 10 and srcToken is erc20', + mockBridgeQuotesErc20Native as QuoteResponse[], + bigIntToHex(BigInt('2608710388388') * 2n), + 12, + ], + [ + 'should append l1GasFees if srcChain is 10 and srcToken is native', + mockBridgeQuotesNativeErc20 as unknown as QuoteResponse[], + bigIntToHex(BigInt('2608710388388')), + 2, + ], + [ + 'should not append l1GasFees if srcChain is not 10', + mockBridgeQuotesNativeErc20Eth as unknown as QuoteResponse[], + undefined, + 0, + ], + ])( + 'updateBridgeQuoteRequestParams: %s', + async ( + _testTitle: string, + quoteResponse: QuoteResponse[], + l1GasFeesInHexWei: Hex | undefined, + getLayer1GasFeeMockCallCount: number, + ) => { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(false); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + getLayer1GasFeeMock.mockResolvedValue('0x25F63418AA4'); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve(quoteResponse as never); + }, 1000); + }); + }); + + const quoteParams = { + srcChainId: 10, + destChainId: 1, + srcTokenAddress: '0x4200000000000000000000000000000000000006', + destTokenAddress: '0x0000000000000000000000000000000000000000', + srcTokenAmount: '991250000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledWith({ + networkClientId: expect.anything(), + updatedQuoteRequest: { + ...quoteRequest, + insufficientBal: true, + }, + }); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // // Loading state + jest.advanceTimersByTime(500); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( + { + ...quoteRequest, + insufficientBal: true, + }, + expect.any(AbortSignal), + BridgeClientId.EXTENSION, + mockFetchFn, + ); + expect( + bridgeController.state.bridgeState.quotesLastFetched, + ).toBeUndefined(); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(1500); + await flushPromises(); + const { quotes } = bridgeController.state.bridgeState; + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + }), + ); + quotes.forEach((quote) => { + const expectedQuote = { ...quote, l1GasFeesInHexWei }; + // eslint-disable-next-line jest/prefer-strict-equal + expect(quote).toEqual(expectedQuote); + }); + + const firstFetchTime = + bridgeController.state.bridgeState.quotesLastFetched; + expect(firstFetchTime).toBeGreaterThan(0); + + expect(getLayer1GasFeeMock).toHaveBeenCalledTimes( + getLayer1GasFeeMockCallCount, + ); + }, + ); + + it('should not fetch quotes if source and destination chains are the same', async () => { + jest.useFakeTimers(); + const fetchBridgeQuotesSpy = jest.spyOn(fetchUtils, 'fetchBridgeQuotes'); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(true); + + const quoteParams = { + srcChainId: 1, + destChainId: 1, // Same chain ID + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + // Advance timers to trigger fetch + jest.advanceTimersByTime(1000); + await flushPromises(); + + expect(fetchBridgeQuotesSpy).not.toHaveBeenCalled(); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(bridgeController.state.bridgeState.quotesLoadingStatus).toBe( + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + ); + }); + + it('should handle abort signals in fetchBridgeQuotes', async () => { + jest.useFakeTimers(); + const fetchBridgeQuotesSpy = jest.spyOn(fetchUtils, 'fetchBridgeQuotes'); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + jest.spyOn(balanceUtils, 'hasSufficientBalance').mockResolvedValue(true); + + // Mock fetchBridgeQuotes to throw AbortError + fetchBridgeQuotesSpy.mockImplementation(async () => { + const error = new Error('Aborted'); + error.name = 'AbortError'; + throw error; + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + // Advance timers to trigger fetch + jest.advanceTimersByTime(1000); + await flushPromises(); + + // Verify state wasn't updated due to abort + expect(bridgeController.state.bridgeState.quoteFetchError).toBeUndefined(); + expect(bridgeController.state.bridgeState.quotesLoadingStatus).toBe(0); + expect(bridgeController.state.bridgeState.quotes).toStrictEqual([]); + + // Test reset abort + fetchBridgeQuotesSpy.mockRejectedValueOnce('Reset controller state'); + + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + jest.advanceTimersByTime(1000); + await flushPromises(); + + // Verify state wasn't updated due to reset + expect(bridgeController.state.bridgeState.quoteFetchError).toBeUndefined(); + expect(bridgeController.state.bridgeState.quotesLoadingStatus).toBe(0); + expect(bridgeController.state.bridgeState.quotes).toStrictEqual([]); + }); +}); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts new file mode 100644 index 00000000000..7788d2b7561 --- /dev/null +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -0,0 +1,375 @@ +import type { StateMetadata } from '@metamask/base-controller'; +import type { ChainId } from '@metamask/controller-utils'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import type { NetworkClientId } from '@metamask/network-controller'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import type { TransactionParams } from '@metamask/transaction-controller'; +import { numberToHex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; +import { BrowserProvider, Contract } from 'ethers'; + +import type { BridgeClientId } from './constants/bridge'; +import { REFRESH_INTERVAL_MS } from './constants/bridge'; +import { + BRIDGE_CONTROLLER_NAME, + DEFAULT_BRIDGE_CONTROLLER_STATE, + METABRIDGE_CHAIN_TO_ADDRESS_MAP, +} from './constants/bridge'; +import { CHAIN_IDS } from './constants/chains'; +import { + type L1GasFees, + type QuoteRequest, + type QuoteResponse, + type TxData, + type BridgeControllerState, + BridgeFeatureFlagsKey, + RequestStatus, +} from './types'; +import type { BridgeControllerMessenger, FetchFunction } from './types'; +import { hasSufficientBalance } from './utils/balance'; +import { getDefaultBridgeControllerState, sumHexes } from './utils/bridge'; +import { fetchBridgeFeatureFlags, fetchBridgeQuotes } from './utils/fetch'; +import { isValidQuoteRequest } from './utils/quote'; + +const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { + bridgeState: { + persist: false, + anonymous: false, + }, +}; + +const RESET_STATE_ABORT_MESSAGE = 'Reset controller state'; + +/** The input to start polling for the {@link BridgeController} */ +type BridgePollingInput = { + networkClientId: NetworkClientId; + updatedQuoteRequest: QuoteRequest; +}; + +export class BridgeController extends StaticIntervalPollingController()< + typeof BRIDGE_CONTROLLER_NAME, + { bridgeState: BridgeControllerState }, + BridgeControllerMessenger +> { + #abortController: AbortController | undefined; + + #quotesFirstFetched: number | undefined; + + readonly #clientId: string; + + readonly #getLayer1GasFee: (params: { + transactionParams: TransactionParams; + chainId: ChainId; + }) => Promise; + + readonly #fetchFn: FetchFunction; + + constructor({ + messenger, + state, + clientId, + getLayer1GasFee, + fetchFn, + }: { + messenger: BridgeControllerMessenger; + state?: Partial; + clientId: BridgeClientId; + getLayer1GasFee: (params: { + transactionParams: TransactionParams; + chainId: ChainId; + }) => Promise; + fetchFn: FetchFunction; + }) { + super({ + name: BRIDGE_CONTROLLER_NAME, + metadata, + messenger, + state: { + bridgeState: { + ...getDefaultBridgeControllerState(), + ...state, + }, + }, + }); + + this.setIntervalLength(REFRESH_INTERVAL_MS); + + this.#abortController = new AbortController(); + this.#getLayer1GasFee = getLayer1GasFee; + this.#clientId = clientId; + this.#fetchFn = fetchFn; + + // Register action handlers + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, + this.setBridgeFeatureFlags.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:updateBridgeQuoteRequestParams`, + this.updateBridgeQuoteRequestParams.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:resetState`, + this.resetState.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:getBridgeERC20Allowance`, + this.getBridgeERC20Allowance.bind(this), + ); + } + + _executePoll = async (pollingInput: BridgePollingInput) => { + await this.#fetchBridgeQuotes(pollingInput); + }; + + updateBridgeQuoteRequestParams = async ( + paramsToUpdate: Partial, + ) => { + this.stopAllPolling(); + this.#abortController?.abort('Quote request updated'); + + const updatedQuoteRequest = { + ...DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest, + ...paramsToUpdate, + }; + + this.update((state) => { + state.bridgeState.quoteRequest = updatedQuoteRequest; + state.bridgeState.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes; + state.bridgeState.quotesLastFetched = + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched; + state.bridgeState.quotesLoadingStatus = + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus; + state.bridgeState.quoteFetchError = + DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError; + state.bridgeState.quotesRefreshCount = + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount; + state.bridgeState.quotesInitialLoadTime = + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesInitialLoadTime; + }); + + if (isValidQuoteRequest(updatedQuoteRequest)) { + this.#quotesFirstFetched = Date.now(); + const walletAddress = this.#getSelectedAccount().address; + const srcChainIdInHex = numberToHex(updatedQuoteRequest.srcChainId); + + const insufficientBal = + paramsToUpdate.insufficientBal || + !(await this.#hasSufficientBalance(updatedQuoteRequest)); + + const networkClientId = this.#getSelectedNetworkClientId(srcChainIdInHex); + this.startPolling({ + networkClientId, + updatedQuoteRequest: { + ...updatedQuoteRequest, + walletAddress, + insufficientBal, + }, + }); + } + }; + + readonly #hasSufficientBalance = async (quoteRequest: QuoteRequest) => { + const walletAddress = this.#getSelectedAccount().address; + const srcChainIdInHex = numberToHex(quoteRequest.srcChainId); + const provider = this.#getSelectedNetworkClient()?.provider; + + return ( + provider && + (await hasSufficientBalance( + provider, + walletAddress, + quoteRequest.srcTokenAddress, + quoteRequest.srcTokenAmount, + srcChainIdInHex, + )) + ); + }; + + resetState = () => { + this.stopAllPolling(); + this.#abortController?.abort(RESET_STATE_ABORT_MESSAGE); + + this.update((state) => { + state.bridgeState = { + ...DEFAULT_BRIDGE_CONTROLLER_STATE, + quotes: [], + bridgeFeatureFlags: state.bridgeState.bridgeFeatureFlags, + }; + }); + }; + + setBridgeFeatureFlags = async () => { + const bridgeFeatureFlags = await fetchBridgeFeatureFlags( + this.#clientId, + this.#fetchFn, + ); + this.update((state) => { + state.bridgeState.bridgeFeatureFlags = bridgeFeatureFlags; + }); + this.setIntervalLength( + bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].refreshRate, + ); + }; + + readonly #fetchBridgeQuotes = async ({ + networkClientId: _networkClientId, + updatedQuoteRequest, + }: BridgePollingInput) => { + const { bridgeState } = this.state; + this.#abortController?.abort('New quote request'); + this.#abortController = new AbortController(); + if (updatedQuoteRequest.srcChainId === updatedQuoteRequest.destChainId) { + return; + } + this.update((state) => { + state.bridgeState.quotesLoadingStatus = RequestStatus.LOADING; + state.bridgeState.quoteRequest = updatedQuoteRequest; + state.bridgeState.quoteFetchError = + DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError; + }); + + try { + const quotes = await fetchBridgeQuotes( + updatedQuoteRequest, + // AbortController is always defined by this line, because we assign it a few lines above, + // not sure why Jest thinks it's not + // Linters accurately say that it's defined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.#abortController!.signal as AbortSignal, + this.#clientId, + this.#fetchFn, + ); + + const quotesWithL1GasFees = await this.#appendL1GasFees(quotes); + + this.update((state) => { + state.bridgeState.quotes = quotesWithL1GasFees; + state.bridgeState.quotesLoadingStatus = RequestStatus.FETCHED; + }); + } catch (error) { + const isAbortError = (error as Error).name === 'AbortError'; + const isAbortedDueToReset = error === RESET_STATE_ABORT_MESSAGE; + if (isAbortedDueToReset || isAbortError) { + return; + } + + this.update((state) => { + state.bridgeState.quoteFetchError = + error instanceof Error ? error.message : 'Unknown error'; + state.bridgeState.quotesLoadingStatus = RequestStatus.ERROR; + }); + console.log('Failed to fetch bridge quotes', error); + } finally { + const { maxRefreshCount } = + bridgeState.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG]; + + const updatedQuotesRefreshCount = bridgeState.quotesRefreshCount + 1; + // Stop polling if the maximum number of refreshes has been reached + if ( + updatedQuoteRequest.insufficientBal || + (!updatedQuoteRequest.insufficientBal && + updatedQuotesRefreshCount >= maxRefreshCount) + ) { + this.stopAllPolling(); + } + + // Update quote fetching stats + const quotesLastFetched = Date.now(); + this.update((state) => { + state.bridgeState.quotesInitialLoadTime = + updatedQuotesRefreshCount === 1 && this.#quotesFirstFetched + ? quotesLastFetched - this.#quotesFirstFetched + : bridgeState.quotesInitialLoadTime; + state.bridgeState.quotesLastFetched = quotesLastFetched; + state.bridgeState.quotesRefreshCount = updatedQuotesRefreshCount; + }); + } + }; + + readonly #appendL1GasFees = async ( + quotes: QuoteResponse[], + ): Promise<(QuoteResponse & L1GasFees)[]> => { + return await Promise.all( + quotes.map(async (quoteResponse) => { + const { quote, trade, approval } = quoteResponse; + const chainId = numberToHex(quote.srcChainId) as ChainId; + if ( + [CHAIN_IDS.OPTIMISM.toString(), CHAIN_IDS.BASE.toString()].includes( + chainId, + ) + ) { + const getTxParams = (txData: TxData) => ({ + from: txData.from, + to: txData.to, + value: txData.value, + data: txData.data, + gasLimit: txData.gasLimit?.toString(), + }); + const approvalL1GasFees = approval + ? await this.#getLayer1GasFee({ + transactionParams: getTxParams(approval), + chainId, + }) + : '0'; + const tradeL1GasFees = await this.#getLayer1GasFee({ + transactionParams: getTxParams(trade), + chainId, + }); + return { + ...quoteResponse, + l1GasFeesInHexWei: sumHexes(approvalL1GasFees, tradeL1GasFees), + }; + } + return quoteResponse; + }), + ); + }; + + #getSelectedAccount() { + return this.messagingSystem.call('AccountsController:getSelectedAccount'); + } + + #getSelectedNetworkClient() { + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const networkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + return networkClient; + } + + #getSelectedNetworkClientId(chainId: Hex) { + return this.messagingSystem.call( + 'NetworkController:findNetworkClientIdByChainId', + chainId, + ); + } + + /** + * + * @param contractAddress - The address of the ERC20 token contract + * @param chainId - The hex chain ID of the bridge network + * @returns The atomic allowance of the ERC20 token contract + */ + getBridgeERC20Allowance = async ( + contractAddress: string, + chainId: Hex, + ): Promise => { + const provider = this.#getSelectedNetworkClient()?.provider; + if (!provider) { + throw new Error('No provider found'); + } + + const ethersProvider = new BrowserProvider(provider); + const contract = new Contract(contractAddress, abiERC20, ethersProvider); + const { address: walletAddress } = this.#getSelectedAccount(); + const allowance: bigint = await contract.allowance( + walletAddress, + METABRIDGE_CHAIN_TO_ADDRESS_MAP[chainId], + ); + return allowance.toString(); + }; +} diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts new file mode 100644 index 00000000000..2fc2500b19c --- /dev/null +++ b/packages/bridge-controller/src/constants/bridge.ts @@ -0,0 +1,68 @@ +import type { Hex } from '@metamask/utils'; +import { ZeroAddress } from 'ethers'; + +import { CHAIN_IDS } from './chains'; +import type { BridgeControllerState } from '../types'; +import { BridgeFeatureFlagsKey } from '../types'; + +// TODO read from feature flags +export const ALLOWED_BRIDGE_CHAIN_IDS = [ + CHAIN_IDS.MAINNET, + CHAIN_IDS.BSC, + CHAIN_IDS.POLYGON, + CHAIN_IDS.ZKSYNC_ERA, + CHAIN_IDS.AVALANCHE, + CHAIN_IDS.OPTIMISM, + CHAIN_IDS.ARBITRUM, + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.BASE, +]; + +export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; + +export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; +export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; + +export enum BridgeClientId { + EXTENSION = 'extension', + MOBILE = 'mobile', +} + +export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; +export const METABRIDGE_ETHEREUM_ADDRESS = + '0x0439e60F02a8900a951603950d8D4527f400C3f1'; +export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour +export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.5; // if a quote returns in x times less return than the best quote, ignore it + +export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high'; +export const BRIDGE_DEFAULT_SLIPPAGE = 0.5; +export const BRIDGE_MM_FEE_RATE = 0.875; +export const REFRESH_INTERVAL_MS = 30 * 1000; +export const DEFAULT_MAX_REFRESH_COUNT = 5; + +export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; +export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { + bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: REFRESH_INTERVAL_MS, + maxRefreshCount: DEFAULT_MAX_REFRESH_COUNT, + support: false, + chains: {}, + }, + }, + quoteRequest: { + walletAddress: undefined, + srcTokenAddress: ZeroAddress, + slippage: BRIDGE_DEFAULT_SLIPPAGE, + }, + quotesInitialLoadTime: undefined, + quotes: [], + quotesLastFetched: undefined, + quotesLoadingStatus: undefined, + quoteFetchError: undefined, + quotesRefreshCount: 0, +}; + +export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { + [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, +}; diff --git a/packages/bridge-controller/src/constants/chains.ts b/packages/bridge-controller/src/constants/chains.ts new file mode 100644 index 00000000000..abf24411276 --- /dev/null +++ b/packages/bridge-controller/src/constants/chains.ts @@ -0,0 +1,173 @@ +import type { AllowedBridgeChainIds } from './bridge'; + +/** + * An object containing all of the chain ids for networks both built in and + * those that we have added custom code to support our feature set. + */ +export const CHAIN_IDS = { + MAINNET: '0x1', + GOERLI: '0x5', + LOCALHOST: '0x539', + BSC: '0x38', + BSC_TESTNET: '0x61', + OPTIMISM: '0xa', + OPTIMISM_TESTNET: '0xaa37dc', + OPTIMISM_GOERLI: '0x1a4', + BASE: '0x2105', + BASE_TESTNET: '0x14a33', + OPBNB: '0xcc', + OPBNB_TESTNET: '0x15eb', + POLYGON: '0x89', + POLYGON_TESTNET: '0x13881', + AVALANCHE: '0xa86a', + AVALANCHE_TESTNET: '0xa869', + FANTOM: '0xfa', + FANTOM_TESTNET: '0xfa2', + CELO: '0xa4ec', + ARBITRUM: '0xa4b1', + HARMONY: '0x63564c40', + PALM: '0x2a15c308d', + SEPOLIA: '0xaa36a7', + HOLESKY: '0x4268', + LINEA_GOERLI: '0xe704', + LINEA_SEPOLIA: '0xe705', + AMOY: '0x13882', + BASE_SEPOLIA: '0x14a34', + BLAST_SEPOLIA: '0xa0c71fd', + OPTIMISM_SEPOLIA: '0xaa37dc', + PALM_TESTNET: '0x2a15c3083', + CELO_TESTNET: '0xaef3', + ZK_SYNC_ERA_TESTNET: '0x12c', + MANTA_SEPOLIA: '0x138b', + UNICHAIN_SEPOLIA: '0x515', + LINEA_MAINNET: '0xe708', + AURORA: '0x4e454152', + MOONBEAM: '0x504', + MOONBEAM_TESTNET: '0x507', + MOONRIVER: '0x505', + CRONOS: '0x19', + GNOSIS: '0x64', + ZKSYNC_ERA: '0x144', + TEST_ETH: '0x539', + ARBITRUM_GOERLI: '0x66eed', + BLAST: '0x13e31', + FILECOIN: '0x13a', + POLYGON_ZKEVM: '0x44d', + SCROLL: '0x82750', + SCROLL_SEPOLIA: '0x8274f', + WETHIO: '0x4e', + CHZ: '0x15b38', + NUMBERS: '0x290b', + SEI: '0x531', + APE_TESTNET: '0x8157', + APE_MAINNET: '0x8173', + BERACHAIN: '0x138d5', + METACHAIN_ONE: '0x1b6e6', + ARBITRUM_SEPOLIA: '0x66eee', + NEAR: '0x18d', + NEAR_TESTNET: '0x18e', + B3: '0x208d', + B3_TESTNET: '0x7c9', + GRAVITY_ALPHA_MAINNET: '0x659', + GRAVITY_ALPHA_TESTNET_SEPOLIA: '0x34c1', + LISK: '0x46f', + LISK_SEPOLIA: '0x106a', + INK_SEPOLIA: '0xba5eD', + INK: '0xdef1', + MODE_SEPOLIA: '0x397', + MODE: '0x868b', +} as const; + +export const NETWORK_TYPES = { + GOERLI: 'goerli', + LOCALHOST: 'localhost', + MAINNET: 'mainnet', + SEPOLIA: 'sepolia', + LINEA_GOERLI: 'linea-goerli', + LINEA_SEPOLIA: 'linea-sepolia', + LINEA_MAINNET: 'linea-mainnet', +} as const; + +export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'; +export const GOERLI_DISPLAY_NAME = 'Goerli'; +export const SEPOLIA_DISPLAY_NAME = 'Sepolia'; +export const LINEA_GOERLI_DISPLAY_NAME = 'Linea Goerli'; +export const LINEA_SEPOLIA_DISPLAY_NAME = 'Linea Sepolia'; +export const LINEA_MAINNET_DISPLAY_NAME = 'Linea Mainnet'; +export const LOCALHOST_DISPLAY_NAME = 'Localhost 8545'; +export const BSC_DISPLAY_NAME = 'Binance Smart Chain'; +export const POLYGON_DISPLAY_NAME = 'Polygon'; +export const AVALANCHE_DISPLAY_NAME = 'Avalanche Network C-Chain'; +export const ARBITRUM_DISPLAY_NAME = 'Arbitrum One'; +export const BNB_DISPLAY_NAME = 'BNB Chain'; +export const OPTIMISM_DISPLAY_NAME = 'OP Mainnet'; +export const FANTOM_DISPLAY_NAME = 'Fantom Opera'; +export const HARMONY_DISPLAY_NAME = 'Harmony Mainnet Shard 0'; +export const PALM_DISPLAY_NAME = 'Palm'; +export const CELO_DISPLAY_NAME = 'Celo Mainnet'; +export const GNOSIS_DISPLAY_NAME = 'Gnosis'; +export const ZK_SYNC_ERA_DISPLAY_NAME = 'zkSync Era Mainnet'; +export const BASE_DISPLAY_NAME = 'Base Mainnet'; +export const AURORA_DISPLAY_NAME = 'Aurora Mainnet'; +export const CRONOS_DISPLAY_NAME = 'Cronos'; +export const POLYGON_ZKEVM_DISPLAY_NAME = 'Polygon zkEVM'; +export const MOONBEAM_DISPLAY_NAME = 'Moonbeam'; +export const MOONRIVER_DISPLAY_NAME = 'Moonriver'; +export const SCROLL_DISPLAY_NAME = 'Scroll'; +export const SCROLL_SEPOLIA_DISPLAY_NAME = 'Scroll Sepolia'; +export const OP_BNB_DISPLAY_NAME = 'opBNB'; +export const BERACHAIN_DISPLAY_NAME = 'Berachain Artio'; +export const METACHAIN_ONE_DISPLAY_NAME = 'Metachain One Mainnet'; +export const LISK_DISPLAY_NAME = 'Lisk'; +export const LISK_SEPOLIA_DISPLAY_NAME = 'Lisk Sepolia'; +export const INK_SEPOLIA_DISPLAY_NAME = 'Ink Sepolia'; +export const INK_DISPLAY_NAME = 'Ink Mainnet'; +export const SONEIUM_DISPLAY_NAME = 'Soneium Mainnet'; +export const MODE_SEPOLIA_DISPLAY_NAME = 'Mode Sepolia'; +export const MODE_DISPLAY_NAME = 'Mode Mainnet'; + +export const NETWORK_TO_NAME_MAP = { + [NETWORK_TYPES.GOERLI]: GOERLI_DISPLAY_NAME, + [NETWORK_TYPES.MAINNET]: MAINNET_DISPLAY_NAME, + [NETWORK_TYPES.LINEA_GOERLI]: LINEA_GOERLI_DISPLAY_NAME, + [NETWORK_TYPES.LINEA_SEPOLIA]: LINEA_SEPOLIA_DISPLAY_NAME, + [NETWORK_TYPES.LINEA_MAINNET]: LINEA_MAINNET_DISPLAY_NAME, + [NETWORK_TYPES.LOCALHOST]: LOCALHOST_DISPLAY_NAME, + [NETWORK_TYPES.SEPOLIA]: SEPOLIA_DISPLAY_NAME, + + [CHAIN_IDS.ARBITRUM]: ARBITRUM_DISPLAY_NAME, + [CHAIN_IDS.AVALANCHE]: AVALANCHE_DISPLAY_NAME, + [CHAIN_IDS.BSC]: BSC_DISPLAY_NAME, + [CHAIN_IDS.BASE]: BASE_DISPLAY_NAME, + [CHAIN_IDS.GOERLI]: GOERLI_DISPLAY_NAME, + [CHAIN_IDS.MAINNET]: MAINNET_DISPLAY_NAME, + [CHAIN_IDS.LINEA_GOERLI]: LINEA_GOERLI_DISPLAY_NAME, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_MAINNET_DISPLAY_NAME, + [CHAIN_IDS.LINEA_SEPOLIA]: LINEA_SEPOLIA_DISPLAY_NAME, + [CHAIN_IDS.LOCALHOST]: LOCALHOST_DISPLAY_NAME, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_DISPLAY_NAME, + [CHAIN_IDS.POLYGON]: POLYGON_DISPLAY_NAME, + [CHAIN_IDS.SCROLL]: SCROLL_DISPLAY_NAME, + [CHAIN_IDS.SCROLL_SEPOLIA]: SCROLL_SEPOLIA_DISPLAY_NAME, + [CHAIN_IDS.SEPOLIA]: SEPOLIA_DISPLAY_NAME, + [CHAIN_IDS.OPBNB]: OP_BNB_DISPLAY_NAME, + [CHAIN_IDS.ZKSYNC_ERA]: ZK_SYNC_ERA_DISPLAY_NAME, + [CHAIN_IDS.BERACHAIN]: BERACHAIN_DISPLAY_NAME, + [CHAIN_IDS.METACHAIN_ONE]: METACHAIN_ONE_DISPLAY_NAME, + [CHAIN_IDS.LISK]: LISK_DISPLAY_NAME, + [CHAIN_IDS.LISK_SEPOLIA]: LISK_SEPOLIA_DISPLAY_NAME, +} as const; +export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< + AllowedBridgeChainIds, + string +> = { + [CHAIN_IDS.MAINNET]: 'Ethereum', + [CHAIN_IDS.LINEA_MAINNET]: 'Linea', + [CHAIN_IDS.POLYGON]: NETWORK_TO_NAME_MAP[CHAIN_IDS.POLYGON], + [CHAIN_IDS.AVALANCHE]: 'Avalanche', + [CHAIN_IDS.BSC]: NETWORK_TO_NAME_MAP[CHAIN_IDS.BSC], + [CHAIN_IDS.ARBITRUM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.ARBITRUM], + [CHAIN_IDS.OPTIMISM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], + [CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era', + [CHAIN_IDS.BASE]: 'Base', +}; diff --git a/packages/bridge-controller/src/constants/swaps.ts b/packages/bridge-controller/src/constants/swaps.ts new file mode 100644 index 00000000000..f226425bd17 --- /dev/null +++ b/packages/bridge-controller/src/constants/swaps.ts @@ -0,0 +1 @@ +export const SWAPS_API_V2_BASE_URL = 'https://swap.api.cx.metamask.io'; diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts new file mode 100644 index 00000000000..be67ca8ccd8 --- /dev/null +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -0,0 +1,144 @@ +import { CHAIN_IDS } from './chains'; + +export type SwapsTokenObject = { + /** + * The symbol of token object + */ + symbol: string; + /** + * The name for the network + */ + name: string; + /** + * An address that the metaswap-api recognizes as the default token + */ + address: string; + /** + * Number of digits after decimal point + */ + decimals: number; + /** + * URL for token icon + */ + iconUrl: string; +}; + +const DEFAULT_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; + +export const CURRENCY_SYMBOLS = { + ARBITRUM: 'ETH', + AVALANCHE: 'AVAX', + BNB: 'BNB', + BUSD: 'BUSD', + CELO: 'CELO', + DAI: 'DAI', + GNOSIS: 'XDAI', + ETH: 'ETH', + FANTOM: 'FTM', + HARMONY: 'ONE', + PALM: 'PALM', + MATIC: 'MATIC', + POL: 'POL', + TEST_ETH: 'TESTETH', + USDC: 'USDC', + USDT: 'USDT', + WETH: 'WETH', + OPTIMISM: 'ETH', + CRONOS: 'CRO', + GLIMMER: 'GLMR', + MOONRIVER: 'MOVR', + ONE: 'ONE', +} as const; + +export const ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.ETH, + name: 'Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +}; + +export const BNB_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.BNB, + name: 'Binance Coin', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const MATIC_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.POL, + name: 'Polygon', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const AVAX_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.AVALANCHE, + name: 'Avalanche', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const TEST_ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.TEST_ETH, + name: 'Test Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const GOERLI_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.ETH, + name: 'Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const SEPOLIA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.ETH, + name: 'Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const ARBITRUM_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +export const OPTIMISM_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +export const ZKSYNC_ERA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +export const LINEA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +export const BASE_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +const SWAPS_TESTNET_CHAIN_ID = '0x539'; + +export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { + [CHAIN_IDS.MAINNET]: ETH_SWAPS_TOKEN_OBJECT, + [SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.BSC]: BNB_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.POLYGON]: MATIC_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.GOERLI]: GOERLI_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.SEPOLIA]: GOERLI_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.AVALANCHE]: AVAX_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, +} as const; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts new file mode 100644 index 00000000000..415682821fe --- /dev/null +++ b/packages/bridge-controller/src/index.ts @@ -0,0 +1,61 @@ +export { BridgeController } from './bridge-controller'; + +export type { + AssetType, + ChainConfiguration, + L1GasFees, + QuoteMetadata, + SortOrder, + BridgeToken, + BridgeFlag, + GasMultiplierByChainId, + FeatureFlagResponse, + BridgeAsset, + QuoteRequest, + Protocol, + ActionTypes, + Step, + RefuelData, + Quote, + QuoteResponse, + ChainId, + FeeType, + FeeData, + TxData, + BridgeFeatureFlagsKey, + BridgeFeatureFlags, + RequestStatus, + BridgeUserAction, + BridgeBackgroundAction, + BridgeControllerState, + BridgeControllerAction, + BridgeControllerActions, + BridgeControllerEvents, + BridgeControllerMessenger, +} from './types'; + +export { + ALLOWED_BRIDGE_CHAIN_IDS, + BridgeClientId, + BRIDGE_QUOTE_MAX_ETA_SECONDS, + BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, + BRIDGE_PREFERRED_GAS_ESTIMATE, + BRIDGE_DEFAULT_SLIPPAGE, + BRIDGE_MM_FEE_RATE, + REFRESH_INTERVAL_MS, + DEFAULT_MAX_REFRESH_COUNT, + DEFAULT_BRIDGE_CONTROLLER_STATE, + METABRIDGE_CHAIN_TO_ADDRESS_MAP, +} from './constants/bridge'; + +export type { AllowedBridgeChainIds } from './constants/bridge'; + +export type { SwapsTokenObject } from './constants/tokens'; + +export { SWAPS_API_V2_BASE_URL } from './constants/swaps'; + +export { + getEthUsdtResetData, + isEthUsdt, + getBridgeApiBaseUrl, +} from './utils/bridge'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts new file mode 100644 index 00000000000..f79031316b2 --- /dev/null +++ b/packages/bridge-controller/src/types.ts @@ -0,0 +1,274 @@ +import type { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; +import type { + ControllerStateChangeEvent, + RestrictedMessenger, +} from '@metamask/base-controller'; +import type { + NetworkControllerFindNetworkClientIdByChainIdAction, + NetworkControllerGetStateAction, + NetworkControllerGetNetworkClientByIdAction, +} from '@metamask/network-controller'; +import type { Hex } from '@metamask/utils'; +import type { BigNumber } from 'bignumber.js'; + +import type { BridgeController } from './bridge-controller'; +import type { BRIDGE_CONTROLLER_NAME } from './constants/bridge'; + +export type FetchFunction = ( + input: RequestInfo | URL, + init?: RequestInit, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +) => Promise; + +/** + * The types of assets that a user can send + * + */ +export enum AssetType { + /** The native asset for the current network, such as ETH */ + native = 'NATIVE', + /** An ERC20 token */ + token = 'TOKEN', + /** An ERC721 or ERC1155 token. */ + NFT = 'NFT', + /** + * A transaction interacting with a contract that isn't a token method + * interaction will be marked as dealing with an unknown asset type. + */ + unknown = 'UNKNOWN', +} + +export type ChainConfiguration = { + isActiveSrc: boolean; + isActiveDest: boolean; +}; + +export type L1GasFees = { + l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by controller +}; +// Values derived from the quote response +// valueInCurrency values are calculated based on the user's selected currency + +export type QuoteMetadata = { + gasFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; + totalNetworkFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; // estimatedGasFees + relayerFees + totalMaxNetworkFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; // maxGasFees + relayerFees + toTokenAmount: { amount: BigNumber; valueInCurrency: BigNumber | null }; + adjustedReturn: { valueInCurrency: BigNumber | null }; // destTokenAmount - totalNetworkFee + sentAmount: { amount: BigNumber; valueInCurrency: BigNumber | null }; // srcTokenAmount + metabridgeFee + swapRate: BigNumber; // destTokenAmount / sentAmount + cost: { valueInCurrency: BigNumber | null }; // sentAmount - adjustedReturn +}; +// Sort order set by the user + +export enum SortOrder { + COST_ASC = 'cost_ascending', + ETA_ASC = 'time_descending', +} + +export type BridgeToken = { + type: AssetType.native | AssetType.token; + address: string; + symbol: string; + image: string; + decimals: number; + chainId: Hex; + balance: string; // raw balance + string: string | undefined; // normalized balance as a stringified number + tokenFiatAmount?: number | null; +} | null; +// Types copied from Metabridge API + +export enum BridgeFlag { + EXTENSION_CONFIG = 'extension-config', +} +type DecimalChainId = string; +export type GasMultiplierByChainId = Record; + +export type FeatureFlagResponse = { + [BridgeFlag.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + support: boolean; + chains: Record; + }; +}; + +export type BridgeAsset = { + chainId: ChainId; + address: string; + symbol: string; + name: string; + decimals: number; + icon?: string; +}; + +export type QuoteRequest = { + walletAddress: string; + destWalletAddress?: string; + srcChainId: ChainId; + destChainId: ChainId; + srcTokenAddress: string; + destTokenAddress: string; + /** + * This is the amount sent, in atomic amount + */ + srcTokenAmount: string; + slippage: number; + aggIds?: string[]; + bridgeIds?: string[]; + insufficientBal?: boolean; + resetApproval?: boolean; + refuel?: boolean; +}; + +export type Protocol = { + name: string; + displayName?: string; + icon?: string; +}; + +export enum ActionTypes { + BRIDGE = 'bridge', + SWAP = 'swap', + REFUEL = 'refuel', +} + +export type Step = { + action: ActionTypes; + srcChainId: ChainId; + destChainId?: ChainId; + srcAsset: BridgeAsset; + destAsset: BridgeAsset; + srcAmount: string; + destAmount: string; + protocol: Protocol; +}; + +export type RefuelData = Step; + +export type Quote = { + requestId: string; + srcChainId: ChainId; + srcAsset: BridgeAsset; + // Some tokens have a fee of 0, so sometimes it's equal to amount sent + srcTokenAmount: string; // Atomic amount, the amount sent - fees + destChainId: ChainId; + destAsset: BridgeAsset; + destTokenAmount: string; // Atomic amount, the amount received + feeData: Record & + Partial>; + bridgeId: string; + bridges: string[]; + steps: Step[]; + refuel?: RefuelData; +}; + +export type QuoteResponse = { + quote: Quote; + approval: TxData | null; + trade: TxData; + estimatedProcessingTimeInSeconds: number; +}; + +export enum ChainId { + ETH = 1, + OPTIMISM = 10, + BSC = 56, + POLYGON = 137, + ZKSYNC = 324, + BASE = 8453, + ARBITRUM = 42161, + AVALANCHE = 43114, + LINEA = 59144, +} + +export enum FeeType { + METABRIDGE = 'metabridge', + REFUEL = 'refuel', +} +export type FeeData = { + amount: string; + asset: BridgeAsset; +}; +export type TxData = { + chainId: ChainId; + to: string; + from: string; + value: string; + data: string; + gasLimit: number | null; +}; +export enum BridgeFeatureFlagsKey { + EXTENSION_CONFIG = 'extensionConfig', +} + +export type BridgeFeatureFlags = { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + support: boolean; + chains: Record; + }; +}; +export enum RequestStatus { + LOADING, + FETCHED, + ERROR, +} +export enum BridgeUserAction { + SELECT_DEST_NETWORK = 'selectDestNetwork', + UPDATE_QUOTE_PARAMS = 'updateBridgeQuoteRequestParams', +} +export enum BridgeBackgroundAction { + SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', + RESET_STATE = 'resetState', + GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', +} +export type BridgeControllerState = { + bridgeFeatureFlags: BridgeFeatureFlags; + quoteRequest: Partial; + quotes: (QuoteResponse & L1GasFees)[]; + quotesInitialLoadTime?: number; + quotesLastFetched?: number; + quotesLoadingStatus?: RequestStatus; + quoteFetchError?: string; + quotesRefreshCount: number; +}; + +export type BridgeControllerAction< + FunctionName extends keyof BridgeController, +> = { + type: `${typeof BRIDGE_CONTROLLER_NAME}:${FunctionName}`; + handler: BridgeController[FunctionName]; +}; + +// Maps to BridgeController function names +export type BridgeControllerActions = + | BridgeControllerAction + | BridgeControllerAction + | BridgeControllerAction + | BridgeControllerAction; + +export type BridgeControllerEvents = ControllerStateChangeEvent< + typeof BRIDGE_CONTROLLER_NAME, + BridgeControllerState +>; + +export type AllowedActions = + | AccountsControllerGetSelectedAccountAction + | NetworkControllerFindNetworkClientIdByChainIdAction + | NetworkControllerGetStateAction + | NetworkControllerGetNetworkClientByIdAction; +export type AllowedEvents = never; + +/** + * The messenger for the BridgeController. + */ +export type BridgeControllerMessenger = RestrictedMessenger< + typeof BRIDGE_CONTROLLER_NAME, + BridgeControllerActions | AllowedActions, + BridgeControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; diff --git a/packages/bridge-controller/src/utils/balance.test.ts b/packages/bridge-controller/src/utils/balance.test.ts new file mode 100644 index 00000000000..a8f4d6569f9 --- /dev/null +++ b/packages/bridge-controller/src/utils/balance.test.ts @@ -0,0 +1,249 @@ +import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import { ZeroAddress } from 'ethers'; +import { BrowserProvider, Contract } from 'ethers'; + +import * as balanceUtils from './balance'; +import { fetchTokenBalance } from './balance'; +import { FakeProvider } from '../../../../tests/fake-provider'; + +declare global { + // eslint-disable-next-line no-var + var ethereumProvider: SafeEventEmitterProvider; +} + +jest.mock('ethers', () => { + return { + ...jest.requireActual('ethers'), + Contract: jest.fn(), + BrowserProvider: jest.fn(), + }; +}); + +describe('balance', () => { + beforeEach(() => { + jest.clearAllMocks(); + global.ethereumProvider = new FakeProvider(); + }); + + describe('calcLatestSrcBalance', () => { + it('should return the ERC20 token balance', async () => { + const mockBalanceOf = jest.fn().mockResolvedValueOnce(BigInt(100)); + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + balanceOf: mockBalanceOf, + })); + + expect( + await balanceUtils.calcLatestSrcBalance( + global.ethereumProvider, + '0x123', + '0x456', + '0x789', + ), + ).toStrictEqual(BigInt(100)); + expect(mockBalanceOf).toHaveBeenCalledTimes(1); + expect(mockBalanceOf).toHaveBeenCalledWith('0x123'); + }); + + it('should return the native asset balance', async () => { + const mockGetBalance = jest.fn().mockImplementation(() => { + return BigInt(100); + }); + (BrowserProvider as unknown as jest.Mock).mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }); + + expect( + await balanceUtils.calcLatestSrcBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', + ZeroAddress, + '0x789', + ), + ).toStrictEqual(BigInt(100)); + expect(mockGetBalance).toHaveBeenCalledTimes(1); + expect(mockGetBalance).toHaveBeenCalledWith( + '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', + ); + }); + + it('should return undefined if token address and chainId are undefined', async () => { + const mockGetBalance = jest.fn(); + (BrowserProvider as unknown as jest.Mock).mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }); + + const mockFetchTokenBalance = jest.spyOn( + balanceUtils, + 'fetchTokenBalance', + ); + expect( + await balanceUtils.calcLatestSrcBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', + undefined as never, + undefined as never, + ), + ).toBeUndefined(); + expect(mockFetchTokenBalance).not.toHaveBeenCalled(); + expect(mockGetBalance).not.toHaveBeenCalled(); + }); + }); + + describe('hasSufficientBalance', () => { + it('should return true if user has sufficient balance', async () => { + const mockGetBalance = jest.fn(); + (BrowserProvider as unknown as jest.Mock).mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }); + + mockGetBalance.mockImplementation(() => { + return BigInt(10000000000000000000); + }); + + const mockBalanceOf = jest + .fn() + .mockResolvedValueOnce(BigInt('10000000000000000001')); + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + balanceOf: mockBalanceOf, + })); + + expect( + await balanceUtils.hasSufficientBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + ZeroAddress, + '10000000000000000000', + '0x1', + ), + ).toBe(true); + + expect( + await balanceUtils.hasSufficientBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + '10000000000000000000', + '0x1', + ), + ).toBe(true); + }); + + it('should return false if user has native assets but insufficient ERC20 src tokens', async () => { + const mockGetBalance = jest.fn(); + (BrowserProvider as unknown as jest.Mock).mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }); + + mockGetBalance.mockImplementation(() => { + return BigInt(10000000000000000000); + }); + const mockFetchTokenBalance = jest.spyOn( + balanceUtils, + 'fetchTokenBalance', + ); + mockFetchTokenBalance.mockResolvedValueOnce(BigInt(9000000000000000000)); + + expect( + await balanceUtils.hasSufficientBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + '10000000000000000000', + '0x1', + ), + ).toBe(false); + }); + + it('should return false if source token balance is undefined', async () => { + const mockBalanceOf = jest.fn().mockResolvedValueOnce(undefined); + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + balanceOf: mockBalanceOf, + })); + + expect( + await balanceUtils.hasSufficientBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + '10000000000000000000', + '0x1', + ), + ).toBe(false); + + expect(mockBalanceOf).toHaveBeenCalledTimes(1); + expect(mockBalanceOf).toHaveBeenCalledWith( + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + ); + }); + }); +}); + +describe('fetchTokenBalance', () => { + let mockProvider: SafeEventEmitterProvider; + const mockAddress = '0x1234567890123456789012345678901234567890'; + const mockUserAddress = '0x9876543210987654321098765432109876543210'; + const mockBalance = BigInt(1000); + + beforeEach(() => { + jest.clearAllMocks(); + mockProvider = new FakeProvider(); + + // Mock BrowserProvider + (BrowserProvider as jest.Mock).mockImplementation(() => ({ + // Add any provider methods needed + })); + }); + + it('should fetch token balance when contract is valid', async () => { + // Mock Contract + const mockBalanceOf = jest.fn().mockResolvedValue(mockBalance); + (Contract as jest.Mock).mockImplementation(() => ({ + balanceOf: mockBalanceOf, + })); + + const result = await fetchTokenBalance( + mockAddress, + mockUserAddress, + mockProvider, + ); + + expect(BrowserProvider).toHaveBeenCalledWith(mockProvider); + expect(Contract).toHaveBeenCalledWith( + mockAddress, + abiERC20, + expect.anything(), + ); + expect(mockBalanceOf).toHaveBeenCalledWith(mockUserAddress); + expect(result).toBe(mockBalance); + }); + + it('should return undefined when contract is invalid', async () => { + // Mock Contract to return an object without balanceOf method + (Contract as jest.Mock).mockImplementation(() => ({ + // Empty object without balanceOf method + })); + + const result = await fetchTokenBalance( + mockAddress, + mockUserAddress, + mockProvider, + ); + + expect(BrowserProvider).toHaveBeenCalledWith(mockProvider); + expect(Contract).toHaveBeenCalledWith( + mockAddress, + abiERC20, + expect.anything(), + ); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/bridge-controller/src/utils/balance.ts b/packages/bridge-controller/src/utils/balance.ts new file mode 100644 index 00000000000..2788423f2df --- /dev/null +++ b/packages/bridge-controller/src/utils/balance.ts @@ -0,0 +1,51 @@ +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import type { Provider } from '@metamask/network-controller'; +import type { Hex } from '@metamask/utils'; +import { BrowserProvider, Contract, getAddress, ZeroAddress } from 'ethers'; + +export const fetchTokenBalance = async ( + address: string, + userAddress: string, + provider: Provider, +): Promise => { + const ethersProvider = new BrowserProvider(provider); + const tokenContract = new Contract(address, abiERC20, ethersProvider); + const tokenBalancePromise = + typeof tokenContract?.balanceOf === 'function' + ? tokenContract.balanceOf(userAddress) + : Promise.resolve(undefined); + return await tokenBalancePromise; +}; + +export const calcLatestSrcBalance = async ( + provider: Provider, + selectedAddress: string, + tokenAddress: string, + chainId: Hex, +): Promise => { + if (tokenAddress && chainId) { + if (tokenAddress === ZeroAddress) { + const ethersProvider = new BrowserProvider(provider); + return await ethersProvider.getBalance(getAddress(selectedAddress)); + } + return await fetchTokenBalance(tokenAddress, selectedAddress, provider); + } + return undefined; +}; + +export const hasSufficientBalance = async ( + provider: Provider, + selectedAddress: string, + tokenAddress: string, + fromTokenAmount: string, + chainId: Hex, +) => { + const srcTokenBalance = await calcLatestSrcBalance( + provider, + selectedAddress, + tokenAddress, + chainId, + ); + + return srcTokenBalance ? srcTokenBalance >= BigInt(fromTokenAmount) : false; +}; diff --git a/packages/bridge-controller/src/utils/bridge.test.ts b/packages/bridge-controller/src/utils/bridge.test.ts new file mode 100644 index 00000000000..013e9bf63eb --- /dev/null +++ b/packages/bridge-controller/src/utils/bridge.test.ts @@ -0,0 +1,170 @@ +/* eslint-disable n/no-process-env */ +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import type { Hex } from '@metamask/utils'; +import { Contract } from 'ethers'; + +import { + getEthUsdtResetData, + isEthUsdt, + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, + sumHexes, + getBridgeApiBaseUrl, +} from './bridge'; +import { + ETH_USDT_ADDRESS, + METABRIDGE_ETHEREUM_ADDRESS, +} from '../constants/bridge'; +import { + BRIDGE_DEV_API_BASE_URL, + BRIDGE_PROD_API_BASE_URL, +} from '../constants/bridge'; +import { CHAIN_IDS } from '../constants/chains'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; + +describe('Bridge utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('sumHexes', () => { + it('returns 0x0 for empty input', () => { + expect(sumHexes()).toBe('0x0'); + }); + + it('returns same value for single input', () => { + expect(sumHexes('0xff')).toBe('0xff'); + expect(sumHexes('0x0')).toBe('0x0'); + expect(sumHexes('0x1')).toBe('0x1'); + }); + + it('correctly sums two hex values', () => { + expect(sumHexes('0x1', '0x1')).toBe('0x2'); + expect(sumHexes('0xff', '0x1')).toBe('0x100'); + expect(sumHexes('0x0', '0xff')).toBe('0xff'); + }); + + it('correctly sums multiple hex values', () => { + expect(sumHexes('0x1', '0x2', '0x3')).toBe('0x6'); + expect(sumHexes('0xff', '0xff', '0x2')).toBe('0x200'); + expect(sumHexes('0x0', '0x0', '0x0')).toBe('0x0'); + }); + + it('handles large numbers', () => { + expect(sumHexes('0xffffffff', '0x1')).toBe('0x100000000'); + expect(sumHexes('0xffffffff', '0xffffffff')).toBe('0x1fffffffe'); + }); + + it('throws for invalid hex strings', () => { + expect(() => sumHexes('0xg')).toThrow('Cannot convert 0xg to a BigInt'); + }); + }); + + describe('getEthUsdtResetData', () => { + it('returns correct encoded function data for USDT approval reset', () => { + const expectedInterface = new Contract(ETH_USDT_ADDRESS, abiERC20) + .interface; + const expectedData = expectedInterface.encodeFunctionData('approve', [ + METABRIDGE_ETHEREUM_ADDRESS, + '0', + ]); + + expect(getEthUsdtResetData()).toBe(expectedData); + }); + }); + + describe('isEthUsdt', () => { + it('returns true for ETH USDT address on mainnet', () => { + expect(isEthUsdt(CHAIN_IDS.MAINNET, ETH_USDT_ADDRESS)).toBe(true); + expect(isEthUsdt(CHAIN_IDS.MAINNET, ETH_USDT_ADDRESS.toUpperCase())).toBe( + true, + ); + }); + + it('returns false for non-mainnet chain', () => { + expect(isEthUsdt(CHAIN_IDS.GOERLI, ETH_USDT_ADDRESS)).toBe(false); + }); + + it('returns false for different address on mainnet', () => { + expect(isEthUsdt(CHAIN_IDS.MAINNET, METABRIDGE_ETHEREUM_ADDRESS)).toBe( + false, + ); + }); + }); + + describe('isSwapsDefaultTokenAddress', () => { + it('returns true for default token address of given chain', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + const defaultToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + expect(isSwapsDefaultTokenAddress(defaultToken.address, chainId)).toBe( + true, + ); + }); + + it('returns false for non-default token address', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + expect(isSwapsDefaultTokenAddress('0x1234', chainId)).toBe(false); + }); + + it('returns false for invalid inputs', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + expect(isSwapsDefaultTokenAddress('', chainId)).toBe(false); + expect(isSwapsDefaultTokenAddress('0x1234', '' as Hex)).toBe(false); + }); + }); + + describe('isSwapsDefaultTokenSymbol', () => { + it('returns true for default token symbol of given chain', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + const defaultToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + expect(isSwapsDefaultTokenSymbol(defaultToken.symbol, chainId)).toBe( + true, + ); + }); + + it('returns false for non-default token symbol', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + expect(isSwapsDefaultTokenSymbol('FAKE', chainId)).toBe(false); + }); + + it('returns false for invalid inputs', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + expect(isSwapsDefaultTokenSymbol('', chainId)).toBe(false); + expect(isSwapsDefaultTokenSymbol('ETH', '' as Hex)).toBe(false); + }); + }); + + describe('getBridgeApiBaseUrl', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('returns custom API URL when BRIDGE_CUSTOM_API_BASE_URL is set', () => { + process.env.BRIDGE_CUSTOM_API_BASE_URL = 'https://custom-api.example.com'; + expect(getBridgeApiBaseUrl()).toBe('https://custom-api.example.com'); + }); + + it('returns dev API URL when BRIDGE_USE_DEV_APIS is set', () => { + process.env.BRIDGE_USE_DEV_APIS = 'true'; + expect(getBridgeApiBaseUrl()).toBe(BRIDGE_DEV_API_BASE_URL); + }); + + it('returns prod API URL by default', () => { + expect(getBridgeApiBaseUrl()).toBe(BRIDGE_PROD_API_BASE_URL); + }); + }); +}); diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts new file mode 100644 index 00000000000..b152c84ef09 --- /dev/null +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -0,0 +1,101 @@ +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import type { Hex } from '@metamask/utils'; +import { Contract } from 'ethers'; + +import { + DEFAULT_BRIDGE_CONTROLLER_STATE, + BRIDGE_DEV_API_BASE_URL, + BRIDGE_PROD_API_BASE_URL, + ETH_USDT_ADDRESS, + METABRIDGE_ETHEREUM_ADDRESS, +} from '../constants/bridge'; +import { CHAIN_IDS } from '../constants/chains'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; +import type { BridgeControllerState } from '../types'; + +export const getDefaultBridgeControllerState = (): BridgeControllerState => { + return DEFAULT_BRIDGE_CONTROLLER_STATE; +}; + +export const getBridgeApiBaseUrl = () => { + if (process.env.BRIDGE_CUSTOM_API_BASE_URL) { + return process.env.BRIDGE_CUSTOM_API_BASE_URL; + } + + if (process.env.BRIDGE_USE_DEV_APIS) { + return BRIDGE_DEV_API_BASE_URL; + } + + return BRIDGE_PROD_API_BASE_URL; +}; +/** + * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum + * + * @returns The txParam data that will reset allowance to 0, combine it with the approval tx params received from Bridge API + */ + +export const getEthUsdtResetData = () => { + const UsdtContractInterface = new Contract(ETH_USDT_ADDRESS, abiERC20) + .interface; + const data = UsdtContractInterface.encodeFunctionData('approve', [ + METABRIDGE_ETHEREUM_ADDRESS, + '0', + ]); + + return data; +}; + +export const isEthUsdt = (chainId: Hex, address: string) => + chainId === CHAIN_IDS.MAINNET && + address.toLowerCase() === ETH_USDT_ADDRESS.toLowerCase(); + +export const sumHexes = (...hexStrings: string[]): Hex => { + if (hexStrings.length === 0) { + return '0x0'; + } + + const sum = hexStrings.reduce((acc, hex) => acc + BigInt(hex), BigInt(0)); + return `0x${sum.toString(16)}`; +}; +/** + * Checks whether the provided address is strictly equal to the address for + * the default swaps token of the provided chain. + * + * @param address - The string to compare to the default token address + * @param chainId - The hex encoded chain ID of the default swaps token to check + * @returns Whether the address is the provided chain's default token address + */ + +export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { + if (!address || !chainId) { + return false; + } + + return ( + address === + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]?.address + ); +}; +/** + * Checks whether the provided symbol is strictly equal to the symbol for + * the default swaps token of the provided chain. + * + * @param symbol - The string to compare to the default token symbol + * @param chainId - The hex encoded chain ID of the default swaps token to check + * @returns Whether the symbol is the provided chain's default token symbol + */ + +export const isSwapsDefaultTokenSymbol = (symbol: string, chainId: Hex) => { + if (!symbol || !chainId) { + return false; + } + + return ( + symbol === + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]?.symbol + ); +}; diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts new file mode 100644 index 00000000000..a287e15af4b --- /dev/null +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -0,0 +1,350 @@ +import { ZeroAddress } from 'ethers'; + +import { + fetchBridgeFeatureFlags, + fetchBridgeQuotes, + fetchBridgeTokens, +} from './fetch'; +import mockBridgeQuotesErc20Erc20 from '../../tests/mock-quotes-erc20-erc20.json'; +import mockBridgeQuotesNativeErc20 from '../../tests/mock-quotes-native-erc20.json'; +import { BridgeClientId } from '../constants/bridge'; +import { CHAIN_IDS } from '../constants/chains'; + +const mockFetchFn = jest.fn(); + +describe('Bridge utils', () => { + describe('fetchBridgeFeatureFlags', () => { + it('should fetch bridge feature flags successfully', async () => { + const mockResponse = { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 1, + support: true, + chains: { + '1': { + isActiveSrc: true, + isActiveDest: true, + }, + '10': { + isActiveSrc: true, + isActiveDest: false, + }, + '59144': { + isActiveSrc: true, + isActiveDest: true, + }, + '120': { + isActiveSrc: true, + isActiveDest: false, + }, + '137': { + isActiveSrc: false, + isActiveDest: true, + }, + '11111': { + isActiveSrc: false, + isActiveDest: true, + }, + }, + }, + }; + + mockFetchFn.mockResolvedValue(mockResponse); + + const result = await fetchBridgeFeatureFlags( + BridgeClientId.EXTENSION, + mockFetchFn, + ); + + expect(mockFetchFn).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', + { + headers: { 'X-Client-Id': 'extension' }, + }, + ); + + expect(result).toStrictEqual({ + extensionConfig: { + maxRefreshCount: 1, + refreshRate: 3, + support: true, + chains: { + [CHAIN_IDS.MAINNET]: { + isActiveSrc: true, + isActiveDest: true, + }, + [CHAIN_IDS.OPTIMISM]: { + isActiveSrc: true, + isActiveDest: false, + }, + [CHAIN_IDS.LINEA_MAINNET]: { + isActiveSrc: true, + isActiveDest: true, + }, + '0x78': { + isActiveSrc: true, + isActiveDest: false, + }, + [CHAIN_IDS.POLYGON]: { + isActiveSrc: false, + isActiveDest: true, + }, + '0x2b67': { + isActiveSrc: false, + isActiveDest: true, + }, + }, + }, + }); + }); + + it('should use fallback bridge feature flags if response is unexpected', async () => { + const mockResponse = { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 1, + support: 25, + chains: { + a: { + isActiveSrc: 1, + isActiveDest: 'test', + }, + '2': { + isActiveSrc: 'test', + isActiveDest: 2, + }, + }, + }, + }; + + mockFetchFn.mockResolvedValue(mockResponse); + + const result = await fetchBridgeFeatureFlags( + BridgeClientId.EXTENSION, + mockFetchFn, + ); + + expect(mockFetchFn).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', + { + headers: { 'X-Client-Id': 'extension' }, + }, + ); + + expect(result).toStrictEqual({ + extensionConfig: { + maxRefreshCount: 5, + refreshRate: 30000, + support: false, + chains: {}, + }, + }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + mockFetchFn.mockRejectedValue(mockError); + + await expect( + fetchBridgeFeatureFlags(BridgeClientId.EXTENSION, mockFetchFn), + ).rejects.toThrow(mockError); + }); + }); + + describe('fetchBridgeTokens', () => { + it('should fetch bridge tokens successfully', async () => { + const mockResponse = [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + decimals: 16, + symbol: 'DEF', + aggregators: ['lifi'], + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f987', + symbol: 'DEF', + }, + { + address: '0x124', + symbol: 'JKL', + decimals: 16, + }, + ]; + + mockFetchFn.mockResolvedValue(mockResponse); + + const result = await fetchBridgeTokens( + '0xa', + BridgeClientId.EXTENSION, + mockFetchFn, + ); + + expect(mockFetchFn).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', + { + headers: { 'X-Client-Id': 'extension' }, + }, + ); + + expect(result).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: '', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + decimals: 16, + symbol: 'DEF', + aggregators: ['lifi'], + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 16, + symbol: 'ABC', + }, + }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + mockFetchFn.mockRejectedValue(mockError); + + await expect( + fetchBridgeTokens('0xa', BridgeClientId.EXTENSION, mockFetchFn), + ).rejects.toThrow(mockError); + }); + }); + + describe('fetchBridgeQuotes', () => { + it('should fetch bridge quotes successfully, no approvals', async () => { + mockFetchFn.mockResolvedValue(mockBridgeQuotesNativeErc20); + const { signal } = new AbortController(); + + const result = await fetchBridgeQuotes( + { + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: ZeroAddress, + destTokenAddress: ZeroAddress, + srcTokenAmount: '20000', + slippage: 0.5, + }, + signal, + BridgeClientId.EXTENSION, + mockFetchFn, + ); + + expect(mockFetchFn).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + { + headers: { 'X-Client-Id': 'extension' }, + signal, + }, + ); + + expect(result).toStrictEqual(mockBridgeQuotesNativeErc20); + }); + + it('should fetch bridge quotes successfully, with approvals', async () => { + mockFetchFn.mockResolvedValue([ + ...mockBridgeQuotesErc20Erc20, + { ...mockBridgeQuotesErc20Erc20[0], approval: null }, + { ...mockBridgeQuotesErc20Erc20[0], trade: null }, + ]); + const { signal } = new AbortController(); + + const result = await fetchBridgeQuotes( + { + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: ZeroAddress, + destTokenAddress: ZeroAddress, + srcTokenAmount: '20000', + slippage: 0.5, + }, + signal, + BridgeClientId.EXTENSION, + mockFetchFn, + ); + + expect(mockFetchFn).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + { + headers: { 'X-Client-Id': 'extension' }, + signal, + }, + ); + + expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); + }); + + it('should filter out malformed bridge quotes', async () => { + mockFetchFn.mockResolvedValue([ + ...mockBridgeQuotesErc20Erc20, + ...mockBridgeQuotesErc20Erc20.map( + ({ quote, ...restOfQuote }) => restOfQuote, + ), + { + ...mockBridgeQuotesErc20Erc20[0], + quote: { + srcAsset: { + ...mockBridgeQuotesErc20Erc20[0].quote.srcAsset, + decimals: undefined, + }, + }, + }, + { + ...mockBridgeQuotesErc20Erc20[1], + quote: { + srcAsset: { + ...mockBridgeQuotesErc20Erc20[1].quote.destAsset, + address: undefined, + }, + }, + }, + ]); + const { signal } = new AbortController(); + + const result = await fetchBridgeQuotes( + { + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: ZeroAddress, + destTokenAddress: ZeroAddress, + srcTokenAmount: '20000', + slippage: 0.5, + }, + signal, + BridgeClientId.EXTENSION, + mockFetchFn, + ); + + expect(mockFetchFn).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + { + headers: { 'X-Client-Id': 'extension' }, + signal, + }, + ); + + expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); + }); + }); +}); diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts new file mode 100644 index 00000000000..674b7bcf452 --- /dev/null +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -0,0 +1,201 @@ +import type { Hex } from '@metamask/utils'; +import { hexToNumber, numberToHex } from '@metamask/utils'; + +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, + getBridgeApiBaseUrl, +} from './bridge'; +import { + FEATURE_FLAG_VALIDATORS, + QUOTE_VALIDATORS, + TX_DATA_VALIDATORS, + TOKEN_VALIDATORS, + validateResponse, + QUOTE_RESPONSE_VALIDATORS, + FEE_DATA_VALIDATORS, +} from './validators'; +import { REFRESH_INTERVAL_MS } from '../constants/bridge'; +import type { SwapsTokenObject } from '../constants/tokens'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; +import type { + FeatureFlagResponse, + FeeData, + Quote, + QuoteRequest, + QuoteResponse, + TxData, + BridgeFeatureFlags, + FetchFunction, +} from '../types'; +import { BridgeFlag, FeeType, BridgeFeatureFlagsKey } from '../types'; + +// TODO put this back in once we have a fetchWithCache equivalent +// const CACHE_REFRESH_TEN_MINUTES = 10 * Duration.Minute; + +export const getClientIdHeader = (clientId: string) => ({ + 'X-Client-Id': clientId, +}); + +/** + * Fetches the bridge feature flags + * + * @param clientId - The client ID for metrics + * @param fetchFn - The fetch function to use + * @returns The bridge feature flags + */ +export async function fetchBridgeFeatureFlags( + clientId: string, + fetchFn: FetchFunction, +): Promise { + const url = `${getBridgeApiBaseUrl()}/getAllFeatureFlags`; + const rawFeatureFlags = await fetchFn(url, { + headers: getClientIdHeader(clientId), + }); + + if ( + validateResponse( + FEATURE_FLAG_VALIDATORS, + rawFeatureFlags, + url, + ) + ) { + return { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + ...rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG], + chains: Object.entries( + rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG].chains, + ).reduce( + (acc, [chainId, value]) => ({ + ...acc, + [numberToHex(Number(chainId))]: value, + }), + {}, + ), + }, + }; + } + + return { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: REFRESH_INTERVAL_MS, + maxRefreshCount: 5, + support: false, + chains: {}, + }, + }; +} + +/** + * Returns a list of enabled (unblocked) tokens + * + * @param chainId - The chain ID to fetch tokens for + * @param clientId - The client ID for metrics + * @param fetchFn - The fetch function to use + * @returns A list of enabled (unblocked) tokens + */ +export async function fetchBridgeTokens( + chainId: Hex, + clientId: string, + fetchFn: FetchFunction, +): Promise> { + // TODO make token api v2 call + const url = `${getBridgeApiBaseUrl()}/getTokens?chainId=${hexToNumber( + chainId, + )}`; + + // TODO we will need to cache these. In Extension fetchWithCache is used. This is due to the following: + // If we allow selecting dest networks which the user has not imported, + // note that the Assets controller won't be able to provide tokens. In extension we fetch+cache the token list from bridge-api to handle this + const tokens = await fetchFn(url, { + headers: getClientIdHeader(clientId), + }); + + const nativeToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + const transformedTokens: Record = {}; + if (nativeToken) { + transformedTokens[nativeToken.address] = nativeToken; + } + + tokens.forEach((token: unknown) => { + if ( + validateResponse(TOKEN_VALIDATORS, token, url, false) && + !( + isSwapsDefaultTokenSymbol(token.symbol, chainId) || + isSwapsDefaultTokenAddress(token.address, chainId) + ) + ) { + transformedTokens[token.address] = token; + } + }); + return transformedTokens; +} + +// Returns a list of bridge tx quotes +/** + * + * @param request - The quote request + * @param signal - The abort signal + * @param clientId - The client ID for metrics + * @param fetchFn - The fetch function to use + * @returns A list of bridge tx quotes + */ +export async function fetchBridgeQuotes( + request: QuoteRequest, + signal: AbortSignal, + clientId: string, + fetchFn: FetchFunction, +): Promise { + const queryParams = new URLSearchParams({ + walletAddress: request.walletAddress, + srcChainId: request.srcChainId.toString(), + destChainId: request.destChainId.toString(), + srcTokenAddress: request.srcTokenAddress, + destTokenAddress: request.destTokenAddress, + srcTokenAmount: request.srcTokenAmount, + slippage: request.slippage.toString(), + insufficientBal: request.insufficientBal ? 'true' : 'false', + resetApproval: request.resetApproval ? 'true' : 'false', + }); + const url = `${getBridgeApiBaseUrl()}/getQuote?${queryParams}`; + const quotes = await fetchFn(url, { + headers: getClientIdHeader(clientId), + signal, + }); + + const filteredQuotes = quotes.filter((quoteResponse: QuoteResponse) => { + const { quote, approval, trade } = quoteResponse; + return ( + validateResponse( + QUOTE_RESPONSE_VALIDATORS, + quoteResponse, + url, + ) && + validateResponse(QUOTE_VALIDATORS, quote, url) && + validateResponse( + TOKEN_VALIDATORS, + quote.srcAsset, + url, + ) && + validateResponse( + TOKEN_VALIDATORS, + quote.destAsset, + url, + ) && + validateResponse(TX_DATA_VALIDATORS, trade, url) && + validateResponse( + FEE_DATA_VALIDATORS, + quote.feeData[FeeType.METABRIDGE], + url, + ) && + (approval + ? validateResponse(TX_DATA_VALIDATORS, approval, url) + : true) + ); + }); + return filteredQuotes; +} diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts new file mode 100644 index 00000000000..8ea616fd345 --- /dev/null +++ b/packages/bridge-controller/src/utils/quote.ts @@ -0,0 +1,36 @@ +import type { QuoteRequest } from '../types'; + +export const isValidQuoteRequest = ( + partialRequest: Partial, + requireAmount = true, +): partialRequest is QuoteRequest => { + const stringFields = ['srcTokenAddress', 'destTokenAddress']; + if (requireAmount) { + stringFields.push('srcTokenAmount'); + } + const numberFields = ['srcChainId', 'destChainId', 'slippage']; + + return ( + stringFields.every( + (field) => + field in partialRequest && + typeof partialRequest[field as keyof typeof partialRequest] === + 'string' && + partialRequest[field as keyof typeof partialRequest] !== undefined && + partialRequest[field as keyof typeof partialRequest] !== '' && + partialRequest[field as keyof typeof partialRequest] !== null, + ) && + numberFields.every( + (field) => + field in partialRequest && + typeof partialRequest[field as keyof typeof partialRequest] === + 'number' && + partialRequest[field as keyof typeof partialRequest] !== undefined && + !isNaN(Number(partialRequest[field as keyof typeof partialRequest])) && + partialRequest[field as keyof typeof partialRequest] !== null, + ) && + (requireAmount + ? Boolean((partialRequest.srcTokenAmount ?? '').match(/^[1-9]\d*$/u)) + : true) + ); +}; diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts new file mode 100644 index 00000000000..56d8f93a47a --- /dev/null +++ b/packages/bridge-controller/src/utils/validators.ts @@ -0,0 +1,162 @@ +import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils'; +import { isStrictHexString } from '@metamask/utils'; + +import type { SwapsTokenObject } from '../constants/tokens'; +import type { + FeatureFlagResponse, + FeeData, + Quote, + QuoteResponse, + TxData, +} from '../types'; +import { BridgeFlag } from '../types'; + +export const truthyString = (string: string) => Boolean(string?.length); +export const truthyDigitString = (string: string) => + truthyString(string) && Boolean(string.match(/^\d+$/u)); + +export const isValidNumber = (v: unknown): v is number => typeof v === 'number'; +const isValidObject = (v: unknown): v is object => + typeof v === 'object' && v !== null; +const isValidString = (v: unknown): v is string => + typeof v === 'string' && v.length > 0; +const isValidHexAddress = (v: unknown) => + isValidString(v) && isValidHexAddress_(v, { allowNonPrefixed: false }); + +type Validator = { + property: keyof ExpectedResponse; + type: string; + validator?: (value: unknown) => boolean; +}; + +export const validateData = ( + validators: Validator[], + object: unknown, + urlUsed: string, + logError = true, +): object is ExpectedResponse => { + return validators.every(({ property, type, validator }) => { + const types = type.split('|'); + const propertyString = String(property); + + const valid = + isValidObject(object) && + types.some( + (_type) => + typeof object[propertyString as keyof typeof object] === _type, + ) && + (!validator || validator(object[propertyString as keyof typeof object])); + + if (!valid && logError) { + const value = isValidObject(object) + ? object[propertyString as keyof typeof object] + : undefined; + const typeString = isValidObject(object) + ? typeof object[propertyString as keyof typeof object] + : 'undefined'; + + console.error( + `response to GET ${urlUsed} invalid for property ${String(property)}; value was:`, + value, + '| type was: ', + typeString, + ); + } + return valid; + }); +}; + +export const validateResponse = ( + validators: Validator[], + data: unknown, + urlUsed: string, + logError = true, +): data is ExpectedResponse => { + return validateData(validators, data, urlUsed, logError); +}; + +export const FEATURE_FLAG_VALIDATORS = [ + { + property: BridgeFlag.EXTENSION_CONFIG, + type: 'object', + validator: ( + v: unknown, + ): v is Pick => + isValidObject(v) && + 'refreshRate' in v && + isValidNumber(v.refreshRate) && + 'maxRefreshCount' in v && + isValidNumber(v.maxRefreshCount) && + 'chains' in v && + isValidObject(v.chains) && + Object.values(v.chains).every((chain) => isValidObject(chain)) && + Object.values(v.chains).every( + (chain) => + 'isActiveSrc' in chain && + 'isActiveDest' in chain && + typeof chain.isActiveSrc === 'boolean' && + typeof chain.isActiveDest === 'boolean', + ), + }, +]; + +export const TOKEN_AGGREGATOR_VALIDATORS = [ + { + property: 'aggregators', + type: 'object', + validator: (v: unknown): v is number[] => + isValidObject(v) && Object.values(v).every(isValidString), + }, +]; + +export const TOKEN_VALIDATORS: Validator[] = [ + { property: 'decimals', type: 'number' }, + { property: 'address', type: 'string', validator: isValidHexAddress }, + { + property: 'symbol', + type: 'string', + validator: (v: unknown) => isValidString(v) && v.length <= 12, + }, +]; + +export const QUOTE_RESPONSE_VALIDATORS: Validator[] = [ + { property: 'quote', type: 'object', validator: isValidObject }, + { property: 'estimatedProcessingTimeInSeconds', type: 'number' }, + { + property: 'approval', + type: 'object|undefined', + validator: (v: unknown) => v === undefined || isValidObject(v), + }, + { property: 'trade', type: 'object', validator: isValidObject }, +]; + +export const QUOTE_VALIDATORS: Validator[] = [ + { property: 'requestId', type: 'string' }, + { property: 'srcTokenAmount', type: 'string' }, + { property: 'destTokenAmount', type: 'string' }, + { property: 'bridgeId', type: 'string' }, + { property: 'bridges', type: 'object', validator: isValidObject }, + { property: 'srcChainId', type: 'number' }, + { property: 'destChainId', type: 'number' }, + { property: 'srcAsset', type: 'object', validator: isValidObject }, + { property: 'destAsset', type: 'object', validator: isValidObject }, + { property: 'feeData', type: 'object', validator: isValidObject }, +]; + +export const FEE_DATA_VALIDATORS: Validator[] = [ + { + property: 'amount', + type: 'string', + validator: (v: unknown) => truthyDigitString(String(v)), + }, + { property: 'asset', type: 'object', validator: isValidObject }, +]; + +export const TX_DATA_VALIDATORS: Validator[] = [ + { property: 'chainId', type: 'number' }, + { property: 'value', type: 'string', validator: isStrictHexString }, + { property: 'gasLimit', type: 'number' }, + { property: 'to', type: 'string', validator: isValidHexAddress }, + { property: 'from', type: 'string', validator: isValidHexAddress }, + { property: 'data', type: 'string', validator: isStrictHexString }, +]; diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.json b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.json new file mode 100644 index 00000000000..8b589aa85e1 --- /dev/null +++ b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.json @@ -0,0 +1,248 @@ +[ + { + "quote": { + "requestId": "90ae8e69-f03a-4cf6-bab7-ed4e3431eb37", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + }, + "srcTokenAmount": "14000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "13984280", + "feeData": { + "metabridge": { + "amount": "0", + "asset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["across"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://miro.medium.com/max/800/1*PN_F5yW4VMBgs_xX-fsyzQ.png" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "14000000", + "destAmount": "13984280" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "approval": { + "chainId": 10, + "to": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000d59f80", + "gasLimit": 61865 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x038d7ea4c68000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000890000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000004a0c3540448000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000019d0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000284792ebcb90000000000000000000000000000000000000000000000000000000000d59f80000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000454000000000000000000000000000000000000000000000000000000000000000c40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000d55a40000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000067041c47000000000000000000000000000000000000000000000000000000006704704d00000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef765753be7f7a64d5509974b0d678e1e3149b02f42c7402906f9888136205038026f20b3f6df2899044cab41d632bc7a6c35debd40516df85de6f194aeb05b72cb9ea4d5ce0f7c56c91a79536331112f1a846dc641c", + "gasLimit": 287227 + }, + "estimatedProcessingTimeInSeconds": 60 + }, + { + "quote": { + "requestId": "0b6caac9-456d-47e6-8982-1945ae81ae82", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + }, + "srcTokenAmount": "14000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "13800000", + "feeData": { + "metabridge": { + "amount": "0", + "asset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["celercircle"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "cctp", + "displayName": "Circle CCTP", + "icon": "https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "14000000", + "destAmount": "13800000" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "approval": { + "chainId": 10, + "to": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000d59f80", + "gasLimit": 61865 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x038d7ea4c68000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004400000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000890000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000002e4c3540448000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000018c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e4b7dfe9d00000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c4000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000030d400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000138bc5930d51a475e4669db259f69e61ca33803675e76540f062a76af8cbaef4672c9926e56d6a8c29a263de3ee8f734ad760461c448f82fdccdd8c2360fffba1b", + "gasLimit": 343079 + }, + "estimatedProcessingTimeInSeconds": 1560 + } +] diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-native.json b/packages/bridge-controller/tests/mock-quotes-erc20-native.json new file mode 100644 index 00000000000..cd4a1963c6f --- /dev/null +++ b/packages/bridge-controller/tests/mock-quotes-erc20-native.json @@ -0,0 +1,894 @@ +[ + { + "quote": { + "requestId": "a63df72a-75ae-4416-a8ab-aff02596c75c", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destChainId": 42161, + "destTokenAmount": "991225000000000000", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["stargate"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "stargate", + "displayName": "StargateV2 (Fast mode)", + "icon": "https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3136", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "991225000000000000" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x1c8598b5db2e", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006c00000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000564a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003804bdedbea3f94faf8c8fac5ec841251d96cf5e64e8706ada4688877885e5249520000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d0000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000043ccfd60b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000001c8598b5db2e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000759e000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c83dc7c11df600d7293f778cb365d3dfcc1ffa2221cf5447a8f2ea407a97792135d9f585ecb68916479dfa1f071f169cbe1cfec831b5ad01f4e4caa09204e5181c", + "gasLimit": 641446 + }, + "estimatedProcessingTimeInSeconds": 64 + }, + { + "quote": { + "requestId": "aad73198-a64d-4310-b12d-9dcc81c412e2", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destChainId": 42161, + "destTokenAmount": "991147696728676903", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["celer"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "celer", + "displayName": "Celer cBridge", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/cbridge.svg" + }, + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "991147696728676903" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000e7bf43c55551b1036e796e7fd3b125d1f9903e2e000000000000000000000000e7bf43c55551b1036e796e7fd3b125d1f9903e2e000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000050f68486970f93a855b27794b8141d32a89a1e0a5ef360034a2f60a4b917c188380000a4b1420000000000000000000000000000000000000600000000000000000dc1a09f859b20002c03873900002777000000000000000000000000000000002d68122053030bf8df41a8bb8c6f0a9de411c7d94eed376b7d91234e1585fd9f77dcf974dd25160d0c2c16c8382d8aa85b0edd429edff19b4d4cdcf50d0a9d4d1c", + "gasLimit": 203352 + }, + "estimatedProcessingTimeInSeconds": 53 + }, + { + "quote": { + "requestId": "6cfd4952-c9b2-4aec-9349-af39c212f84b", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destChainId": 42161, + "destTokenAmount": "991112862890876485", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["across"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png" + }, + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "991112862890876485" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000000902340ab8f6a57ef0c43231b98141d32a89a1e0a5ef360034a2f60a4b917c18838420000000000000000000000000000000000000600000000000000000dc1a09f859b20000000a4b100007dd39298f9ad673645ebffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b710000000000000000000000000000000088d06e7971021eee573a0ab6bc3e22039fc1c5ded5d12c4cf2b6311f47f909e06197aa8b2f647ae78ae33a6ea5d23f7c951c0e1686abecd01d7c796990d56f391c", + "gasLimit": 177423 + }, + "estimatedProcessingTimeInSeconds": 15 + }, + { + "quote": { + "requestId": "2c2ba7d8-3922-4081-9f27-63b7d5cc1986", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destChainId": 42161, + "destTokenAmount": "990221346602370184", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["hop"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "hop", + "displayName": "Hop", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/hop.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3136", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "990221346602370184" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000484ca360ae0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000001168a464edd170000000000000000000000000000000000000000000000000dac6213fc70c84400000000000000000000000000000000000000000000000000000000673a3b080000000000000000000000000000000000000000000000000dac6213fc70c84400000000000000000000000000000000000000000000000000000000673a3b0800000000000000000000000086ca30bef97fb651b8d866d45503684b90cb3312000000000000000000000000710bda329b2a6224e4b44833de30f38e7f81d5640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067997b63db4b9059d22e50750707b46a6d48dfbb32e50d85fc3bff1170ed9ca30000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003686f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d0000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000043ccfd60b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000099d00cde1f22e8afd37d7f103ec3c6c1eb835ace46e502ec8c5ab51413e539461b89c0e26892efd1de1cbfe4222b5589e76231080252197507cce4fb72a30b031b", + "gasLimit": 547501 + }, + "estimatedProcessingTimeInSeconds": 24.159 + }, + { + "quote": { + "requestId": "a77bc7b2-e8c8-4463-89db-5dd239d6aacc", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "name": "Wrapped Ether", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/WETH", + "logoURI": "https://media.socket.tech/tokens/all/WETH", + "chainAgnosticId": "ETH" + }, + "srcTokenAmount": "991250000000000000", + "destChainId": 42161, + "destAsset": { + "chainId": 42161, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "destTokenAmount": "991147696728676903", + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "chainId": 10, + "address": "0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "name": "Wrapped Ether", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/WETH", + "logoURI": "https://media.socket.tech/tokens/all/WETH", + "chainAgnosticId": "ETH" + } + } + }, + "bridgeId": "socket", + "bridges": ["celer"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "celer", + "displayName": "Celer", + "icon": "https://socketicons.s3.amazonaws.com/Celer+Light.png" + }, + "srcAsset": { + "chainId": 10, + "address": "0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "name": "Wrapped Ether", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/WETH", + "logoURI": "https://media.socket.tech/tokens/all/WETH", + "chainAgnosticId": "ETH" + }, + "destAsset": { + "chainId": 42161, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "srcAmount": "991250000000000000", + "destAmount": "991147696728676903" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a5187000000000000000000000000000000000000000000000000000000000000004c0000001252106ce9141d32a89a1e0a5ef360034a2f60a4b917c18838420000000000000000000000000000000000000600000000000000000dc1a09f859b20000000a4b1245fa5dd00002777000000000000000000000000000000000000000022be703a074ef6089a301c364c2bbf391d51067ea5cd91515c9ec5421cdaabb23451cd2086f3ebe3e19ff138f3a9be154dcae6033838cc5fabeeb0d260b075cb1c", + "gasLimit": 182048 + }, + "estimatedProcessingTimeInSeconds": 360 + }, + { + "quote": { + "requestId": "4f2154d9b330221b2ad461adf63acc2c", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "id": "10_0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "name": "Wrapped ETH", + "decimals": 18, + "usdPrice": 3135.9632118339764, + "coingeckoId": "weth", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + "volatility": 2, + "axelarNetworkSymbol": "WETH", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg" + }, + "destChainId": 42161, + "destTokenAmount": "989989428114299041", + "destAsset": { + "id": "42161_0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": "ETH", + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "name": "ETH", + "decimals": 18, + "usdPrice": 3133.259355489038, + "coingeckoId": "ethereum", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/eth.svg", + "volatility": 2, + "axelarNetworkSymbol": "ETH", + "subGraphIds": ["chainflip-bridge"], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/eth.svg" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "id": "10_0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "name": "Wrapped ETH", + "decimals": 18, + "usdPrice": 3135.9632118339764, + "coingeckoId": "weth", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + "volatility": 2, + "axelarNetworkSymbol": "WETH", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg" + } + } + }, + "bridgeId": "squid", + "bridges": ["axelar"], + "steps": [ + { + "action": "swap", + "srcChainId": 10, + "destChainId": 10, + "protocol": { + "name": "Uniswap V3", + "displayName": "Uniswap V3" + }, + "srcAsset": { + "id": "10_0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "name": "Wrapped ETH", + "decimals": 18, + "usdPrice": 3135.9632118339764, + "coingeckoId": "weth", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + "axelarNetworkSymbol": "WETH", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg" + }, + "destAsset": { + "id": "10_0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "chainId": 10, + "name": "USDC", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc", "cctp-uusdc-optimism-to-noble"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "srcAmount": "991250000000000000", + "destAmount": "3100880215" + }, + { + "action": "swap", + "srcChainId": 10, + "destChainId": 10, + "protocol": { + "name": "Uniswap V3", + "displayName": "Uniswap V3" + }, + "srcAsset": { + "id": "10_0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "chainId": 10, + "name": "USDC", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc", "cctp-uusdc-optimism-to-noble"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "10_0x7f5c764cbc14f9669b88837ca1490cca17c31607", + "symbol": "USDC.e", + "address": "0x7f5c764cbc14f9669b88837ca1490cca17c31607", + "chainId": 10, + "name": "USDC.e", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC.e", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "srcAmount": "3100880215", + "destAmount": "3101045779" + }, + { + "action": "swap", + "srcChainId": 10, + "destChainId": 10, + "protocol": { + "name": "Uniswap V3", + "displayName": "Uniswap V3" + }, + "srcAsset": { + "id": "10_0x7f5c764cbc14f9669b88837ca1490cca17c31607", + "symbol": "USDC.e", + "address": "0x7f5c764cbc14f9669b88837ca1490cca17c31607", + "chainId": 10, + "name": "USDC.e", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC.e", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "10_0xeb466342c4d449bc9f53a865d5cb90586f405215", + "symbol": "USDC.axl", + "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215", + "chainId": 10, + "name": " USDC (Axelar)", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "interchainTokenId": null, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "axlUSDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg" + }, + "srcAmount": "3101045779", + "destAmount": "3101521947" + }, + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "axelar", + "displayName": "Axelar" + }, + "srcAsset": { + "id": "10_0xeb466342c4d449bc9f53a865d5cb90586f405215", + "symbol": "USDC.axl", + "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215", + "chainId": 10, + "name": " USDC (Axelar)", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "interchainTokenId": null, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "axlUSDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "42161_0xeb466342c4d449bc9f53a865d5cb90586f405215", + "symbol": "USDC.axl", + "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215", + "chainId": 42161, + "name": " USDC (Axelar)", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "interchainTokenId": null, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "axlUSDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg" + }, + "srcAmount": "3101521947", + "destAmount": "3101521947" + }, + { + "action": "swap", + "srcChainId": 42161, + "destChainId": 42161, + "protocol": { + "name": "Pancakeswap V3", + "displayName": "Pancakeswap V3" + }, + "srcAsset": { + "id": "42161_0xeb466342c4d449bc9f53a865d5cb90586f405215", + "symbol": "USDC.axl", + "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215", + "chainId": 42161, + "name": " USDC (Axelar)", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "interchainTokenId": null, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "axlUSDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "symbol": "USDC", + "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "chainId": 42161, + "name": "USDC", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC", + "subGraphOnly": false, + "subGraphIds": [ + "uusdc", + "cctp-uusdc-arbitrum-to-noble", + "chainflip-bridge" + ], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "srcAmount": "3101521947", + "destAmount": "3100543869" + }, + { + "action": "swap", + "srcChainId": 42161, + "destChainId": 42161, + "protocol": { + "name": "Uniswap V3", + "displayName": "Uniswap V3" + }, + "srcAsset": { + "id": "42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "symbol": "USDC", + "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "chainId": 42161, + "name": "USDC", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC", + "subGraphOnly": false, + "subGraphIds": [ + "uusdc", + "cctp-uusdc-arbitrum-to-noble", + "chainflip-bridge" + ], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "42161_0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "symbol": "WETH", + "address": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "chainId": 42161, + "name": "Wrapped ETH", + "decimals": 18, + "usdPrice": 3135.9632118339764, + "interchainTokenId": null, + "coingeckoId": "weth", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/weth.svg", + "axelarNetworkSymbol": "WETH", + "subGraphOnly": false, + "subGraphIds": ["arbitrum-weth-wei"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/weth.svg" + }, + "srcAmount": "3100543869", + "destAmount": "989989428114299041" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x4653ce53e6b1", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000e73717569644164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b60000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000001a14846a1bc600000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000ce00000000000000000000000000000000000000000000000000000000000000d200000000000000000000000000000000000000000000000000000000000000d600000000000000000000000000000000000000000000000000000000000000dc0000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000098000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf00000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c0000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000b8833d8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8d3ad5700000000000000000000000000000000000000000000000000000000b8c346b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d66600000000000000000000000000000000000000000000000000000000b8d6341300000000000000000000000000000000000000000000000000000000b8ca89fa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000761786c55534443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008417262697472756d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a307863653136463639333735353230616230313337376365374238386635424138433438463844363636000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c100000000000000000000000000000000000000000000000000000000000000040000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009200000000000000000000000000000000000000000000000000000000000000a8000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8dd781b00000000000000000000000000000000000000000000000000000000b8bb9ee30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f40521500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8ce8b7d0000000000000000000000000000000000000000000000000db72b79f837011c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000000004f2154d9b330221b2ad461adf63acc2c000000000000000000000000000000004f2154d9b330221b2ad461adf63acc2c0000000000000000000000003c17c95cdb5887c334bfae85750ce00e1a720a76eff35e60db6c9f3b8384a6d63db3c56f1ce6545b50ba2f250429055ca77e7e6203ddd65a7a4d89ae1af3d61b1c", + "gasLimit": 710342 + }, + "estimatedProcessingTimeInSeconds": 20 + } +] diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json new file mode 100644 index 00000000000..0afd77760e7 --- /dev/null +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json @@ -0,0 +1,258 @@ +[ + { + "quote": { + "requestId": "34c4136d-8558-4d87-bdea-eef8d2d30d6d", + "srcChainId": 1, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destChainId": 42161, + "destTokenAmount": "3104367033", + "destAsset": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "chainId": 42161, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9998000399920016", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["across"], + "steps": [ + { + "action": "swap", + "srcChainId": 1, + "destChainId": 1, + "protocol": { + "name": "0x", + "displayName": "0x", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/zerox.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9997000899730081", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "3104701473" + }, + { + "action": "bridge", + "srcChainId": 1, + "destChainId": 42161, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png" + }, + "srcAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9997000899730081", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "destAsset": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "chainId": 42161, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9998000399920016", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "srcAmount": "3104701473", + "destAmount": "3104367033" + } + ] + }, + "trade": { + "chainId": 1, + "to": "0x0439e60F02a8900a951603950d8D4527f400C3f1", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x0de0b6b3a7640000", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c696669416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de51520000000000000000000000000000000000000000000000000000000000000a003a3f733200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000094027363a1fac5600d1f7e8a4c50087ff1f32a09359512d2379d46b331c6033cc7b000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000b8211d6e000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066163726f73730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000005c42213bc0b00000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004e41fff991f0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b909399a00000000000000000000000000000000000000000000000000000000000000a094cc69295a8f2a3016ede239627ab300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e48d68a15600000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f4710000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2010001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012438c9c147000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000005000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000620541d325b000000000000000000000000000000000000000000000000000000000673656d70000000000000000000000000000000000000000000000000000000000000080ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b71dcbfe555f9a744b18195d9b52032871d6f3c5a558275c08a71c2b6214801f5161be976f49181b854a3ebcbe1f2b896133b03314a5ff2746e6494c43e59d0c9ee1c", + "gasLimit": 540076 + }, + "estimatedProcessingTimeInSeconds": 45 + }, + { + "quote": { + "requestId": "5bf0f2f0-655c-4e13-a545-1ebad6f9d2bc", + "srcChainId": 1, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destChainId": 42161, + "destTokenAmount": "3104601473", + "destAsset": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "chainId": 42161, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9998000399920016", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["celercircle"], + "steps": [ + { + "action": "swap", + "srcChainId": 1, + "destChainId": 1, + "protocol": { + "name": "0x", + "displayName": "0x", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/zerox.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9997000899730081", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "3104701473" + }, + { + "action": "bridge", + "srcChainId": 1, + "destChainId": 42161, + "protocol": { + "name": "celercircle", + "displayName": "Circle CCTP", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/circle.png" + }, + "srcAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9997000899730081", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "destAsset": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "chainId": 42161, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9998000399920016", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "srcAmount": "3104701473", + "destAmount": "3104601473" + } + ] + }, + "trade": { + "chainId": 1, + "to": "0x0439e60F02a8900a951603950d8D4527f400C3f1", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x0de0b6b3a7640000", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c696669416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000009248fab066300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200b431adcab44c6fe13ade53dbd3b714f57922ab5b776924a913685ad0fe680f6c000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000b8211d6e000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b63656c6572636972636c65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000005c42213bc0b00000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004e41fff991f0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b909399a00000000000000000000000000000000000000000000000000000000000000a0c0452b52ecb7cf70409b16cd627ab300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e48d68a15600000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f4710000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2010001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012438c9c147000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000005000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000047896dca097909ba9db4c9631bce0e53090bce14a9b7d203e21fa80cee7a16fa049aa1ef7d663c2ec3148e698e01774b62ddedc9c2dcd21994e549cd6f318f971b", + "gasLimit": 682910 + }, + "estimatedProcessingTimeInSeconds": 1029.717 + } +] diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20.json b/packages/bridge-controller/tests/mock-quotes-native-erc20.json new file mode 100644 index 00000000000..f7efe7950ba --- /dev/null +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20.json @@ -0,0 +1,294 @@ +[ + { + "quote": { + "requestId": "381c23bc-e3e4-48fe-bc53-257471e388ad", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "srcTokenAmount": "9912500000000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "24438902", + "feeData": { + "metabridge": { + "amount": "87500000000000", + "asset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["across"], + "steps": [ + { + "action": "swap", + "srcChainId": 10, + "protocol": { + "name": "zerox", + "displayName": "0x", + "icon": "https://media.socket.tech/dexes/0x.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://assets.polygon.technology/tokenAssets/eth.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/eth.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "srcAmount": "9912500000000000", + "destAmount": "24456223" + }, + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://miro.medium.com/max/800/1*PN_F5yW4VMBgs_xX-fsyzQ.png" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "24456223", + "destAmount": "24438902" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x27147114878000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002714711487800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b657441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f600000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000004f94ae6af800000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000e2037c6145a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000d64123506490000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001960000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000019d0000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000904ee8f0b86000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc156080000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000828415565b0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000001734d0800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000173dbd3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002b42000000000000000000000000000000000000060001f40b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000008ecb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004200000000000000000000000000000000000006000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000001000000000000000000000000000000000000011000000000000000000000000000000000000000021582def464917822ff6092c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000043a900000000000000000000000000000000000000000000000000000000000000c40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000174e7be000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000067041c47000000000000000000000000000000000000000000000000000000006704704d00000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef765753be7f7a64d5509974b0d678e1e3149b02f41fec59a4aef7d9ac92ee5eeaf293cb28c2261e7fd322723a97cb83762f7302296636026e52849fdad0f9db6e1640f914660e6b13f5b1a29345344c8c5687abbf1b", + "gasLimit": 610414 + }, + "estimatedProcessingTimeInSeconds": 60 + }, + { + "quote": { + "requestId": "4277a368-40d7-4e82-aa67-74f29dc5f98a", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "srcTokenAmount": "9912500000000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "24256223", + "feeData": { + "metabridge": { + "amount": "87500000000000", + "asset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["celercircle"], + "steps": [ + { + "action": "swap", + "srcChainId": 10, + "protocol": { + "name": "zerox", + "displayName": "0x", + "icon": "https://media.socket.tech/dexes/0x.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://assets.polygon.technology/tokenAssets/eth.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/eth.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "srcAmount": "9912500000000000", + "destAmount": "24456223" + }, + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "cctp", + "displayName": "Circle CCTP", + "icon": "https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "24456223", + "destAmount": "24256223" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x27147114878000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002714711487800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b657441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000004f94ae6af800000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000c6437c6145a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000bc4123506490000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001960000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000018c0000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000904ee8f0b86000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc156080000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000828415565b0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000001734d0800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000173dbd3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002b42000000000000000000000000000000000000060001f40b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000008ecb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004200000000000000000000000000000000000006000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd00000000000000000000000010000000000000000000000000000000000000110000000000000000000000000000000000000000974132b87a5cb75e32f034280000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000030d4000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f9e43204a24f476db20f2518722627a122d31a1bc7c63fc15412e6a327295a9460b76bea5bb53b1f73fa6a15811055f6bada592d2e9e6c8cf48a855ce6968951c", + "gasLimit": 664389 + }, + "estimatedProcessingTimeInSeconds": 15 + } +] diff --git a/packages/bridge-controller/tsconfig.build.json b/packages/bridge-controller/tsconfig.build.json new file mode 100644 index 00000000000..b62ec3ff054 --- /dev/null +++ b/packages/bridge-controller/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../accounts-controller/tsconfig.build.json" }, + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../polling-controller/tsconfig.build.json" }, + { "path": "../transaction-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json new file mode 100644 index 00000000000..3f93de1f5e6 --- /dev/null +++ b/packages/bridge-controller/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "resolveJsonModule": true + }, + "references": [ + { "path": "../accounts-controller" }, + { "path": "../base-controller" }, + { "path": "../controller-utils" }, + { "path": "../network-controller" }, + { "path": "../polling-controller" }, + { "path": "../transaction-controller" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/bridge-controller/typedoc.json b/packages/bridge-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/bridge-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/teams.json b/teams.json index eabdb25af76..427e514be97 100644 --- a/teams.json +++ b/teams.json @@ -5,6 +5,7 @@ "metamask/approval-controller": "team-confirmations", "metamask/assets-controllers": "team-assets", "metamask/base-controller": "team-wallet-framework", + "metamask/bridge-controller": "team-swaps,team-bridge", "metamask/build-utils": "team-wallet-framework", "metamask/composable-controller": "team-wallet-framework", "metamask/controller-utils": "team-wallet-framework", diff --git a/yarn.lock b/yarn.lock index dc8e1e4cfac..5ad3020e25f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2582,6 +2582,39 @@ __metadata: languageName: unknown linkType: soft +"@metamask/bridge-controller@workspace:packages/bridge-controller": + version: 0.0.0-use.local + resolution: "@metamask/bridge-controller@workspace:packages/bridge-controller" + dependencies: + "@metamask/accounts-controller": "npm:^23.1.0" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/controller-utils": "npm:^11.5.0" + "@metamask/eth-json-rpc-provider": "npm:^4.1.8" + "@metamask/json-rpc-engine": "npm:^10.0.3" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/network-controller": "npm:^22.2.1" + "@metamask/polling-controller": "npm:^12.0.3" + "@metamask/transaction-controller": "npm:^45.1.0" + "@metamask/utils": "npm:^11.1.0" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + ethers: "npm:^6.12.0" + jest: "npm:^27.5.1" + jest-environment-jsdom: "npm:^27.5.1" + lodash: "npm:^4.17.21" + nock: "npm:^13.3.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + peerDependencies: + "@metamask/accounts-controller": ^23.0.0 + "@metamask/network-controller": ^22.0.0 + "@metamask/transaction-controller": ^45.0.0 + languageName: unknown + linkType: soft + "@metamask/browser-passworder@npm:^4.3.0": version: 4.3.0 resolution: "@metamask/browser-passworder@npm:4.3.0" From 3f8fd988e68fbe8c036f828b2a331e3a5a223d65 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Fri, 14 Feb 2025 12:33:50 +0100 Subject: [PATCH 16/20] feat: add optional `validateAgainstSchema` option when creating user storage entry paths (#5326) ## Explanation This PR adds a new `validateAgainstSchema` option when creating user storage entry paths. This defaults to true, but can be relaxed to false when using the SDK. SDK users should use the feature and key names they decide to use, and Controller users should follow the internal schema. ## References Related to: https://consensyssoftware.atlassian.net/browse/IDENTITY-31 ## Changelog ### `@metamask/profile-sync-controller` - **ADDED**: Optional `validateAgainstSchema` option when creating user storage entry paths ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../src/sdk/user-storage.ts | 60 ++++++----- .../src/shared/storage-schema.test.ts | 19 ++++ .../src/shared/storage-schema.ts | 101 +++++++++++++----- 3 files changed, 128 insertions(+), 52 deletions(-) diff --git a/packages/profile-sync-controller/src/sdk/user-storage.ts b/packages/profile-sync-controller/src/sdk/user-storage.ts index b8e7f63b54d..536ecaa9d1d 100644 --- a/packages/profile-sync-controller/src/sdk/user-storage.ts +++ b/packages/profile-sync-controller/src/sdk/user-storage.ts @@ -5,10 +5,10 @@ import { SHARED_SALT } from '../shared/encryption/constants'; import type { Env } from '../shared/env'; import { getEnvUrls } from '../shared/env'; import type { - UserStorageFeatureKeys, - UserStorageFeatureNames, - UserStoragePathWithFeatureAndKey, - UserStoragePathWithFeatureOnly, + UserStorageGenericFeatureKey, + UserStorageGenericFeatureName, + UserStorageGenericPathWithFeatureAndKey, + UserStorageGenericPathWithFeatureOnly, } from '../shared/storage-schema'; import { createEntryPath } from '../shared/storage-schema'; @@ -54,42 +54,46 @@ export class UserStorage { } async setItem( - path: UserStoragePathWithFeatureAndKey, + path: UserStorageGenericPathWithFeatureAndKey, value: string, ): Promise { await this.#upsertUserStorage(path, value); } - async batchSetItems( - path: FeatureName, - values: [UserStorageFeatureKeys, string][], + async batchSetItems( + path: UserStorageGenericFeatureName, + values: [UserStorageGenericFeatureKey, string][], ) { await this.#batchUpsertUserStorage(path, values); } - async getItem(path: UserStoragePathWithFeatureAndKey): Promise { + async getItem( + path: UserStorageGenericPathWithFeatureAndKey, + ): Promise { return this.#getUserStorage(path); } async getAllFeatureItems( - path: UserStoragePathWithFeatureOnly, + path: UserStorageGenericFeatureName, ): Promise { return this.#getUserStorageAllFeatureEntries(path); } - async deleteItem(path: UserStoragePathWithFeatureAndKey): Promise { + async deleteItem( + path: UserStorageGenericPathWithFeatureAndKey, + ): Promise { return this.#deleteUserStorage(path); } async deleteAllFeatureItems( - path: UserStoragePathWithFeatureOnly, + path: UserStorageGenericFeatureName, ): Promise { return this.#deleteUserStorageAllFeatureEntries(path); } async batchDeleteItems( - path: UserStoragePathWithFeatureOnly, - values: string[], + path: UserStorageGenericFeatureName, + values: UserStorageGenericFeatureKey[], ) { return this.#batchDeleteUserStorage(path, values); } @@ -110,14 +114,16 @@ export class UserStorage { } async #upsertUserStorage( - path: UserStoragePathWithFeatureAndKey, + path: UserStorageGenericPathWithFeatureAndKey, data: string, ): Promise { try { const headers = await this.#getAuthorizationHeader(); const storageKey = await this.getStorageKey(); const encryptedData = await encryption.encryptString(data, storageKey); - const encryptedPath = createEntryPath(path, storageKey); + const encryptedPath = createEntryPath(path, storageKey, { + validateAgainstSchema: false, + }); const url = new URL(STORAGE_URL(this.env, encryptedPath)); @@ -150,7 +156,7 @@ export class UserStorage { } async #batchUpsertUserStorage( - path: UserStoragePathWithFeatureOnly, + path: UserStorageGenericPathWithFeatureOnly, data: [string, string][], ): Promise { try { @@ -201,7 +207,7 @@ export class UserStorage { } async #batchUpsertUserStorageWithAlreadyHashedAndEncryptedEntries( - path: UserStoragePathWithFeatureOnly, + path: UserStorageGenericPathWithFeatureOnly, encryptedData: [string, string][], ): Promise { try { @@ -242,12 +248,14 @@ export class UserStorage { } async #getUserStorage( - path: UserStoragePathWithFeatureAndKey, + path: UserStorageGenericPathWithFeatureAndKey, ): Promise { try { const headers = await this.#getAuthorizationHeader(); const storageKey = await this.getStorageKey(); - const encryptedPath = createEntryPath(path, storageKey); + const encryptedPath = createEntryPath(path, storageKey, { + validateAgainstSchema: false, + }); const url = new URL(STORAGE_URL(this.env, encryptedPath)); @@ -300,7 +308,7 @@ export class UserStorage { } async #getUserStorageAllFeatureEntries( - path: UserStoragePathWithFeatureOnly, + path: UserStorageGenericPathWithFeatureOnly, ): Promise { try { const headers = await this.#getAuthorizationHeader(); @@ -383,12 +391,14 @@ export class UserStorage { } async #deleteUserStorage( - path: UserStoragePathWithFeatureAndKey, + path: UserStorageGenericPathWithFeatureAndKey, ): Promise { try { const headers = await this.#getAuthorizationHeader(); const storageKey = await this.getStorageKey(); - const encryptedPath = createEntryPath(path, storageKey); + const encryptedPath = createEntryPath(path, storageKey, { + validateAgainstSchema: false, + }); const url = new URL(STORAGE_URL(this.env, encryptedPath)); @@ -428,7 +438,7 @@ export class UserStorage { } async #deleteUserStorageAllFeatureEntries( - path: UserStoragePathWithFeatureOnly, + path: UserStorageGenericPathWithFeatureOnly, ): Promise { try { const headers = await this.#getAuthorizationHeader(); @@ -469,7 +479,7 @@ export class UserStorage { } async #batchDeleteUserStorage( - path: UserStoragePathWithFeatureOnly, + path: UserStorageGenericPathWithFeatureOnly, data: string[], ): Promise { try { diff --git a/packages/profile-sync-controller/src/shared/storage-schema.test.ts b/packages/profile-sync-controller/src/shared/storage-schema.test.ts index 95e096779e5..e5bcd4459f0 100644 --- a/packages/profile-sync-controller/src/shared/storage-schema.test.ts +++ b/packages/profile-sync-controller/src/shared/storage-schema.test.ts @@ -49,6 +49,15 @@ describe('user-storage/schema.ts', () => { ); }); + it('should not throw errors if validateAgainstSchema is false', () => { + const path = 'invalid.feature'; + expect(() => + getFeatureAndKeyFromPath(path, { + validateAgainstSchema: false, + }), + ).not.toThrow(); + }); + it('should return feature and key from path', () => { const result = getFeatureAndKeyFromPath( `${USER_STORAGE_FEATURE_NAMES.notifications}.notification_settings`, @@ -68,5 +77,15 @@ describe('user-storage/schema.ts', () => { key: '0x123', }); }); + + it('should return feature and key from path with arbitrary feature and key when validateAgainstSchema is false', () => { + const result = getFeatureAndKeyFromPath('feature.key', { + validateAgainstSchema: false, + }); + expect(result).toStrictEqual({ + feature: 'feature', + key: 'key', + }); + }); }); }); diff --git a/packages/profile-sync-controller/src/shared/storage-schema.ts b/packages/profile-sync-controller/src/shared/storage-schema.ts index 5ebc2a2c732..51fe4b3f8fc 100644 --- a/packages/profile-sync-controller/src/shared/storage-schema.ts +++ b/packages/profile-sync-controller/src/shared/storage-schema.ts @@ -41,9 +41,36 @@ export type UserStoragePathWithFeatureAndKey = { [K in UserStorageFeatureNames]: `${K}.${UserStorageFeatureKeys}`; }[UserStoragePathWithFeatureOnly]; -export const getFeatureAndKeyFromPath = ( - path: UserStoragePathWithFeatureAndKey, -): UserStorageFeatureAndKey => { +/** + * The below types are mainly used for the SDK. + * These exist so that the SDK can be used with arbitrary feature names and keys. + * + * We only type enforce feature names and keys when using UserStorageController. + * This is done so we don't end up with magic strings within the applications. + */ + +export type UserStorageGenericFeatureName = string; +export type UserStorageGenericFeatureKey = string; +export type UserStorageGenericPathWithFeatureAndKey = + `${UserStorageGenericFeatureName}.${UserStorageGenericFeatureKey}`; +export type UserStorageGenericPathWithFeatureOnly = + UserStorageGenericFeatureName; + +type UserStorageGenericFeatureAndKey = { + feature: UserStorageGenericFeatureName; + key: UserStorageGenericFeatureKey; +}; + +export const getFeatureAndKeyFromPath = ( + path: T extends true + ? UserStoragePathWithFeatureAndKey + : UserStorageGenericPathWithFeatureAndKey, + options: { + validateAgainstSchema: T; + } = { validateAgainstSchema: true as T }, +): T extends true + ? UserStorageFeatureAndKey + : UserStorageGenericFeatureAndKey => { const pathRegex = /^\w+\.\w+$/u; if (!pathRegex.test(path)) { @@ -52,29 +79,41 @@ export const getFeatureAndKeyFromPath = ( ); } - const [feature, key] = path.split('.') as [ - UserStorageFeatureNames, - UserStorageFeatureKeys, - ]; - - if (!(feature in USER_STORAGE_SCHEMA)) { - throw new Error(`user-storage - invalid feature provided: ${feature}`); - } - - const validFeature = USER_STORAGE_SCHEMA[feature] as readonly string[]; - - if ( - !validFeature.includes(key) && - !validFeature.includes(ALLOW_ARBITRARY_KEYS) - ) { - const validKeys = USER_STORAGE_SCHEMA[feature].join(', '); - - throw new Error( - `user-storage - invalid key provided for this feature: ${key}. Valid keys: ${validKeys}`, - ); + const [feature, key] = path.split('.'); + + if (options.validateAgainstSchema) { + const featureToValidate = feature as UserStorageFeatureNames; + const keyToValidate = key as UserStorageFeatureKeys< + typeof featureToValidate + >; + + if (!(featureToValidate in USER_STORAGE_SCHEMA)) { + throw new Error( + `user-storage - invalid feature provided: ${featureToValidate}. Valid features: ${Object.keys( + USER_STORAGE_SCHEMA, + ).join(', ')}`, + ); + } + + const validFeature = USER_STORAGE_SCHEMA[ + featureToValidate + ] as readonly string[]; + + if ( + !validFeature.includes(keyToValidate) && + !validFeature.includes(ALLOW_ARBITRARY_KEYS) + ) { + const validKeys = USER_STORAGE_SCHEMA[featureToValidate].join(', '); + + throw new Error( + `user-storage - invalid key provided for this feature: ${keyToValidate}. Valid keys: ${validKeys}`, + ); + } } - return { feature, key }; + return { feature, key } as T extends true + ? UserStorageFeatureAndKey + : UserStorageGenericFeatureAndKey; }; export const isPathWithFeatureAndKey = ( @@ -92,13 +131,21 @@ export const isPathWithFeatureAndKey = ( * * @param path - string in the form of `${feature}.${key}` that matches schema * @param storageKey - users storage key + * @param options - options object + * @param options.validateAgainstSchema - whether to validate the path against the schema. + * This defaults to true, and should only be set to false when using the SDK with arbitrary feature names and keys. * @returns path to store entry */ -export function createEntryPath( - path: UserStoragePathWithFeatureAndKey, +export function createEntryPath( + path: T extends true + ? UserStoragePathWithFeatureAndKey + : UserStorageGenericPathWithFeatureAndKey, storageKey: string, + options: { + validateAgainstSchema: T; + } = { validateAgainstSchema: true as T }, ): string { - const { feature, key } = getFeatureAndKeyFromPath(path); + const { feature, key } = getFeatureAndKeyFromPath(path, options); const hashedKey = createSHA256Hash(key + storageKey); return `${feature}/${hashedKey}`; From 1e5b3407a233b58bbfdf3bcb3e283a74bf57f13f Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Fri, 14 Feb 2025 07:43:49 -0800 Subject: [PATCH 17/20] Release 299.0.0 (#5318) ## Explanation This is a controller release that includes the first version of the `MultichainNetworkController`, which also include `AccountController` updates. The other version bumps are related to updating their peer dependency version of accounts controller. ## References Related to: [#804](https://github.com/MetaMask/accounts-planning/issues/804) ## Changelog ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Charly Chevalier Co-authored-by: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> --- package.json | 2 +- packages/accounts-controller/CHANGELOG.md | 14 +++++- packages/accounts-controller/package.json | 2 +- packages/assets-controllers/CHANGELOG.md | 14 +++++- packages/assets-controllers/package.json | 6 +-- packages/bridge-controller/CHANGELOG.md | 5 +++ packages/bridge-controller/package.json | 8 ++-- packages/earn-controller/CHANGELOG.md | 9 +++- packages/earn-controller/package.json | 6 +-- .../CHANGELOG.md | 11 ++++- .../package.json | 4 +- .../CHANGELOG.md | 9 +++- .../package.json | 6 +-- .../CHANGELOG.md | 14 +++++- .../package.json | 6 +-- packages/profile-sync-controller/CHANGELOG.md | 19 +++++++- packages/profile-sync-controller/package.json | 6 +-- packages/transaction-controller/CHANGELOG.md | 9 +++- packages/transaction-controller/package.json | 6 +-- .../user-operation-controller/CHANGELOG.md | 9 +++- .../user-operation-controller/package.json | 6 +-- yarn.lock | 44 +++++++++---------- 22 files changed, 155 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index db5e7384657..dbfee920a13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "298.0.0", + "version": "299.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index a37b6fa4c12..63cffecab42 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [24.0.0] + +### Added + +- **BREAKING:** Now requires `MultichainNetworkController:didNetworkChange` event to be registered on the messenger ([#5215](https://github.com/MetaMask/core/pull/5215)) + - This will be used to keep accounts in sync with EVM and non-EVM network changes. + +### Changed + +- **BREAKING:** Add `@metamask/network-controller@^22.0.0` peer dependency ([#5215](https://github.com/MetaMask/core/pull/5215)), ([#5327](https://github.com/MetaMask/core/pull/5327)) + ## [23.1.0] ### Added @@ -444,7 +455,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@23.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@24.0.0...HEAD +[24.0.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@23.1.0...@metamask/accounts-controller@24.0.0 [23.1.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@23.0.1...@metamask/accounts-controller@23.1.0 [23.0.1]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@23.0.0...@metamask/accounts-controller@23.0.1 [23.0.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@22.0.0...@metamask/accounts-controller@23.0.0 diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 0c6c3f7ac29..2b7de49c311 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/accounts-controller", - "version": "23.1.0", + "version": "24.0.0", "description": "Manages internal accounts", "keywords": [ "MetaMask", diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 33abb6376c4..a9dd141f502 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [50.0.0] + +### Changed + +- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency from `^23.0.1` to `^24.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) +- Removed legacy poll function to prevent redundant polling ([#5321](https://github.com/MetaMask/core/pull/5321)) + +### Fixed + +- Ensure that the polling is not triggered on the constructor with the initialisation of the controller ([#5321](https://github.com/MetaMask/core/pull/5321)) + ## [49.0.0] ### Added @@ -1394,7 +1405,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@49.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@50.0.0...HEAD +[50.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@49.0.0...@metamask/assets-controllers@50.0.0 [49.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@48.0.0...@metamask/assets-controllers@49.0.0 [48.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@47.0.0...@metamask/assets-controllers@48.0.0 [47.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@46.0.1...@metamask/assets-controllers@47.0.0 diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index fb14e6e63b2..ea8b3cd4ba8 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/assets-controllers", - "version": "49.0.0", + "version": "50.0.0", "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", "keywords": [ "MetaMask", @@ -77,7 +77,7 @@ }, "devDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^23.1.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/approval-controller": "^7.1.3", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", @@ -105,7 +105,7 @@ "webextension-polyfill": "^0.12.0" }, "peerDependencies": { - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^24.0.0", "@metamask/approval-controller": "^7.0.0", "@metamask/keyring-controller": "^19.0.0", "@metamask/network-controller": "^22.0.0", diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 8fcf72c699c..8869e80b3be 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -11,4 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release +### Changed + +- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency from `^23.0.0` to `^24.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) +- **BREAKING:** Bump `@metamask/transaction-controller` peer dependency from `^45.0.0` to `^46.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) + [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 7b4a0b7571a..4f7c837d54a 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -55,12 +55,12 @@ "ethers": "^6.12.0" }, "devDependencies": { - "@metamask/accounts-controller": "^23.1.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/json-rpc-engine": "^10.0.3", "@metamask/network-controller": "^22.2.1", - "@metamask/transaction-controller": "^45.1.0", + "@metamask/transaction-controller": "^46.0.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -73,9 +73,9 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^23.0.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/network-controller": "^22.0.0", - "@metamask/transaction-controller": "^45.0.0" + "@metamask/transaction-controller": "^46.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/earn-controller/CHANGELOG.md b/packages/earn-controller/CHANGELOG.md index 05433fad43b..c7952557ca0 100644 --- a/packages/earn-controller/CHANGELOG.md +++ b/packages/earn-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.0] + +### Changed + +- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency from `^23.0.0` to `^24.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) + ## [0.2.1] ### Changed @@ -26,7 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#5271](https://github.com/MetaMask/core/pull/5271)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/earn-controller@0.2.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/earn-controller@0.3.0...HEAD +[0.3.0]: https://github.com/MetaMask/core/compare/@metamask/earn-controller@0.2.1...@metamask/earn-controller@0.3.0 [0.2.1]: https://github.com/MetaMask/core/compare/@metamask/earn-controller@0.2.0...@metamask/earn-controller@0.2.1 [0.2.0]: https://github.com/MetaMask/core/compare/@metamask/earn-controller@0.1.0...@metamask/earn-controller@0.2.0 [0.1.0]: https://github.com/MetaMask/core/releases/tag/@metamask/earn-controller@0.1.0 diff --git a/packages/earn-controller/package.json b/packages/earn-controller/package.json index 0acc8812145..46ffb282b29 100644 --- a/packages/earn-controller/package.json +++ b/packages/earn-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/earn-controller", - "version": "0.2.1", + "version": "0.3.0", "description": "Manages state for earning features and coordinates interactions between staking services, SDK integrations, and other controllers to enable users to participate in various earning opportunities", "keywords": [ "MetaMask", @@ -53,7 +53,7 @@ "@metamask/stake-sdk": "^1.0.0" }, "devDependencies": { - "@metamask/accounts-controller": "^23.1.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/network-controller": "^22.2.1", "@types/jest": "^27.4.1", @@ -65,7 +65,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^23.0.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/network-controller": "^22.1.1" }, "engines": { diff --git a/packages/multichain-network-controller/CHANGELOG.md b/packages/multichain-network-controller/CHANGELOG.md index b518709c7b8..8ee176414d5 100644 --- a/packages/multichain-network-controller/CHANGELOG.md +++ b/packages/multichain-network-controller/CHANGELOG.md @@ -7,4 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -[Unreleased]: https://github.com/MetaMask/core/ +## [0.1.0] + +### Added + +- Initial release ([#5215](https://github.com/MetaMask/core/pull/5215)) + - Handle both EVM and non-EVM network and account switching for the associated network. + - Act as a proxy for the `NetworkController` (for EVM network changes). + +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/multichain-network-controller@0.1.0...HEAD +[0.1.0]: https://github.com/MetaMask/core/releases/tag/@metamask/multichain-network-controller@0.1.0 diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index 6fd4275f479..cec39a5848e 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-network-controller", - "version": "0.0.0", + "version": "0.1.0", "description": "Multichain network controller", "keywords": [ "MetaMask", @@ -68,7 +68,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^23.0.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/network-controller": "^22.0.0" }, "engines": { diff --git a/packages/multichain-transactions-controller/CHANGELOG.md b/packages/multichain-transactions-controller/CHANGELOG.md index dc86afe2dfa..55ca0ede115 100644 --- a/packages/multichain-transactions-controller/CHANGELOG.md +++ b/packages/multichain-transactions-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.0] + +### Changed + +- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency from `^23.0.0` to `^24.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) + ## [0.3.0] ### Changed @@ -46,7 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#5133](https://github.com/MetaMask/core/pull/5133)), ([#5177](https://github.com/MetaMask/core/pull/5177)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/multichain-transactions-controller@0.3.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/multichain-transactions-controller@0.4.0...HEAD +[0.4.0]: https://github.com/MetaMask/core/compare/@metamask/multichain-transactions-controller@0.3.0...@metamask/multichain-transactions-controller@0.4.0 [0.3.0]: https://github.com/MetaMask/core/compare/@metamask/multichain-transactions-controller@0.2.0...@metamask/multichain-transactions-controller@0.3.0 [0.2.0]: https://github.com/MetaMask/core/compare/@metamask/multichain-transactions-controller@0.1.0...@metamask/multichain-transactions-controller@0.2.0 [0.1.0]: https://github.com/MetaMask/core/compare/@metamask/multichain-transactions-controller@0.0.1...@metamask/multichain-transactions-controller@0.1.0 diff --git a/packages/multichain-transactions-controller/package.json b/packages/multichain-transactions-controller/package.json index 13702a68a90..e5cc49b35f4 100644 --- a/packages/multichain-transactions-controller/package.json +++ b/packages/multichain-transactions-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-transactions-controller", - "version": "0.3.0", + "version": "0.4.0", "description": "This package is responsible for getting transactions from our Bitcoin and Solana snaps", "keywords": [ "MetaMask", @@ -61,7 +61,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/accounts-controller": "^23.1.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.1.0", "@types/jest": "^27.4.1", @@ -73,7 +73,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^23.0.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/snaps-controllers": "^9.19.0" }, "engines": { diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 9b7cde4a8cf..49511638232 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.21.0] + +### Added + +- Lock conditional checks when initializing accounts inside the `NotificationServicesController` ([#5323](https://github.com/MetaMask/core/pull/5323)) +- Accounts initialize call when the wallet is unlocked ([#5323](https://github.com/MetaMask/core/pull/5323)) + +### Changed + +- **BREAKING:** Bump `@metamask/profile-sync-controller` peer dependency from `^7.0.0` to `^8.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) + ## [0.20.1] ### Changed @@ -308,7 +319,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.20.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.21.0...HEAD +[0.21.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.20.1...@metamask/notification-services-controller@0.21.0 [0.20.1]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.20.0...@metamask/notification-services-controller@0.20.1 [0.20.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.19.0...@metamask/notification-services-controller@0.20.0 [0.19.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.18.0...@metamask/notification-services-controller@0.19.0 diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index 54af5356df3..e544c35cd02 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/notification-services-controller", - "version": "0.20.1", + "version": "0.21.0", "description": "Manages New MetaMask decentralized Notification system", "keywords": [ "MetaMask", @@ -113,7 +113,7 @@ "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.1.0", - "@metamask/profile-sync-controller": "^7.0.1", + "@metamask/profile-sync-controller": "^8.0.0", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", "contentful": "^10.15.0", @@ -128,7 +128,7 @@ }, "peerDependencies": { "@metamask/keyring-controller": "^19.0.0", - "@metamask/profile-sync-controller": "^7.0.0" + "@metamask/profile-sync-controller": "^8.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index 99529b9da1d..869971917d9 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.0] + +### Added + +- Add `perform{BatchSetStorage,DeleteStorage,BatchDeleteStorage}` as messenger actions ([#5311](https://github.com/MetaMask/core/pull/5311)) +- Add optional `validateAgainstSchema` option when creating user storage entry paths ([#5326](https://github.com/MetaMask/core/pull/5326)) + +### Changed + +- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency from `^23.0.0` to `^24.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) +- Change `maxNumberOfAccountsToAdd` default value from `100` to `Infinity` ([#5322](https://github.com/MetaMask/core/pull/5322)) + +### Removed + +- Removed unused events from `UserStorageController` ([#5324](https://github.com/MetaMask/core/pull/5324)) + ## [7.0.1] ### Changed @@ -467,7 +483,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/profile-sync-controller@7.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@8.0.0...HEAD +[8.0.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@7.0.1...@metamask/profile-sync-controller@8.0.0 [7.0.1]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@7.0.0...@metamask/profile-sync-controller@7.0.1 [7.0.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@6.0.0...@metamask/profile-sync-controller@7.0.0 [6.0.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@5.0.0...@metamask/profile-sync-controller@6.0.0 diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index 8e3a1a2d4c2..4d528d68989 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/profile-sync-controller", - "version": "7.0.1", + "version": "8.0.0", "description": "The profile sync helps developers synchronize data across multiple clients and devices in a privacy-preserving way. All data saved in the user storage database is encrypted client-side to preserve privacy. The user storage provides a modular design, giving developers the flexibility to construct and manage their storage spaces in a way that best suits their needs", "keywords": [ "MetaMask", @@ -115,7 +115,7 @@ "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/preinstall-always-fail": "^2.1.0", - "@metamask/accounts-controller": "^23.1.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-internal-api": "^4.0.1", "@metamask/providers": "^18.1.1", @@ -133,7 +133,7 @@ "webextension-polyfill": "^0.12.0" }, "peerDependencies": { - "@metamask/accounts-controller": "^23.0.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/keyring-controller": "^19.0.0", "@metamask/network-controller": "^22.0.0", "@metamask/providers": "^18.1.0", diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 01428f01bce..af30d1018a4 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [46.0.0] + ### Added - Adds ability of re-simulating transaction depending on the `isActive` property on `transactionMeta` ([#5189](https://github.com/MetaMask/core/pull/5189)) @@ -14,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Re-simulation of transactions will occur every 3 seconds if `isActive` is `true`. - Adds `setTransactionActive` function to update the `isActive` property on `transactionMeta`. ([#5189](https://github.com/MetaMask/core/pull/5189)) +### Changed + +- **BREAKING:** Bump `@metamask/accounts-controller` peer dependency from `^23.0.0` to `^24.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) + ## [45.1.0] ### Added @@ -1284,7 +1290,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@45.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@46.0.0...HEAD +[46.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@45.1.0...@metamask/transaction-controller@46.0.0 [45.1.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@45.0.0...@metamask/transaction-controller@45.1.0 [45.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@44.1.0...@metamask/transaction-controller@45.0.0 [44.1.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@44.0.0...@metamask/transaction-controller@44.1.0 diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index bf16e5a4b8d..40057299f7b 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/transaction-controller", - "version": "45.1.0", + "version": "46.0.0", "description": "Stores transactions alongside their periodically updated statuses and manages interactions such as approval and cancellation", "keywords": [ "MetaMask", @@ -69,7 +69,7 @@ }, "devDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^23.1.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/approval-controller": "^7.1.3", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-block-tracker": "^11.0.3", @@ -92,7 +92,7 @@ }, "peerDependencies": { "@babel/runtime": "^7.0.0", - "@metamask/accounts-controller": "^23.0.0", + "@metamask/accounts-controller": "^24.0.0", "@metamask/approval-controller": "^7.0.0", "@metamask/eth-block-tracker": ">=9", "@metamask/gas-fee-controller": "^22.0.0", diff --git a/packages/user-operation-controller/CHANGELOG.md b/packages/user-operation-controller/CHANGELOG.md index 835ac62878e..e8700bbb7ac 100644 --- a/packages/user-operation-controller/CHANGELOG.md +++ b/packages/user-operation-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [25.0.0] + +### Changed + +- **BREAKING:** Bump `@metamask/transaction-controller` peer dependency from `^45.0.0` to `^46.0.0` ([#5318](https://github.com/MetaMask/core/pull/5318)) + ## [24.0.1] ### Changed @@ -335,7 +341,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial Release ([#3749](https://github.com/MetaMask/core/pull/3749)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@24.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@25.0.0...HEAD +[25.0.0]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@24.0.1...@metamask/user-operation-controller@25.0.0 [24.0.1]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@24.0.0...@metamask/user-operation-controller@24.0.1 [24.0.0]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@23.0.0...@metamask/user-operation-controller@24.0.0 [23.0.0]: https://github.com/MetaMask/core/compare/@metamask/user-operation-controller@22.0.0...@metamask/user-operation-controller@23.0.0 diff --git a/packages/user-operation-controller/package.json b/packages/user-operation-controller/package.json index f1750769049..8c5fba9fd11 100644 --- a/packages/user-operation-controller/package.json +++ b/packages/user-operation-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/user-operation-controller", - "version": "24.0.1", + "version": "25.0.0", "description": "Creates user operations and manages their life cycle", "keywords": [ "MetaMask", @@ -67,7 +67,7 @@ "@metamask/gas-fee-controller": "^22.0.3", "@metamask/keyring-controller": "^19.1.0", "@metamask/network-controller": "^22.2.1", - "@metamask/transaction-controller": "^45.1.0", + "@metamask/transaction-controller": "^46.0.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -82,7 +82,7 @@ "@metamask/gas-fee-controller": "^22.0.0", "@metamask/keyring-controller": "^19.0.0", "@metamask/network-controller": "^22.0.0", - "@metamask/transaction-controller": "^45.0.0" + "@metamask/transaction-controller": "^46.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/yarn.lock b/yarn.lock index 5ad3020e25f..48b2f1eb5e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2340,7 +2340,7 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^23.1.0, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^24.0.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: @@ -2462,7 +2462,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/accounts-controller": "npm:^23.1.0" + "@metamask/accounts-controller": "npm:^24.0.0" "@metamask/approval-controller": "npm:^7.1.3" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" @@ -2509,7 +2509,7 @@ __metadata: uuid: "npm:^8.3.2" webextension-polyfill: "npm:^0.12.0" peerDependencies: - "@metamask/accounts-controller": ^23.0.1 + "@metamask/accounts-controller": ^24.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^19.0.0 "@metamask/network-controller": ^22.0.0 @@ -2586,7 +2586,7 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/bridge-controller@workspace:packages/bridge-controller" dependencies: - "@metamask/accounts-controller": "npm:^23.1.0" + "@metamask/accounts-controller": "npm:^24.0.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" @@ -2595,7 +2595,7 @@ __metadata: "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/network-controller": "npm:^22.2.1" "@metamask/polling-controller": "npm:^12.0.3" - "@metamask/transaction-controller": "npm:^45.1.0" + "@metamask/transaction-controller": "npm:^46.0.0" "@metamask/utils": "npm:^11.1.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2609,9 +2609,9 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/accounts-controller": ^23.0.0 + "@metamask/accounts-controller": ^24.0.0 "@metamask/network-controller": ^22.0.0 - "@metamask/transaction-controller": ^45.0.0 + "@metamask/transaction-controller": ^46.0.0 languageName: unknown linkType: soft @@ -2785,7 +2785,7 @@ __metadata: resolution: "@metamask/earn-controller@workspace:packages/earn-controller" dependencies: "@ethersproject/providers": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^23.1.0" + "@metamask/accounts-controller": "npm:^24.0.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" @@ -2799,7 +2799,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/accounts-controller": ^23.0.0 + "@metamask/accounts-controller": ^24.0.0 "@metamask/network-controller": ^22.1.1 languageName: unknown linkType: soft @@ -3508,7 +3508,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/accounts-controller": ^23.0.0 + "@metamask/accounts-controller": ^24.0.0 "@metamask/network-controller": ^22.0.0 languageName: unknown linkType: soft @@ -3517,7 +3517,7 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller" dependencies: - "@metamask/accounts-controller": "npm:^23.1.0" + "@metamask/accounts-controller": "npm:^24.0.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/keyring-api": "npm:^17.0.0" @@ -3540,7 +3540,7 @@ __metadata: typescript: "npm:~5.2.2" uuid: "npm:^8.3.2" peerDependencies: - "@metamask/accounts-controller": ^23.0.0 + "@metamask/accounts-controller": ^24.0.0 "@metamask/snaps-controllers": ^9.19.0 languageName: unknown linkType: soft @@ -3660,7 +3660,7 @@ __metadata: "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" "@metamask/keyring-controller": "npm:^19.1.0" - "@metamask/profile-sync-controller": "npm:^7.0.1" + "@metamask/profile-sync-controller": "npm:^8.0.0" "@metamask/utils": "npm:^11.1.0" "@types/jest": "npm:^27.4.1" "@types/readable-stream": "npm:^2.3.0" @@ -3679,7 +3679,7 @@ __metadata: uuid: "npm:^8.3.2" peerDependencies: "@metamask/keyring-controller": ^19.0.0 - "@metamask/profile-sync-controller": ^7.0.0 + "@metamask/profile-sync-controller": ^8.0.0 languageName: unknown linkType: soft @@ -3841,13 +3841,13 @@ __metadata: languageName: unknown linkType: soft -"@metamask/profile-sync-controller@npm:^7.0.1, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": +"@metamask/profile-sync-controller@npm:^8.0.0, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": version: 0.0.0-use.local resolution: "@metamask/profile-sync-controller@workspace:packages/profile-sync-controller" dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" - "@metamask/accounts-controller": "npm:^23.1.0" + "@metamask/accounts-controller": "npm:^24.0.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/keyring-api": "npm:^17.0.0" @@ -3875,7 +3875,7 @@ __metadata: typescript: "npm:~5.2.2" webextension-polyfill: "npm:^0.12.0" peerDependencies: - "@metamask/accounts-controller": ^23.0.0 + "@metamask/accounts-controller": ^24.0.0 "@metamask/keyring-controller": ^19.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/providers": ^18.1.0 @@ -4220,7 +4220,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/transaction-controller@npm:^45.1.0, @metamask/transaction-controller@workspace:packages/transaction-controller": +"@metamask/transaction-controller@npm:^46.0.0, @metamask/transaction-controller@workspace:packages/transaction-controller": version: 0.0.0-use.local resolution: "@metamask/transaction-controller@workspace:packages/transaction-controller" dependencies: @@ -4231,7 +4231,7 @@ __metadata: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^23.1.0" + "@metamask/accounts-controller": "npm:^24.0.0" "@metamask/approval-controller": "npm:^7.1.3" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" @@ -4266,7 +4266,7 @@ __metadata: uuid: "npm:^8.3.2" peerDependencies: "@babel/runtime": ^7.0.0 - "@metamask/accounts-controller": ^23.0.0 + "@metamask/accounts-controller": ^24.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/eth-block-tracker": ">=9" "@metamask/gas-fee-controller": ^22.0.0 @@ -4290,7 +4290,7 @@ __metadata: "@metamask/polling-controller": "npm:^12.0.3" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^45.1.0" + "@metamask/transaction-controller": "npm:^46.0.0" "@metamask/utils": "npm:^11.1.0" "@types/jest": "npm:^27.4.1" bn.js: "npm:^5.2.1" @@ -4309,7 +4309,7 @@ __metadata: "@metamask/gas-fee-controller": ^22.0.0 "@metamask/keyring-controller": ^19.0.0 "@metamask/network-controller": ^22.0.0 - "@metamask/transaction-controller": ^45.0.0 + "@metamask/transaction-controller": ^46.0.0 languageName: unknown linkType: soft From f8c8a90a9b29a8b2966ac5324d220e044a1b30b9 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 14 Feb 2025 09:05:35 -0700 Subject: [PATCH 18/20] Create RPC middleware using RPC services (#5290) When creating the middleware stack for a network, NetworkController makes use of functions from two packages for sending requests: - `createInfuraMiddleware` from `@metamask/eth-json-rpc-infura` for Infura networks - `createFetchMiddleware` from `@metamask/eth-json-rpc-middleware` for custom RPC endpoints Currently, each of these middleware implements similar heuristics for determining when the network is down and retrying requests appropriately. We want to improve upon this logic so that we incorporate the circuit breaker pattern, exponential backoff, and jitter for retries, and, in the case of Infura, we can fail over to an alternate RPC endpoint when it is down. To make this happen, we have implemented an RpcService class which extracts all of this logic into one place. As a result, we can vastly simplify the Infura and fetch middleware creation functions so that they only need to take an RPC service and the RPC service will handle the rest. To accommodate these changes, this commit: - Upgrades the `@metamask/eth-json-rpc-infura` and `@metamask/eth-json-rpc-middleware` packages to allow an RPC service to be passed. - Adds two new required options to NetworkController, `fetch` and `btoa`. These are also requirements of RpcService and allows consumers to pass in whatever versions of these functions is specific to their platform. - Updates `createAutoManagedNetworkClient` to take `fetch` and `btoa` and pass them along to `createNetworkClient`. - Updates `createNetworkClient` to create an RPC service and pass it along to the two middleware creation functions. Consequently, these functions now handle requests in exactly the same way. --- eslint-warning-thresholds.json | 15 +- .../src/AssetsContractController.test.ts | 2 + .../src/GasFeeController.test.ts | 2 + packages/network-controller/CHANGELOG.md | 32 +- packages/network-controller/package.json | 4 +- .../src/NetworkController.ts | 69 +- ...create-auto-managed-network-client.test.ts | 72 +- .../src/create-auto-managed-network-client.ts | 43 +- .../src/create-network-client.ts | 59 +- .../tests/NetworkController.test.ts | 1758 +++++++++++------ .../tests/provider-api-tests/block-param.ts | 1251 ++++-------- .../tests/provider-api-tests/helpers.ts | 35 +- .../provider-api-tests/no-block-param.ts | 760 +++---- .../tests/provider-api-tests/shared-tests.ts | 29 - ...troller-integration.update-network.test.ts | 2 + .../TransactionControllerIntegration.test.ts | 2 + yarn.lock | 64 +- 17 files changed, 2040 insertions(+), 2159 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index ae34e6428f5..9a3e6c309d3 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -416,33 +416,20 @@ "jsdoc/tag-lines": 1, "prettier/prettier": 1 }, - "packages/network-controller/src/create-auto-managed-network-client.test.ts": { - "import-x/order": 1 - }, - "packages/network-controller/src/create-network-client.ts": { - "@typescript-eslint/no-unsafe-enum-comparison": 1 - }, "packages/network-controller/tests/NetworkController.test.ts": { "@typescript-eslint/no-unused-vars": 1, "@typescript-eslint/prefer-promise-reject-errors": 1, - "import-x/order": 1, - "jest/no-conditional-in-test": 4 + "import-x/order": 1 }, "packages/network-controller/tests/create-network-client.test.ts": { "import-x/order": 1 }, - "packages/network-controller/tests/provider-api-tests/block-param.ts": { - "jest/no-conditional-in-test": 1 - }, "packages/network-controller/tests/provider-api-tests/helpers.ts": { "@typescript-eslint/prefer-promise-reject-errors": 1, "import-x/namespace": 1, "import-x/no-named-as-default-member": 1, "promise/catch-or-return": 1 }, - "packages/network-controller/tests/provider-api-tests/no-block-param.ts": { - "jest/no-conditional-in-test": 2 - }, "packages/permission-controller/src/Permission.ts": { "prettier/prettier": 11 }, diff --git a/packages/assets-controllers/src/AssetsContractController.test.ts b/packages/assets-controllers/src/AssetsContractController.test.ts index 5910d7a51fd..d2bbb4220a9 100644 --- a/packages/assets-controllers/src/AssetsContractController.test.ts +++ b/packages/assets-controllers/src/AssetsContractController.test.ts @@ -91,6 +91,8 @@ async function setupAssetContractControllers({ allowedActions: [], allowedEvents: [], }), + fetch, + btoa, }); if (useNetworkControllerProvider) { await networkController.initializeProvider(); diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index bed99788db3..d2ad90b6241 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -80,6 +80,8 @@ const setupNetworkController = async ({ messenger: restrictedMessenger, state, infuraProjectId: '123', + fetch, + btoa, }); if (initializeProvider) { diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 6cedf221574..77dc401045b 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,11 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Implement circuit breaker pattern when retrying requests to Infura and custom RPC endpoints ([#5290](https://github.com/MetaMask/core/pull/5290)) + - If the network is perceived to be down after 5 attempts, further retries will be paused for 30 seconds + - "Down" means the following: + - A failure to reach the network (exact error depending on platform / HTTP client) + - The request responds with a non-JSON-parseable or non-JSON-RPC-compatible body + - The request returns a non-200 response +- Use exponential backoff / jitter when retrying requests to Infura and custom RPC endpoints ([#5290](https://github.com/MetaMask/core/pull/5290)) + - As requests are retried, the delay between retries will increase exponentially (using random variance to prevent bursts) + +### Changed + +- **BREAKING:** `NetworkController` constructor now takes two required options, `fetch` and `btoa` ([#5290](https://github.com/MetaMask/core/pull/5290)) + - These are passed along to functions that create the JSON-RPC middleware +- Synchronize retry logic and error handling behavior between Infura and custom RPC endpoints ([#5290](https://github.com/MetaMask/core/pull/5290)) + - A request to a custom endpoint that returns a 418 response will no longer return a JSON-RPC response with the error "Request is being rate limited" + - A request to a custom endpoint that returns a 429 response now returns a JSON-RPC response with the error "Request is being rate limited" + - A request to a custom endpoint that throws an "ECONNRESET" error will now be retried up to 5 times + - A request to a Infura endpoint that fails more than 5 times in a row will now respond with a JSON-RPC error that encompasses the failure instead of hiding it as "InfuraProvider - cannot complete request. All retries exhausted" + - A request to a Infura endpoint that returns a non-retriable, non-2xx response will now respond with a JSON-RPC error that has the underling message "Non-200 status code: '\'" rather than including the raw response from the endpoint + - A request to a custom endpoint that fails with a retriable error more than 5 times in a row will now respond with a JSON-RPC error that encompasses the failure instead of returning an empty response + - A "retriable error" is now regarded as the following: + - A failure to reach the network (exact error depending on platform / HTTP client) + - The request responds with a non-JSON-parseable or non-JSON-RPC-compatible body + - The request returns a 503 or 504 response +- Bump dependencies to support usage of RPC services internally for network requests ([#5290](https://github.com/MetaMask/core/pull/5290)) + - Bump `@metamask/eth-json-rpc-infura` to `^10.1.0` + - Bump `@metamask/eth-json-rpc-middleware` to `^15.1.0` + ## [22.2.1] ### Changed -- Bump `@metamask/base-controller` from `^7.1.1` to `^8.0.0` [#5305](https://github.com/MetaMask/core/pull/5305)) +- Bump `@metamask/base-controller` from `^7.1.1` to `^8.0.0` ([#5305](https://github.com/MetaMask/core/pull/5305)) ## [22.2.0] diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 3f8449348e6..c80f31db7f8 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -50,8 +50,8 @@ "@metamask/base-controller": "^8.0.0", "@metamask/controller-utils": "^11.5.0", "@metamask/eth-block-tracker": "^11.0.3", - "@metamask/eth-json-rpc-infura": "^10.0.0", - "@metamask/eth-json-rpc-middleware": "^15.0.1", + "@metamask/eth-json-rpc-infura": "^10.1.0", + "@metamask/eth-json-rpc-middleware": "^15.1.0", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/eth-query": "^4.0.0", "@metamask/json-rpc-engine": "^10.0.3", diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 731b427e36b..73ac9d96631 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -531,6 +531,15 @@ export type NetworkControllerOptions = { infuraProjectId: string; state?: Partial; log?: Logger; + /** + * A function that can be used to make an HTTP request, compatible with the + * Fetch API. + */ + fetch: typeof fetch; + /** + * A function that can be used to convert a binary string into base-64. + */ + btoa: typeof btoa; }; /** @@ -909,6 +918,10 @@ export class NetworkController extends BaseController< #log: Logger | undefined; + readonly #fetch: typeof fetch; + + readonly #btoa: typeof btoa; + #networkConfigurationsByNetworkClientId: Map< NetworkClientId, NetworkConfiguration @@ -919,6 +932,8 @@ export class NetworkController extends BaseController< state, infuraProjectId, log, + fetch: givenFetch, + btoa: givenBtoa, }: NetworkControllerOptions) { const initialState = { ...getDefaultNetworkControllerState(), ...state }; validateNetworkControllerState(initialState); @@ -948,6 +963,8 @@ export class NetworkController extends BaseController< this.#infuraProjectId = infuraProjectId; this.#log = log; + this.#fetch = givenFetch; + this.#btoa = givenBtoa; this.#previouslySelectedNetworkClientId = this.state.selectedNetworkClientId; @@ -2425,20 +2442,28 @@ export class NetworkController extends BaseController< autoManagedNetworkClientRegistry[NetworkClientType.Infura][ addedRpcEndpoint.networkClientId ] = createAutoManagedNetworkClient({ - type: NetworkClientType.Infura, - chainId: networkFields.chainId, - network: addedRpcEndpoint.networkClientId, - infuraProjectId: this.#infuraProjectId, - ticker: networkFields.nativeCurrency, + networkClientConfiguration: { + type: NetworkClientType.Infura, + chainId: networkFields.chainId, + network: addedRpcEndpoint.networkClientId, + infuraProjectId: this.#infuraProjectId, + ticker: networkFields.nativeCurrency, + }, + fetch: this.#fetch, + btoa: this.#btoa, }); } else { autoManagedNetworkClientRegistry[NetworkClientType.Custom][ addedRpcEndpoint.networkClientId ] = createAutoManagedNetworkClient({ - type: NetworkClientType.Custom, - chainId: networkFields.chainId, - rpcUrl: addedRpcEndpoint.url, - ticker: networkFields.nativeCurrency, + networkClientConfiguration: { + type: NetworkClientType.Custom, + chainId: networkFields.chainId, + rpcUrl: addedRpcEndpoint.url, + ticker: networkFields.nativeCurrency, + }, + fetch: this.#fetch, + btoa: this.#btoa, }); } } @@ -2589,21 +2614,29 @@ export class NetworkController extends BaseController< return [ rpcEndpoint.networkClientId, createAutoManagedNetworkClient({ - type: NetworkClientType.Infura, - network: infuraNetworkName, - infuraProjectId: this.#infuraProjectId, - chainId: networkConfiguration.chainId, - ticker: networkConfiguration.nativeCurrency, + networkClientConfiguration: { + type: NetworkClientType.Infura, + network: infuraNetworkName, + infuraProjectId: this.#infuraProjectId, + chainId: networkConfiguration.chainId, + ticker: networkConfiguration.nativeCurrency, + }, + fetch: this.#fetch, + btoa: this.#btoa, }), ] as const; } return [ rpcEndpoint.networkClientId, createAutoManagedNetworkClient({ - type: NetworkClientType.Custom, - chainId: networkConfiguration.chainId, - rpcUrl: rpcEndpoint.url, - ticker: networkConfiguration.nativeCurrency, + networkClientConfiguration: { + type: NetworkClientType.Custom, + chainId: networkConfiguration.chainId, + rpcUrl: rpcEndpoint.url, + ticker: networkConfiguration.nativeCurrency, + }, + fetch: this.#fetch, + btoa: this.#btoa, }), ] as const; }); diff --git a/packages/network-controller/src/create-auto-managed-network-client.test.ts b/packages/network-controller/src/create-auto-managed-network-client.test.ts index 421c720ff5c..52eb5339255 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.test.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.test.ts @@ -1,6 +1,5 @@ import { BUILT_IN_NETWORKS, NetworkType } from '@metamask/controller-utils'; -import { mockNetwork } from '../../../tests/mock-network'; import { createAutoManagedNetworkClient } from './create-auto-managed-network-client'; import * as createNetworkClientModule from './create-network-client'; import type { @@ -8,6 +7,7 @@ import type { InfuraNetworkClientConfiguration, } from './types'; import { NetworkClientType } from './types'; +import { mockNetwork } from '../../../tests/mock-network'; describe('createAutoManagedNetworkClient', () => { const networkClientConfigurations: [ @@ -31,9 +31,11 @@ describe('createAutoManagedNetworkClient', () => { for (const networkClientConfiguration of networkClientConfigurations) { describe(`given configuration for a ${networkClientConfiguration.type} network client`, () => { it('allows the network client configuration to be accessed', () => { - const { configuration } = createAutoManagedNetworkClient( + const { configuration } = createAutoManagedNetworkClient({ networkClientConfiguration, - ); + fetch, + btoa, + }); expect(configuration).toStrictEqual(networkClientConfiguration); }); @@ -41,14 +43,20 @@ describe('createAutoManagedNetworkClient', () => { it('does not make any network requests initially', () => { // If unexpected requests occurred, then Nock would throw expect(() => { - createAutoManagedNetworkClient(networkClientConfiguration); + createAutoManagedNetworkClient({ + networkClientConfiguration, + fetch, + btoa, + }); }).not.toThrow(); }); it('returns a provider proxy that has the same interface as a provider', () => { - const { provider } = createAutoManagedNetworkClient( + const { provider } = createAutoManagedNetworkClient({ networkClientConfiguration, - ); + fetch, + btoa, + }); // This also tests the `has` trap in the proxy expect('addListener' in provider).toBe(true); @@ -87,9 +95,11 @@ describe('createAutoManagedNetworkClient', () => { ], }); - const { provider } = createAutoManagedNetworkClient( + const { provider } = createAutoManagedNetworkClient({ networkClientConfiguration, - ); + fetch, + btoa, + }); const result = await provider.request({ id: 1, @@ -121,9 +131,11 @@ describe('createAutoManagedNetworkClient', () => { 'createNetworkClient', ); - const { provider } = createAutoManagedNetworkClient( + const { provider } = createAutoManagedNetworkClient({ networkClientConfiguration, - ); + fetch, + btoa, + }); await provider.request({ id: 1, @@ -138,15 +150,19 @@ describe('createAutoManagedNetworkClient', () => { params: [], }); expect(createNetworkClientMock).toHaveBeenCalledTimes(1); - expect(createNetworkClientMock).toHaveBeenCalledWith( - networkClientConfiguration, - ); + expect(createNetworkClientMock).toHaveBeenCalledWith({ + configuration: networkClientConfiguration, + fetch, + btoa, + }); }); it('returns a block tracker proxy that has the same interface as a block tracker', () => { - const { blockTracker } = createAutoManagedNetworkClient( + const { blockTracker } = createAutoManagedNetworkClient({ networkClientConfiguration, - ); + fetch, + btoa, + }); // This also tests the `has` trap in the proxy expect('addListener' in blockTracker).toBe(true); @@ -196,9 +212,11 @@ describe('createAutoManagedNetworkClient', () => { ], }); - const { blockTracker } = createAutoManagedNetworkClient( + const { blockTracker } = createAutoManagedNetworkClient({ networkClientConfiguration, - ); + fetch, + btoa, + }); const blockNumberViaLatest = await new Promise((resolve) => { blockTracker.once('latest', resolve); @@ -251,9 +269,11 @@ describe('createAutoManagedNetworkClient', () => { 'createNetworkClient', ); - const { blockTracker } = createAutoManagedNetworkClient( + const { blockTracker } = createAutoManagedNetworkClient({ networkClientConfiguration, - ); + fetch, + btoa, + }); await new Promise((resolve) => { blockTracker.once('latest', resolve); @@ -264,9 +284,11 @@ describe('createAutoManagedNetworkClient', () => { await blockTracker.getLatestBlock(); await blockTracker.checkForLatestBlock(); expect(createNetworkClientMock).toHaveBeenCalledTimes(1); - expect(createNetworkClientMock).toHaveBeenCalledWith( - networkClientConfiguration, - ); + expect(createNetworkClientMock).toHaveBeenCalledWith({ + configuration: networkClientConfiguration, + fetch, + btoa, + }); }); it('allows the block tracker to be destroyed', () => { @@ -284,9 +306,11 @@ describe('createAutoManagedNetworkClient', () => { }, ], }); - const { blockTracker, destroy } = createAutoManagedNetworkClient( + const { blockTracker, destroy } = createAutoManagedNetworkClient({ networkClientConfiguration, - ); + fetch, + btoa, + }); // Start the block tracker blockTracker.on('latest', () => { // do nothing diff --git a/packages/network-controller/src/create-auto-managed-network-client.ts b/packages/network-controller/src/create-auto-managed-network-client.ts index 543c6582815..d310c561d99 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.ts @@ -59,15 +59,26 @@ const UNINITIALIZED_TARGET = { __UNINITIALIZED__: true }; * part of the network client is serving as the receiver. The network client is * then cached for subsequent usages. * - * @param networkClientConfiguration - The configuration object that will be + * @param args - The arguments. + * @param args.networkClientConfiguration - The configuration object that will be * used to instantiate the network client when it is needed. + * @param args.fetch - A function that can be used to make an HTTP request, + * compatible with the Fetch API. + * @param args.btoa - A function that can be used to convert a binary string + * into base-64. * @returns The auto-managed network client. */ export function createAutoManagedNetworkClient< Configuration extends NetworkClientConfiguration, ->( - networkClientConfiguration: Configuration, -): AutoManagedNetworkClient { +>({ + networkClientConfiguration, + fetch: givenFetch, + btoa: givenBtoa, +}: { + networkClientConfiguration: Configuration; + fetch: typeof fetch; + btoa: typeof btoa; +}): AutoManagedNetworkClient { let networkClient: NetworkClient | undefined; const providerProxy = new Proxy(UNINITIALIZED_TARGET, { @@ -78,7 +89,11 @@ export function createAutoManagedNetworkClient< return networkClient?.provider; } - networkClient ??= createNetworkClient(networkClientConfiguration); + networkClient ??= createNetworkClient({ + configuration: networkClientConfiguration, + fetch: givenFetch, + btoa: givenBtoa, + }); if (networkClient === undefined) { throw new Error( "It looks like `createNetworkClient` didn't return anything. Perhaps it's being mocked?", @@ -115,7 +130,11 @@ export function createAutoManagedNetworkClient< if (propertyName === REFLECTIVE_PROPERTY_NAME) { return true; } - networkClient ??= createNetworkClient(networkClientConfiguration); + networkClient ??= createNetworkClient({ + configuration: networkClientConfiguration, + fetch: givenFetch, + btoa: givenBtoa, + }); const { provider } = networkClient; return propertyName in provider; }, @@ -131,7 +150,11 @@ export function createAutoManagedNetworkClient< return networkClient?.blockTracker; } - networkClient ??= createNetworkClient(networkClientConfiguration); + networkClient ??= createNetworkClient({ + configuration: networkClientConfiguration, + fetch: givenFetch, + btoa: givenBtoa, + }); if (networkClient === undefined) { throw new Error( "It looks like createNetworkClient returned undefined. Perhaps it's mocked?", @@ -168,7 +191,11 @@ export function createAutoManagedNetworkClient< if (propertyName === REFLECTIVE_PROPERTY_NAME) { return true; } - networkClient ??= createNetworkClient(networkClientConfiguration); + networkClient ??= createNetworkClient({ + configuration: networkClientConfiguration, + fetch: givenFetch, + btoa: givenBtoa, + }); const { blockTracker } = networkClient; return propertyName in blockTracker; }, diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index e6620184878..2944dceb496 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -25,6 +25,7 @@ import { import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; import type { Hex, Json, JsonRpcParams } from '@metamask/utils'; +import { RpcService } from './rpc-service/rpc-service'; import type { BlockTracker, NetworkClientConfiguration, @@ -48,30 +49,50 @@ export type NetworkClient = { /** * Create a JSON RPC network client for a specific network. * - * @param networkConfig - The network configuration. + * @param args - The arguments. + * @param args.configuration - The network configuration. + * @param args.fetch - A function that can be used to make an HTTP request, + * compatible with the Fetch API. + * @param args.btoa - A function that can be used to convert a binary string + * into base-64. * @returns The network client. */ -export function createNetworkClient( - networkConfig: NetworkClientConfiguration, -): NetworkClient { +export function createNetworkClient({ + configuration, + fetch: givenFetch, + btoa: givenBtoa, +}: { + configuration: NetworkClientConfiguration; + fetch: typeof fetch; + btoa: typeof btoa; +}): NetworkClient { + const rpcService = + configuration.type === NetworkClientType.Infura + ? new RpcService({ + fetch: givenFetch, + btoa: givenBtoa, + endpointUrl: `https://${configuration.network}.infura.io/v3/${configuration.infuraProjectId}`, + }) + : new RpcService({ + fetch: givenFetch, + btoa: givenBtoa, + endpointUrl: configuration.rpcUrl, + }); + const rpcApiMiddleware = - networkConfig.type === NetworkClientType.Infura + configuration.type === NetworkClientType.Infura ? createInfuraMiddleware({ - network: networkConfig.network, - projectId: networkConfig.infuraProjectId, - maxAttempts: 5, - source: 'metamask', + rpcService, + options: { + source: 'metamask', + }, }) - : createFetchMiddleware({ - btoa: global.btoa, - fetch: global.fetch, - rpcUrl: networkConfig.rpcUrl, - }); + : createFetchMiddleware({ rpcService }); const rpcProvider = providerFromMiddleware(rpcApiMiddleware); const blockTrackerOpts = - process.env.IN_TEST && networkConfig.type === 'custom' + process.env.IN_TEST && configuration.type === NetworkClientType.Custom ? { pollingInterval: SECOND } : {}; const blockTracker = new PollingBlockTracker({ @@ -80,16 +101,16 @@ export function createNetworkClient( }); const networkMiddleware = - networkConfig.type === NetworkClientType.Infura + configuration.type === NetworkClientType.Infura ? createInfuraNetworkMiddleware({ blockTracker, - network: networkConfig.network, + network: configuration.network, rpcProvider, rpcApiMiddleware, }) : createCustomNetworkMiddleware({ blockTracker, - chainId: networkConfig.chainId, + chainId: configuration.chainId, rpcApiMiddleware, }); @@ -105,7 +126,7 @@ export function createNetworkClient( blockTracker.destroy(); }; - return { configuration: networkConfig, provider, blockTracker, destroy }; + return { configuration, provider, blockTracker, destroy }; } /** diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 77d08dff3ec..e11448fc2d9 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1,3 +1,6 @@ +// A lot of the tests in this file have conditionals. +/* eslint-disable jest/no-conditional-in-test */ + import { Messenger } from '@metamask/base-controller'; import { BUILT_IN_NETWORKS, @@ -165,6 +168,8 @@ describe('NetworkController', () => { networkConfigurationsByChainId: {}, }, infuraProjectId: 'infura-project-id', + fetch, + btoa, }), ).toThrow( 'NetworkController state is invalid: `networkConfigurationsByChainId` cannot be empty', @@ -187,6 +192,8 @@ describe('NetworkController', () => { }, }, infuraProjectId: 'infura-project-id', + fetch, + btoa, }), ).toThrow( "NetworkController state has invalid `networkConfigurationsByChainId`: Network configuration 'Test Network' is filed under '0x1337' which does not match its `chainId` of '0x1338'", @@ -216,6 +223,8 @@ describe('NetworkController', () => { }, }, infuraProjectId: 'infura-project-id', + fetch, + btoa, }), ).toThrow( "NetworkController state has invalid `networkConfigurationsByChainId`: Network configuration 'Test Network' has a `defaultBlockExplorerUrlIndex` that does not refer to an entry in `blockExplorerUrls`", @@ -244,6 +253,8 @@ describe('NetworkController', () => { }, }, infuraProjectId: 'infura-project-id', + fetch, + btoa, }), ).toThrow( "NetworkController state has invalid `networkConfigurationsByChainId`: Network configuration 'Test Network' has a `defaultBlockExplorerUrlIndex` that does not refer to an entry in `blockExplorerUrls`", @@ -272,6 +283,8 @@ describe('NetworkController', () => { }, }, infuraProjectId: 'infura-project-id', + fetch, + btoa, }), ).toThrow( "NetworkController state has invalid `networkConfigurationsByChainId`: Network configuration 'Test Network' has a `defaultRpcEndpointIndex` that does not refer to an entry in `rpcEndpoints`", @@ -310,6 +323,8 @@ describe('NetworkController', () => { }, }, infuraProjectId: 'infura-project-id', + fetch, + btoa, }), ).toThrow( 'NetworkController state has invalid `networkConfigurationsByChainId`: Every RPC endpoint across all network configurations must have a unique `networkClientId`', @@ -332,6 +347,8 @@ describe('NetworkController', () => { }, }, infuraProjectId: 'infura-project-id', + fetch, + btoa, }), ).toThrow( "NetworkController state is invalid: `selectedNetworkClientId` 'nonexistent' does not refer to an RPC endpoint within a network configuration", @@ -590,11 +607,15 @@ describe('NetworkController', () => { const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClient); @@ -660,10 +681,14 @@ describe('NetworkController', () => { const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClient); @@ -797,18 +822,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -890,18 +923,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker[InfuraNetworkType.goerli], - type: NetworkClientType.Infura, + configuration: { + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker[InfuraNetworkType.goerli], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -1343,18 +1384,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId[infuraNetworkType], - infuraProjectId, - network: infuraNetworkType, - ticker: NetworksTicker[infuraNetworkType], - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId[infuraNetworkType], + infuraProjectId, + network: infuraNetworkType, + ticker: NetworksTicker[infuraNetworkType], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -1441,18 +1490,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId[infuraNetworkType], - infuraProjectId, - network: infuraNetworkType, - ticker: NetworksTicker[infuraNetworkType], - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId[infuraNetworkType], + infuraProjectId, + network: infuraNetworkType, + ticker: NetworksTicker[infuraNetworkType], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -1534,18 +1591,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId[infuraNetworkType], - infuraProjectId, - network: infuraNetworkType, - ticker: NetworksTicker[infuraNetworkType], - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId[infuraNetworkType], + infuraProjectId, + network: infuraNetworkType, + ticker: NetworksTicker[infuraNetworkType], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -1611,11 +1676,15 @@ describe('NetworkController', () => { const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient() .calledWith({ - chainId: ChainId[infuraNetworkType], - infuraProjectId, - network: infuraNetworkType, - ticker: NetworksTicker[infuraNetworkType], - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId[infuraNetworkType], + infuraProjectId, + network: infuraNetworkType, + ticker: NetworksTicker[infuraNetworkType], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClient); await controller.initializeProvider(); @@ -1659,11 +1728,15 @@ describe('NetworkController', () => { const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient() .calledWith({ - chainId: ChainId[infuraNetworkType], - infuraProjectId, - network: infuraNetworkType, - ticker: NetworksTicker[infuraNetworkType], - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId[infuraNetworkType], + infuraProjectId, + network: infuraNetworkType, + ticker: NetworksTicker[infuraNetworkType], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClient); await controller.initializeProvider(); @@ -1763,18 +1836,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: ChainId[InfuraNetworkType.goerli], - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker[InfuraNetworkType.goerli], - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId[InfuraNetworkType.goerli], + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker[InfuraNetworkType.goerli], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -1862,18 +1943,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: ChainId[InfuraNetworkType.goerli], - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker[InfuraNetworkType.goerli], - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId[InfuraNetworkType.goerli], + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker[InfuraNetworkType.goerli], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -1960,18 +2049,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: ChainId[InfuraNetworkType.goerli], - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker[InfuraNetworkType.goerli], - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId[InfuraNetworkType.goerli], + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker[InfuraNetworkType.goerli], + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -2038,10 +2135,14 @@ describe('NetworkController', () => { const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClient); await controller.initializeProvider(); @@ -2097,10 +2198,14 @@ describe('NetworkController', () => { const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClient); await controller.initializeProvider(); @@ -3584,29 +3689,41 @@ describe('NetworkController', () => { expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( 2, { - infuraProjectId, - chainId: infuraChainId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + networkClientConfiguration: { + infuraProjectId, + chainId: infuraChainId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( 3, { - chainId: infuraChainId, - rpcUrl: 'https://test.endpoint/2', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: infuraChainId, + rpcUrl: 'https://test.endpoint/2', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( 4, { - chainId: infuraChainId, - rpcUrl: 'https://test.endpoint/3', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: infuraChainId, + rpcUrl: 'https://test.endpoint/3', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }, ); expect( @@ -4937,11 +5054,15 @@ describe('NetworkController', () => { expect( createAutoManagedNetworkClientSpy, ).toHaveBeenNthCalledWith(3, { - chainId: infuraChainId, - infuraProjectId: 'some-infura-project-id', - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + networkClientConfiguration: { + chainId: infuraChainId, + infuraProjectId: 'some-infura-project-id', + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }); expect( @@ -5160,18 +5281,26 @@ describe('NetworkController', () => { expect( createAutoManagedNetworkClientSpy, ).toHaveBeenNthCalledWith(3, { - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint/1', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint/1', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect( createAutoManagedNetworkClientSpy, ).toHaveBeenNthCalledWith(4, { - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint/2', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint/2', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect( @@ -5551,17 +5680,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/1', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/1', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/2', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/2', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -5655,17 +5792,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/1', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/1', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/2', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/2', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -5778,24 +5923,36 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/1', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/1', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/2', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/2', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/3', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/3', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[2]); await controller.initializeProvider(); @@ -5904,24 +6061,36 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/1', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/1', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/2', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/2', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.network/3', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.network/3', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[2]); await controller.initializeProvider(); @@ -6009,10 +6178,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://some.other.url', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://some.other.url', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); const existingNetworkClient = controller.getNetworkClientById( @@ -6080,10 +6253,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://some.other.url', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://some.other.url', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -6099,10 +6276,14 @@ describe('NetworkController', () => { }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: infuraChainId, - rpcUrl: 'https://some.other.url', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: infuraChainId, + rpcUrl: 'https://some.other.url', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect( getNetworkConfigurationsByNetworkClientId( @@ -6155,10 +6336,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://some.other.url', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://some.other.url', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -6228,10 +6413,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://some.other.url', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://some.other.url', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -6315,17 +6504,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://some.other.url', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://some.other.url', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -6413,17 +6610,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://some.other.url', - ticker: infuraNativeTokenName, - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://some.other.url', + ticker: infuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -6947,16 +7152,24 @@ describe('NetworkController', () => { }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint/2', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint/2', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint/3', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint/3', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect( @@ -7335,17 +7548,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/2', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/2', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -7439,17 +7660,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/2', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/2', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -7561,24 +7790,36 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/2', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/2', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/3', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/3', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[2]); await controller.initializeProvider(); @@ -7687,24 +7928,36 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/2', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/2', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network/3', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network/3', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[2]); await controller.initializeProvider(); @@ -7789,10 +8042,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://some.other.url', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://some.other.url', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); const existingNetworkClient = controller.getNetworkClientById( @@ -7860,10 +8117,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://some.other.url', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://some.other.url', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -7879,10 +8140,14 @@ describe('NetworkController', () => { }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: '0x1337', - rpcUrl: 'https://some.other.url', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: '0x1337', + rpcUrl: 'https://some.other.url', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect( getNetworkConfigurationsByNetworkClientId( @@ -7935,10 +8200,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://some.other.url', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://some.other.url', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -8006,10 +8275,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://some.other.url', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://some.other.url', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -8094,17 +8367,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://some.other.url', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://some.other.url', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -8194,17 +8475,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://some.other.url', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://some.other.url', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -8794,12 +9083,16 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, - }) - .mockReturnValue(buildFakeClient()); + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, + }) + .mockReturnValue(buildFakeClient()); await controller.updateNetwork('0x1337', { ...networkConfigurationToUpdate, @@ -8874,10 +9167,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); const existingNetworkClient1 = controller.getNetworkClientById( @@ -8961,10 +9258,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -8974,16 +9275,24 @@ describe('NetworkController', () => { }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: infuraChainId, + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.endpoint/2', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: infuraChainId, + rpcUrl: 'https://test.endpoint/2', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect( @@ -9050,10 +9359,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -9135,17 +9448,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -9229,17 +9550,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -9416,10 +9745,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -9508,10 +9841,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); const existingNetworkClient1 = controller.getNetworkClientById( @@ -9609,10 +9946,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -9629,16 +9970,24 @@ describe('NetworkController', () => { ); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: '0x1337', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.endpoint/2', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: '0x1337', + rpcUrl: 'https://test.endpoint/2', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect( @@ -9711,10 +10060,14 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -9805,17 +10158,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -9899,17 +10260,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -10088,11 +10457,15 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: anotherInfuraChainId, - infuraProjectId: 'some-infura-project-id', - network: anotherInfuraNetworkType, - ticker: anotherInfuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: anotherInfuraChainId, + infuraProjectId: 'some-infura-project-id', + network: anotherInfuraNetworkType, + ticker: anotherInfuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -10191,11 +10564,15 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: anotherInfuraChainId, - infuraProjectId: 'some-infura-project-id', - network: anotherInfuraNetworkType, - ticker: anotherInfuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: anotherInfuraChainId, + infuraProjectId: 'some-infura-project-id', + network: anotherInfuraNetworkType, + ticker: anotherInfuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); const existingNetworkClient1 = controller.getNetworkClientById( @@ -10294,11 +10671,15 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: anotherInfuraChainId, - infuraProjectId: 'some-infura-project-id', - network: anotherInfuraNetworkType, - ticker: anotherInfuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: anotherInfuraChainId, + infuraProjectId: 'some-infura-project-id', + network: anotherInfuraNetworkType, + ticker: anotherInfuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -10315,16 +10696,24 @@ describe('NetworkController', () => { }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: anotherInfuraChainId, - rpcUrl: 'https://test.endpoint/1', - ticker: anotherInfuraNativeTokenName, - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: anotherInfuraChainId, + rpcUrl: 'https://test.endpoint/1', + ticker: anotherInfuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: anotherInfuraChainId, - rpcUrl: 'https://test.endpoint/2', - ticker: anotherInfuraNativeTokenName, - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: anotherInfuraChainId, + rpcUrl: 'https://test.endpoint/2', + ticker: anotherInfuraNativeTokenName, + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect( @@ -10398,11 +10787,15 @@ describe('NetworkController', () => { async ({ controller }) => { mockCreateNetworkClient() .calledWith({ - chainId: anotherInfuraChainId, - infuraProjectId: 'some-infura-project-id', - network: anotherInfuraNetworkType, - ticker: anotherInfuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: anotherInfuraChainId, + infuraProjectId: 'some-infura-project-id', + network: anotherInfuraNetworkType, + ticker: anotherInfuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(buildFakeClient()); @@ -10497,17 +10890,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: anotherInfuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: anotherInfuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -10591,17 +10992,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: infuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: infuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: anotherInfuraChainId, - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: anotherInfuraChainId, + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -10793,10 +11202,14 @@ describe('NetworkController', () => { const fakeNetworkClients = [buildFakeClient(fakeProviders[0])]; mockCreateNetworkClient() .calledWith({ - chainId: '0x2448', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x2448', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]); @@ -10884,10 +11297,14 @@ describe('NetworkController', () => { const fakeNetworkClients = [buildFakeClient(fakeProviders[0])]; mockCreateNetworkClient() .calledWith({ - chainId: '0x2448', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x2448', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]); const existingNetworkClient1 = controller.getNetworkClientById( @@ -10979,10 +11396,14 @@ describe('NetworkController', () => { const fakeNetworkClients = [buildFakeClient(fakeProviders[0])]; mockCreateNetworkClient() .calledWith({ - chainId: '0x2448', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x2448', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]); @@ -10992,17 +11413,25 @@ describe('NetworkController', () => { }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: '0x2448', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + networkClientConfiguration: { + chainId: '0x2448', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ - chainId: '0x2448', - rpcUrl: 'https://test.endpoint/2', - ticker: 'TOKEN', - type: NetworkClientType.Custom, - }); + networkClientConfiguration: { + chainId: '0x2448', + rpcUrl: 'https://test.endpoint/2', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, + }); expect( getNetworkConfigurationsByNetworkClientId( @@ -11081,10 +11510,14 @@ describe('NetworkController', () => { const fakeNetworkClients = [buildFakeClient(fakeProviders[0])]; mockCreateNetworkClient() .calledWith({ - chainId: '0x2448', - rpcUrl: 'https://test.endpoint/1', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x2448', + rpcUrl: 'https://test.endpoint/1', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]); @@ -11167,17 +11600,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x2448', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x2448', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -11260,17 +11701,25 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x2448', - rpcUrl: 'https://rpc.endpoint', - ticker: 'TOKEN', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x2448', + rpcUrl: 'https://rpc.endpoint', + ticker: 'TOKEN', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); @@ -12059,18 +12508,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork('AAAA-AAAA-AAAA-AAAA'); @@ -12129,18 +12586,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork('AAAA-AAAA-AAAA-AAAA'); @@ -12216,18 +12681,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork('AAAA-AAAA-AAAA-AAAA'); @@ -12278,18 +12751,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork('AAAA-AAAA-AAAA-AAAA'); @@ -12349,18 +12830,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork('AAAA-AAAA-AAAA-AAAA'); @@ -12432,18 +12921,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork('AAAA-AAAA-AAAA-AAAA'); @@ -12518,18 +13015,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: infuraChainId, - infuraProjectId, - network: infuraNetworkType, - ticker: infuraNativeTokenName, - type: NetworkClientType.Infura, + configuration: { + chainId: infuraChainId, + infuraProjectId, + network: infuraNetworkType, + ticker: infuraNativeTokenName, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork('AAAA-AAAA-AAAA-AAAA'); @@ -12679,18 +13184,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId.goerli, - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker.goerli, - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId.goerli, + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker.goerli, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork(InfuraNetworkType.goerli); @@ -12749,18 +13262,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId.goerli, - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker.goerli, - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId.goerli, + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker.goerli, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork(InfuraNetworkType.goerli); @@ -12842,18 +13363,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId.goerli, - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker.goerli, - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId.goerli, + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker.goerli, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork(InfuraNetworkType.goerli); @@ -12905,18 +13434,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId.goerli, - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker.goerli, - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId.goerli, + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker.goerli, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork(InfuraNetworkType.goerli); @@ -12967,18 +13504,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId.goerli, - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker.goerli, - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId.goerli, + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker.goerli, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork(InfuraNetworkType.goerli); @@ -13046,18 +13591,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId.goerli, - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker.goerli, - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId.goerli, + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker.goerli, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork(InfuraNetworkType.goerli); @@ -13128,18 +13681,26 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - chainId: ChainId.goerli, - infuraProjectId, - network: InfuraNetworkType.goerli, - ticker: NetworksTicker.goerli, - type: NetworkClientType.Infura, + configuration: { + chainId: ChainId.goerli, + infuraProjectId, + network: InfuraNetworkType.goerli, + ticker: NetworksTicker.goerli, + type: NetworkClientType.Infura, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://test.network', - ticker: 'TEST', - type: NetworkClientType.Custom, + configuration: { + chainId: '0x1337', + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }, + fetch, + btoa, }) .mockReturnValue(fakeNetworkClients[1]); await controller.setActiveNetwork(InfuraNetworkType.goerli); @@ -13454,10 +14015,14 @@ function refreshNetworkTests({ await operation(controller); expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: expectedNetworkClientConfiguration.chainId, - rpcUrl: expectedNetworkClientConfiguration.rpcUrl, - type: NetworkClientType.Custom, - ticker: expectedNetworkClientConfiguration.ticker, + configuration: { + chainId: expectedNetworkClientConfiguration.chainId, + rpcUrl: expectedNetworkClientConfiguration.rpcUrl, + type: NetworkClientType.Custom, + ticker: expectedNetworkClientConfiguration.ticker, + }, + fetch, + btoa, }); const { provider } = controller.getProviderAndBlockTracker(); assert(provider); @@ -13496,8 +14061,12 @@ function refreshNetworkTests({ await operation(controller); expect(createNetworkClientMock).toHaveBeenCalledWith({ - ...expectedNetworkClientConfiguration, - infuraProjectId: 'infura-project-id', + configuration: { + ...expectedNetworkClientConfiguration, + infuraProjectId: 'infura-project-id', + }, + fetch, + btoa, }); const { provider } = controller.getProviderAndBlockTracker(); assert(provider); @@ -13528,7 +14097,7 @@ function refreshNetworkTests({ ]; const { selectedNetworkClientId } = controller.state; let initializationNetworkClientConfiguration: - | Parameters[0] + | Parameters[0]['configuration'] | undefined; for (const matchingNetworkConfiguration of Object.values( @@ -13567,7 +14136,7 @@ function refreshNetworkTests({ const operationNetworkClientConfiguration: Parameters< typeof createNetworkClient - >[0] = + >[0]['configuration'] = expectedNetworkClientConfiguration.type === NetworkClientType.Custom ? expectedNetworkClientConfiguration : { @@ -13575,9 +14144,17 @@ function refreshNetworkTests({ infuraProjectId: 'infura-project-id', }; mockCreateNetworkClient() - .calledWith(initializationNetworkClientConfiguration) + .calledWith({ + configuration: initializationNetworkClientConfiguration, + fetch, + btoa, + }) .mockReturnValue(fakeNetworkClients[0]) - .calledWith(operationNetworkClientConfiguration) + .calledWith({ + configuration: operationNetworkClientConfiguration, + fetch, + btoa, + }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); const { provider: providerBefore } = @@ -14433,6 +15010,8 @@ async function withController( const controller = new NetworkController({ messenger: restrictedMessenger, infuraProjectId: 'infura-project-id', + fetch, + btoa, ...rest, }); try { @@ -14475,7 +15054,8 @@ function buildFakeClient( * optionally provided for certain RPC methods. * * @param stubs - The list of RPC methods you want to stub along with their - * responses. `eth_getBlockByNumber` will be stubbed by default. + * responses. `eth_getBlockByNumber` and `eth_blockNumber will be stubbed by + * default. * @returns The object. */ function buildFakeProvider(stubs: FakeProviderStub[] = []): Provider { diff --git a/packages/network-controller/tests/provider-api-tests/block-param.ts b/packages/network-controller/tests/provider-api-tests/block-param.ts index 16e5cc22799..6cf6f7af20f 100644 --- a/packages/network-controller/tests/provider-api-tests/block-param.ts +++ b/packages/network-controller/tests/provider-api-tests/block-param.ts @@ -6,11 +6,6 @@ import { withMockedCommunications, withNetworkClient, } from './helpers'; -import { - buildFetchFailedErrorMessage, - buildInfuraClientRetriesExhaustedErrorMessage, - buildJsonRpcEngineEmptyResponseErrorMessage, -} from './shared-tests'; type TestsForRpcMethodSupportingBlockParam = { providerType: ProviderType; @@ -385,167 +380,45 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - // There is a difference in how we are testing the Infura middleware vs. the - // custom RPC middleware (or, more specifically, the fetch middleware) - // because of what both middleware treat as rate limiting errors. In this - // case, the fetch middleware treats a 418 response from the RPC endpoint as - // such an error, whereas to the Infura middleware, it is a 429 response. - if (providerType === 'infura') { - it('throws a generic, undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - id: 123, - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - '{"id":123,"jsonrpc":"2.0"}', - ); - }); - }); - - it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited', - ); - }); - }); - } else { - it('throws an error with a custom message if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); + it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited.', - ); + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 429, + }, }); - }); - - it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); - await expect(promiseForResult).rejects.toThrow( - "Non-200 status code: '429'", - ); - }); + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited', + ); }); - } + }); - it('throws an undescriptive error message if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { + it('throws an undescriptive error message if the request to the RPC endpoint returns a response that is not 405, 429, 503, or 504', async () => { await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; @@ -577,11 +450,9 @@ export function testsForRpcMethodSupportingBlockParam( async ({ makeRpcCall }) => makeRpcCall(request), ); - const msg = - providerType === 'infura' - ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' - : "Non-200 status code: '420'"; - await expect(promiseForResult).rejects.toThrow(msg); + await expect(promiseForResult).rejects.toThrow( + "Non-200 status code: '420'", + ); }); }); @@ -643,99 +514,47 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - // Both the Infura middleware and custom RPC middleware detect a 503 or - // 504 response and retry the request to the RPC endpoint automatically - // but differ in what sort of response is returned when the number of - // retries is exhausted. - if (providerType === 'infura') { - it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - error: 'Some error', - httpStatus, - }, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout'), - ); - }); - }); - } else { - it(`produces an empty response if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; + it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - error: 'Some error', - httpStatus, - }, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + error: 'Some error', + httpStatus, + }, + times: 5, }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + await expect(promiseForResult).rejects.toThrow('Gateway timeout'); }); - } + }); }); it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { @@ -744,6 +563,10 @@ export function testsForRpcMethodSupportingBlockParam( method, params: buildMockParams({ blockParam, blockParamIndex }), }; + const error = new Error('Request timed out'); + // @ts-expect-error `code` does not exist on the Error type, but is + // still used by Node. + error.code = 'ETIMEDOUT'; // The first time a block-cacheable request is made, the // block-cache middleware will request the latest block number @@ -764,7 +587,7 @@ export function testsForRpcMethodSupportingBlockParam( blockParamIndex, '0x100', ), - error: 'ETIMEDOUT: Some message', + error, times: 4, }); comms.mockRpcCall({ @@ -793,669 +616,345 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - // Both the Infura and fetch middleware detect ETIMEDOUT errors and will - // automatically retry the request to the RPC endpoint in question, but each - // produces a different error if the number of retries is exhausted. - if (providerType === 'infura') { - it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; + it('re-throws a "ETIMEDOUT" error produced even after making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const error = new Error('Request timed out'); + // @ts-expect-error `code` does not exist on the Error type, but is + // still used by Node. + error.code = 'ETIMEDOUT'; - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error, + times: 5, }); - }); - } else { - it('produces an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); + await expect(promiseForResult).rejects.toThrow(error.message); }); - } - - // The Infura middleware treats a response that contains an ECONNRESET - // message as an innocuous error that is likely to disappear on a retry. The - // custom RPC middleware, on the other hand, does not specially handle this - // error. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'ECONNRESET: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); + }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const error = new Error('Connection reset'); + // @ts-expect-error `code` does not exist on the Error type, but is + // still used by Node. + error.code = 'ECONNRESET'; - expect(result).toBe('the result'); + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error, + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, }); - }); - - it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); + expect(result).toBe('the result'); }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; + }); - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); + it('re-throws a "ECONNRESET" error produced even after making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const error = new Error('Connection reset'); + // @ts-expect-error `code` does not exist on the Error type, but is + // still used by Node. + error.code = 'ECONNRESET'; - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error, + times: 5, + }); - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, ); }, ); + + await expect(promiseForResult).rejects.toThrow(error.message); }); - } + }); - // Both the Infura and fetch middleware will attempt to parse the response - // body as JSON, and if this step produces an error, both middleware will - // also attempt to retry the request. However, this error handling code is - // slightly different between the two. As the error in this case is a - // SyntaxError, the Infura middleware will catch it immediately, whereas the - // custom RPC middleware will catch it and re-throw a separate error, which - // it then catches later. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if a "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; + it('retries the request to the RPC endpoint up to 5 times if the request has an invalid JSON response, returning the successful result if it is valid JSON on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'SyntaxError: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toBe('the result'); + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + body: 'invalid JSON', + }, + times: 4, }); - }); - - it('causes a request to fail with a custom error if a "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, }); - }); - - it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'failed to parse response body: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); + expect(result).toBe('the result'); }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; + }); - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); + it('causes a request to fail with a custom error if the request to the RPC endpoint has an invalid JSON response 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + body: 'invalid JSON', + }, + times: 5, + }); - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, ); }, ); - }); - - it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'failed to parse response body: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - expect(result).toBe('the result'); - }); + await expect(promiseForResult).rejects.toThrow('not valid JSON'); }); + }); - it('produces an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + it('retries the request to the RPC endpoint up to 5 times if a connection error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const error = new TypeError('Failed to fetch'); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error, + times: 4, }); - }); - } - - // Only the custom RPC middleware will detect a "Failed to fetch" error and - // attempt to retry the request to the RPC endpoint; the Infura middleware - // does not. - if (providerType === 'infura') { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'Failed to fetch: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, }); - }); - } else { - it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'Failed to fetch: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); - expect(result).toBe('the result'); - }); + expect(result).toBe('the result'); }); + }); - it('produces an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + it('re-throws a connection error produced even after making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const error = new TypeError('Failed to fetch'); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error, + times: 5, }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow(error.message); }); - } + }); }); describe.each([ diff --git a/packages/network-controller/tests/provider-api-tests/helpers.ts b/packages/network-controller/tests/provider-api-tests/helpers.ts index 02a749d0b07..bba49cc8181 100644 --- a/packages/network-controller/tests/provider-api-tests/helpers.ts +++ b/packages/network-controller/tests/provider-api-tests/helpers.ts @@ -79,8 +79,7 @@ type Response = { result?: any; httpStatus?: number; }; -type ResponseBody = { body: JSONRPCResponse }; -type BodyOrResponse = ResponseBody | Response; +type BodyOrResponse = { body: JSONRPCResponse | string } | Response; type CurriedMockRpcCallOptions = { request: Request; // The response data. @@ -143,7 +142,7 @@ function mockRpcCall({ // for consistency with makeRpcCall, assume that the `body` contains it const { method, params = [], ...rest } = request; let httpStatus = 200; - let completeResponse: JSONRPCResponse = { id: 2, jsonrpc: '2.0' }; + let completeResponse: JSONRPCResponse | string = { id: 2, jsonrpc: '2.0' }; if (response !== undefined) { if ('body' in response) { completeResponse = response.body; @@ -195,6 +194,10 @@ function mockRpcCall({ // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any return nockRequest.reply(httpStatus, (_, requestBody: any) => { + if (typeof completeResponse === 'string') { + return completeResponse; + } + if (response !== undefined && !('body' in response)) { if (response.id === undefined) { completeResponse.id = requestBody.id; @@ -485,17 +488,25 @@ export async function withNetworkClient( const clientUnderTest = providerType === 'infura' ? createNetworkClient({ - network: infuraNetwork, - infuraProjectId: MOCK_INFURA_PROJECT_ID, - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[infuraNetwork].chainId, - ticker: BUILT_IN_NETWORKS[infuraNetwork].ticker, + configuration: { + network: infuraNetwork, + infuraProjectId: MOCK_INFURA_PROJECT_ID, + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[infuraNetwork].chainId, + ticker: BUILT_IN_NETWORKS[infuraNetwork].ticker, + }, + fetch, + btoa, }) : createNetworkClient({ - chainId: customChainId, - rpcUrl: customRpcUrl, - type: NetworkClientType.Custom, - ticker: customTicker, + configuration: { + chainId: customChainId, + rpcUrl: customRpcUrl, + type: NetworkClientType.Custom, + ticker: customTicker, + }, + fetch, + btoa, }); /* eslint-disable-next-line n/no-process-env */ process.env.IN_TEST = inTest; diff --git a/packages/network-controller/tests/provider-api-tests/no-block-param.ts b/packages/network-controller/tests/provider-api-tests/no-block-param.ts index 15f4cace4cc..de13c243e56 100644 --- a/packages/network-controller/tests/provider-api-tests/no-block-param.ts +++ b/packages/network-controller/tests/provider-api-tests/no-block-param.ts @@ -4,11 +4,6 @@ import { withMockedCommunications, withNetworkClient, } from './helpers'; -import { - buildFetchFailedErrorMessage, - buildInfuraClientRetriesExhaustedErrorMessage, - buildJsonRpcEngineEmptyResponseErrorMessage, -} from './shared-tests'; type TestsForRpcMethodAssumingNoBlockParamOptions = { providerType: ProviderType; @@ -265,114 +260,32 @@ export function testsForRpcMethodAssumingNoBlockParam( }); }); - // There is a difference in how we are testing the Infura middleware vs. the - // custom RPC middleware (or, more specifically, the fetch middleware) because - // of what both middleware treat as rate limiting errors. In this case, the - // fetch middleware treats a 418 response from the RPC endpoint as such an - // error, whereas to the Infura middleware, it is a 429 response. - if (providerType === 'infura') { - it('throws an undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { id: 123, method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - '{"id":123,"jsonrpc":"2.0"}', - ); - }); - }); - - it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited', - ); - }); - }); - } else { - it('throws a custom error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); + it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited.', - ); + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 429, + }, }); - }); - - it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); - await expect(promiseForResult).rejects.toThrow( - "Non-200 status code: '429'", - ); - }); + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited', + ); }); - } + }); - it('throws a generic, undescriptive error if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { + it('throws a generic, undescriptive error if the request to the RPC endpoint returns a response that is not 405, 429, 503, or 504', async () => { await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; @@ -394,11 +307,9 @@ export function testsForRpcMethodAssumingNoBlockParam( async ({ makeRpcCall }) => makeRpcCall(request), ); - const errorMessage = - providerType === 'infura' - ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' - : "Non-200 status code: '420'"; - await expect(promiseForResult).rejects.toThrow(errorMessage); + await expect(promiseForResult).rejects.toThrow( + "Non-200 status code: '420'", + ); }); }); @@ -468,11 +379,7 @@ export function testsForRpcMethodAssumingNoBlockParam( ); }, ); - const err = - providerType === 'infura' - ? buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout') - : buildJsonRpcEngineEmptyResponseErrorMessage(method); - await expect(promiseForResult).rejects.toThrow(err); + await expect(promiseForResult).rejects.toThrow('Gateway timeout'); }); }); }); @@ -480,6 +387,10 @@ export function testsForRpcMethodAssumingNoBlockParam( it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; + const error = new Error('Request timed out'); + // @ts-expect-error `code` does not exist on the Error type, but is + // still used by Node. + error.code = 'ETIMEDOUT'; // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't @@ -489,7 +400,7 @@ export function testsForRpcMethodAssumingNoBlockParam( // on the 5th try. comms.mockRpcCall({ request, - error: 'ETIMEDOUT: Some message', + error, times: 4, }); comms.mockRpcCall({ @@ -514,461 +425,240 @@ export function testsForRpcMethodAssumingNoBlockParam( }); }); - // Both the Infura and fetch middleware detect ETIMEDOUT errors and will - // automatically retry the request to the RPC endpoint in question, but both - // produce a different error if the number of retries is exhausted. - if (providerType === 'infura') { - it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + it('re-throws a "ETIMEDOUT" error produced even after making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const error = new Error('Request timed out'); + // @ts-expect-error `code` does not exist on the Error type, but is + // still used by Node. + error.code = 'ETIMEDOUT'; - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error, + times: 5, }); - }); - } else { - it('returns an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); + await expect(promiseForResult).rejects.toThrow(error.message); }); - } + }); - // The Infura middleware treats a response that contains an ECONNRESET message - // as an innocuous error that is likely to disappear on a retry. The custom - // RPC middleware, on the other hand, does not specially handle this error. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; + it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const error = new Error('Connection reset'); + // @ts-expect-error `code` does not exist on the Error type, but is + // still used by Node. + error.code = 'ECONNRESET'; - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'ECONNRESET: Some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error, + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); - expect(result).toBe('the result'); - }); + expect(result).toBe('the result'); }); + }); - it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + it('re-throws a "ECONNRESET" error produced even after making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const error = new Error('Connection reset'); + // @ts-expect-error `code` does not exist on the Error type, but is + // still used by Node. + error.code = 'ECONNRESET'; - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error, + times: 5, }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { method }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, ); }, ); + + await expect(promiseForResult).rejects.toThrow(error.message); }); - } - - // Both the Infura and fetch middleware will attempt to parse the response - // body as JSON, and if this step produces an error, both middleware will also - // attempt to retry the request. However, this error handling code is slightly - // different between the two. As the error in this case is a SyntaxError, the - // Infura middleware will catch it immediately, whereas the custom RPC - // middleware will catch it and re-throw a separate error, which it then - // catches later. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'SyntaxError: Some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); + }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + it('retries the request to the RPC endpoint up to 5 times if the request has an invalid JSON response, returning the successful result if it is valid JSON on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; - expect(result).toBe('the result'); + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + response: { + body: 'invalid JSON', + }, + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, }); - }); - - it('causes a request to fail with a custom error if an "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'SyntaxError: Some message'; - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); + expect(result).toBe('the result'); }); + }); - it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); + it('causes a request to fail with a custom error if the request to the RPC endpoint has an invalid JSON response 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + body: 'invalid JSON', + }, + times: 5, }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { method }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, ); }, ); - }); - - it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'failed to parse response body: some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toBe('the result'); - }); + await expect(promiseForResult).rejects.toThrow('not valid JSON'); }); + }); - it('returns an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + it('retries the request to the RPC endpoint up to 5 times if a connection error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const error = new TypeError('Failed to fetch'); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error, + times: 4, }); - }); - } - - // Only the custom RPC middleware will detect a "Failed to fetch" error and - // attempt to retry the request to the RPC endpoint; the Infura middleware - // does not. - if (providerType === 'infura') { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, }); - }); - } else { - it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'Failed to fetch: some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); - expect(result).toBe('the result'); - }); + expect(result).toBe('the result'); }); + }); - it('returns an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); + it('re-throws a connection error produced even after making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const error = new TypeError('Failed to fetch'); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error, + times: 5, }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow(error.message); }); - } + }); } diff --git a/packages/network-controller/tests/provider-api-tests/shared-tests.ts b/packages/network-controller/tests/provider-api-tests/shared-tests.ts index 473a0ff2439..06241443d4f 100644 --- a/packages/network-controller/tests/provider-api-tests/shared-tests.ts +++ b/packages/network-controller/tests/provider-api-tests/shared-tests.ts @@ -19,35 +19,6 @@ export function buildInfuraClientRetriesExhaustedErrorMessage(reason: string) { ); } -/** - * Constructs an error message that JsonRpcEngine would produce in the event - * that the response object is empty as it leaves the middleware. - * - * @param method - The RPC method. - * @returns The error message. - */ -export function buildJsonRpcEngineEmptyResponseErrorMessage(method: string) { - return new RegExp( - `^JsonRpcEngine: Response has no error or result for request:.+"method": "${method}"`, - 'us', - ); -} - -/** - * Constructs an error message that `fetch` with throw if it cannot make a - * request. - * - * @param url - The URL being fetched - * @param reason - The reason. - * @returns The error message. - */ -export function buildFetchFailedErrorMessage(url: string, reason: string) { - return new RegExp( - `^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`, - 'us', - ); -} - /** * Defines tests that are common to both the Infura and JSON-RPC network client. * diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.update-network.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.update-network.test.ts index a596142e983..ac817a65382 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.update-network.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.update-network.test.ts @@ -96,6 +96,8 @@ describe('network-syncing/controller-integration - dispatchUpdateNetwork()', () messenger: networkControllerMessenger, state: networkState, infuraProjectId: 'TEST_ID', + fetch, + btoa, }); return { networkController, baseMessenger }; diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index a4e88c28f69..f53bec63e90 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -162,6 +162,8 @@ const setupController = async ( allowedEvents: [], }), infuraProjectId, + fetch, + btoa, }); await networkController.initializeProvider(); const { provider, blockTracker } = diff --git a/yarn.lock b/yarn.lock index 48b2f1eb5e1..d5521d6350c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2890,16 +2890,16 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-block-tracker@npm:^11.0.3": - version: 11.0.3 - resolution: "@metamask/eth-block-tracker@npm:11.0.3" +"@metamask/eth-block-tracker@npm:^11.0.3, @metamask/eth-block-tracker@npm:^11.0.4": + version: 11.0.4 + resolution: "@metamask/eth-block-tracker@npm:11.0.4" dependencies: "@metamask/eth-json-rpc-provider": "npm:^4.1.5" "@metamask/safe-event-emitter": "npm:^3.1.1" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^11.0.1" json-rpc-random-id: "npm:^1.0.1" pify: "npm:^5.0.0" - checksum: 10/c73a570f889c613ab309643c84a4aed1a4eeed5c101434da84b34babe2352218c65f863602e013a8a55052e3f80a538efed865cc5fb7af558d168c52c5a399a4 + checksum: 10/56b60255a3ae23a378570a49c30d0c13bd74094c0509a978cad20ef57079c80bae91fd35749acb9ac5feef2922eec45a6fef8c0ee6e754cbf3722f8e5d0d771e languageName: node linkType: hard @@ -2929,38 +2929,38 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-json-rpc-infura@npm:^10.0.0": - version: 10.0.0 - resolution: "@metamask/eth-json-rpc-infura@npm:10.0.0" +"@metamask/eth-json-rpc-infura@npm:^10.1.0": + version: 10.1.0 + resolution: "@metamask/eth-json-rpc-infura@npm:10.1.0" dependencies: - "@metamask/eth-json-rpc-provider": "npm:^4.1.5" - "@metamask/json-rpc-engine": "npm:^10.0.0" - "@metamask/rpc-errors": "npm:^7.0.0" - "@metamask/utils": "npm:^9.1.0" - checksum: 10/17e0147ff86c48107983035e9bda4d16fba321ee0e29733347e9338a4c795c506a2ffd643c44c9d5334886696412cf288f852d06311fed0d76edc8847ee6b8de + "@metamask/eth-json-rpc-provider": "npm:^4.1.7" + "@metamask/json-rpc-engine": "npm:^10.0.2" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.0.1" + checksum: 10/e3305d8a2535c3dd0e4b127fb6d01e70245f394b05c6fe81030a9043ad6fd4b8d904e00830236f88cb80b09fa6490ea22e7abaa8230a4fd4912436d0738ee702 languageName: node linkType: hard -"@metamask/eth-json-rpc-middleware@npm:^15.0.1": - version: 15.0.1 - resolution: "@metamask/eth-json-rpc-middleware@npm:15.0.1" +"@metamask/eth-json-rpc-middleware@npm:^15.1.0": + version: 15.2.0 + resolution: "@metamask/eth-json-rpc-middleware@npm:15.2.0" dependencies: - "@metamask/eth-block-tracker": "npm:^11.0.3" - "@metamask/eth-json-rpc-provider": "npm:^4.1.5" - "@metamask/eth-sig-util": "npm:^7.0.3" - "@metamask/json-rpc-engine": "npm:^10.0.0" - "@metamask/rpc-errors": "npm:^7.0.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/eth-block-tracker": "npm:^11.0.4" + "@metamask/eth-json-rpc-provider": "npm:^4.1.7" + "@metamask/eth-sig-util": "npm:^8.1.2" + "@metamask/json-rpc-engine": "npm:^10.0.2" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.1.0" "@types/bn.js": "npm:^5.1.5" bn.js: "npm:^5.2.1" klona: "npm:^2.0.6" pify: "npm:^5.0.0" safe-stable-stringify: "npm:^2.4.3" - checksum: 10/9777fca31440bf0076f5d2c24e2ddb4848ecd9d41b0a5d6114c27339567e60bfcb9057d6bfa81f18f5ca0ffa848ecf9603c765f606b8de206d3e34dba519c501 + checksum: 10/52dcb5927fe5e2db318965e3c5179704a1fa56ebccabeda93b8f9a6c28cb8958d5fefd7bddf5673c6532eab5d46ced8c7001394ce5cc634d8acd491755bcdd4c languageName: node linkType: hard -"@metamask/eth-json-rpc-provider@npm:^4.1.5, @metamask/eth-json-rpc-provider@npm:^4.1.8, @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.7, @metamask/eth-json-rpc-provider@npm:^4.1.8, @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: @@ -3008,7 +3008,7 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-sig-util@npm:^8.0.0, @metamask/eth-sig-util@npm:^8.2.0": +"@metamask/eth-sig-util@npm:^8.0.0, @metamask/eth-sig-util@npm:^8.1.2, @metamask/eth-sig-util@npm:^8.2.0": version: 8.2.0 resolution: "@metamask/eth-sig-util@npm:8.2.0" dependencies: @@ -3604,8 +3604,8 @@ __metadata: "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" "@metamask/eth-block-tracker": "npm:^11.0.3" - "@metamask/eth-json-rpc-infura": "npm:^10.0.0" - "@metamask/eth-json-rpc-middleware": "npm:^15.0.1" + "@metamask/eth-json-rpc-infura": "npm:^10.1.0" + "@metamask/eth-json-rpc-middleware": "npm:^15.1.0" "@metamask/eth-json-rpc-provider": "npm:^4.1.8" "@metamask/eth-query": "npm:^4.0.0" "@metamask/json-rpc-engine": "npm:^10.0.3" @@ -3974,7 +3974,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/rpc-errors@npm:^7.0.0, @metamask/rpc-errors@npm:^7.0.2": +"@metamask/rpc-errors@npm:^7.0.2": version: 7.0.2 resolution: "@metamask/rpc-errors@npm:7.0.2" dependencies: @@ -4314,8 +4314,8 @@ __metadata: linkType: soft "@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0": - version: 11.1.0 - resolution: "@metamask/utils@npm:11.1.0" + version: 11.2.0 + resolution: "@metamask/utils@npm:11.2.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -4326,7 +4326,7 @@ __metadata: pony-cause: "npm:^2.1.10" semver: "npm:^7.5.4" uuid: "npm:^9.0.1" - checksum: 10/756f13987881fe26adaa0a54354bc5af20cedee4dd228a736d481697dc634adb9e6e54d8f1dcc1d487b2376ab4ba8c576ecbb24beab2fb63aff721d0d5c0f5fe + checksum: 10/9cc2cb6af4627085e72a310ba9b8921c69757d94e2992d4664627e5a0d99b1f2f7f8069c6f22262515135e1172bd66b82d00512d90ea2ec6da4e768f3d7d4ae2 languageName: node linkType: hard @@ -4347,7 +4347,7 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1": +"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.2.1": version: 9.3.0 resolution: "@metamask/utils@npm:9.3.0" dependencies: From 731da56f027a7809749eb2b321f75314e8829605 Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:12:13 +0100 Subject: [PATCH 19/20] feat: add `KeyringController:withKeyring` action (#5332) --- packages/keyring-controller/CHANGELOG.md | 5 +++++ .../src/KeyringController.test.ts | 22 +++++++++++++++++++ .../src/KeyringController.ts | 13 ++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 1390ffdb22f..59c0a21205b 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `KeyringController:withKeyring` action ([#5332](https://github.com/MetaMask/core/pull/5332)) + - The action can be used to consume the `withKeyring` method of the `KeyringController` class + ## [19.1.0] ### Added diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index b9fab0c14a0..19daf6e49ed 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -3838,6 +3838,28 @@ describe('KeyringController', () => { }); }); }); + + describe('withKeyring', () => { + it('should call withKeyring', async () => { + await withController( + { keyringBuilders: [keyringBuilderFactory(MockKeyring)] }, + async ({ controller, messenger }) => { + await controller.addNewKeyring(MockKeyring.type); + + const actionReturnValue = await messenger.call( + 'KeyringController:withKeyring', + { type: MockKeyring.type }, + async (keyring) => { + expect(keyring.type).toBe(MockKeyring.type); + return keyring.type; + }, + ); + + expect(actionReturnValue).toBe(MockKeyring.type); + }, + ); + }); + }); }); describe('run conditions', () => { diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index d28b07ce93c..55f3acad097 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -177,6 +177,11 @@ export type KeyringControllerAddNewAccountAction = { handler: KeyringController['addNewAccount']; }; +export type KeyringControllerWithKeyringAction = { + type: `${typeof name}:withKeyring`; + handler: KeyringController['withKeyring']; +}; + export type KeyringControllerStateChangeEvent = { type: `${typeof name}:stateChange`; payload: [KeyringControllerState, Patch[]]; @@ -216,7 +221,8 @@ export type KeyringControllerActions = | KeyringControllerPrepareUserOperationAction | KeyringControllerPatchUserOperationAction | KeyringControllerSignUserOperationAction - | KeyringControllerAddNewAccountAction; + | KeyringControllerAddNewAccountAction + | KeyringControllerWithKeyringAction; export type KeyringControllerEvents = | KeyringControllerStateChangeEvent @@ -1796,6 +1802,11 @@ export class KeyringController extends BaseController< `${name}:addNewAccount`, this.addNewAccount.bind(this), ); + + this.messagingSystem.registerActionHandler( + `${name}:withKeyring`, + this.withKeyring.bind(this), + ); } /** From 0982b83a177e4280b66813666026f1e7f82eee97 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Fri, 14 Feb 2025 17:28:03 +0100 Subject: [PATCH 20/20] Use Prettier 2 for Jest (#5330) ## Explanation Jest doesn't support Prettier 3, so after the migration to ESLint 9 (requiring Prettier 3), Jest snapshots could no longer be created or updated. The recommended solution by Jest is to add `prettier-2` as separate dependency and referencing that in the config. --- jest.config.packages.js | 4 ++++ jest.config.scripts.js | 4 ++++ package.json | 1 + yarn.lock | 19 ++++++++++--------- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/jest.config.packages.js b/jest.config.packages.js index 96bde23bfc7..98abf47be2b 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -100,6 +100,10 @@ module.exports = { // A preset that is used as a base for Jest's configuration preset: 'ts-jest', + // The path to the Prettier executable used to format snapshots + // Jest doesn't support Prettier 3 yet, so we use Prettier 2 + prettierPath: require.resolve('prettier-2'), + // Run tests from one or more projects // projects: undefined diff --git a/jest.config.scripts.js b/jest.config.scripts.js index 343c51b9d2c..cb984e727e2 100644 --- a/jest.config.scripts.js +++ b/jest.config.scripts.js @@ -50,6 +50,10 @@ module.exports = { // // A preset that is used as a base for Jest's configuration // preset: 'ts-jest', + // The path to the Prettier executable used to format snapshots + // Jest doesn't support Prettier 3 yet, so we use Prettier 2 + prettierPath: require.resolve('prettier-2'), + // "resetMocks" resets all mocks, including mocked modules, to jest.fn(), // between each test case. resetMocks: true, diff --git a/package.json b/package.json index dbfee920a13..00fbc57d3ac 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "lodash": "^4.17.21", "nock": "^13.3.1", "prettier": "^3.3.3", + "prettier-2": "npm:prettier@^2.8.8", "prettier-plugin-packagejson": "^2.4.5", "rimraf": "^5.0.5", "semver": "^7.6.3", diff --git a/yarn.lock b/yarn.lock index d5521d6350c..d934e6a8810 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2747,6 +2747,7 @@ __metadata: lodash: "npm:^4.17.21" nock: "npm:^13.3.1" prettier: "npm:^3.3.3" + prettier-2: "npm:prettier@^2.8.8" prettier-plugin-packagejson: "npm:^2.4.5" rimraf: "npm:^5.0.5" semver: "npm:^7.6.3" @@ -11738,6 +11739,15 @@ __metadata: languageName: node linkType: hard +"prettier-2@npm:prettier@^2.8.8, prettier@npm:^2.8.8": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: 10/00cdb6ab0281f98306cd1847425c24cbaaa48a5ff03633945ab4c701901b8e96ad558eb0777364ffc312f437af9b5a07d0f45346266e8245beaf6247b9c62b24 + languageName: node + linkType: hard + "prettier-linter-helpers@npm:^1.0.0": version: 1.0.0 resolution: "prettier-linter-helpers@npm:1.0.0" @@ -11762,15 +11772,6 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.8.8": - version: 2.8.8 - resolution: "prettier@npm:2.8.8" - bin: - prettier: bin-prettier.js - checksum: 10/00cdb6ab0281f98306cd1847425c24cbaaa48a5ff03633945ab4c701901b8e96ad558eb0777364ffc312f437af9b5a07d0f45346266e8245beaf6247b9c62b24 - languageName: node - linkType: hard - "prettier@npm:^3.3.3": version: 3.4.2 resolution: "prettier@npm:3.4.2"