diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 10fcf481d94..fcfb57e5fde 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -37,12 +37,16 @@ ## 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 ## 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 +66,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 @@ -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 9645d28513e..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) @@ -39,6 +40,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 +87,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 +108,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 +120,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 +145,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 +200,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..9a3e6c309d3 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 }, @@ -35,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, @@ -420,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 }, @@ -568,7 +551,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, @@ -704,13 +686,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/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 48ebc122150..00fbc57d3ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "297.0.0", + "version": "299.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { @@ -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/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 88cb13fa5e4..63cffecab42 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,23 @@ 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 + +- Add new keyring type for OneKey ([#5216](https://github.com/MetaMask/core/pull/5216)) + ## [23.0.1] ### Changed @@ -438,7 +455,9 @@ 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@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 [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..2b7de49c311 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": "24.0.0", "description": "Manages internal accounts", "keywords": [ "MetaMask", @@ -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", @@ -62,7 +63,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", @@ -76,6 +77,7 @@ }, "peerDependencies": { "@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/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 07c400f9a4e..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'; @@ -1875,6 +1959,7 @@ describe('AccountsController', () => { KeyringTypes.simple, KeyringTypes.hd, KeyringTypes.trezor, + KeyringTypes.oneKey, KeyringTypes.ledger, KeyringTypes.lattice, KeyringTypes.qr, @@ -2144,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, @@ -2177,7 +2239,7 @@ describe('AccountsController', () => { expected: mockOlderEvmAccount, }, { - lastSelectedAccount: mockNonEvmAccount, + lastSelectedAccount: mockBtcAccount, expected: mockNewerEvmAccount, }, ])( @@ -2189,7 +2251,7 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: lastSelectedAccount.id, }, @@ -2205,9 +2267,9 @@ describe('AccountsController', () => { initialState: { internalAccounts: { accounts: { - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, - selectedAccount: mockNonEvmAccount.id, + selectedAccount: mockBtcAccount.id, }, }, }); @@ -2234,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, @@ -2265,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", @@ -2287,7 +2326,7 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: selectedAccount.id, }, @@ -2312,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..84ea1a113ad 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. + */ + #handleOnMultichainNetworkDidChange(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; + }); + + // DO NOT publish AccountsController:setSelectedAccount to prevent circular listener loops + } + /** * 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', + (id) => this.#handleOnMultichainNetworkDidChange(id), + ); + } + /** * 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 d3cb5aede23..3562df9b566 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'; } @@ -47,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. */ @@ -62,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. */ @@ -71,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/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 594cbdd7354..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,11 +77,11 @@ }, "devDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^23.0.1", + "@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", - "@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", @@ -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/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 * 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/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md new file mode 100644 index 00000000000..8869e80b3be --- /dev/null +++ b/packages/bridge-controller/CHANGELOG.md @@ -0,0 +1,19 @@ +# 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 + +### 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/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..4f7c837d54a --- /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": "^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": "^46.0.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": "^24.0.0", + "@metamask/network-controller": "^22.0.0", + "@metamask/transaction-controller": "^46.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/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 1f1457bc23e..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.0.1", + "@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/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/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 71590b4092e..59c0a21205b 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,21 @@ 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 + +- 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 +675,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/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 b04582ee3ca..55f3acad097 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', @@ -176,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[]]; @@ -215,7 +221,8 @@ export type KeyringControllerActions = | KeyringControllerPrepareUserOperationAction | KeyringControllerPatchUserOperationAction | KeyringControllerSignUserOperationAction - | KeyringControllerAddNewAccountAction; + | KeyringControllerAddNewAccountAction + | KeyringControllerWithKeyringAction; export type KeyringControllerEvents = | KeyringControllerStateChangeEvent @@ -1795,6 +1802,11 @@ export class KeyringController extends BaseController< `${name}:addNewAccount`, this.addNewAccount.bind(this), ); + + this.messagingSystem.registerActionHandler( + `${name}:withKeyring`, + this.withKeyring.bind(this), + ); } /** diff --git a/packages/multichain-network-controller/CHANGELOG.md b/packages/multichain-network-controller/CHANGELOG.md new file mode 100644 index 00000000000..8ee176414d5 --- /dev/null +++ b/packages/multichain-network-controller/CHANGELOG.md @@ -0,0 +1,19 @@ +# 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] + +## [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/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..cec39a5848e --- /dev/null +++ b/packages/multichain-network-controller/package.json @@ -0,0 +1,81 @@ +{ + "name": "@metamask/multichain-network-controller", + "version": "0.1.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", + "@metamask/network-controller": "^22.2.1", + "@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": "^24.0.0", + "@metamask/network-controller": "^22.0.0" + }, + "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..5f4728054f0 --- /dev/null +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -0,0 +1,382 @@ +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, + })), + 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 + 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); + }); + + 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', () => { + 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..b9c3d5f441b --- /dev/null +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -0,0 +1,206 @@ +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 { + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + + 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 selection state if needed + if (shouldSetEvmActive) { + this.update((state) => { + state.isEvmSelected = true; + }); + } + + // 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, + ); + } + } + + /** + * 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, + ); + } + + /** + * 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 + */ + #handleOnSelectedAccountChange(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; + }); + + // 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. + */ + #subscribeToMessageEvents() { + // Handle network switch when account is changed + this.messagingSystem.subscribe( + 'AccountsController:selectedAccountChange', + (account) => this.#handleOnSelectedAccountChange(account), + ); + } + + /** + * 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/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 b62ac865df6..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,9 +61,9 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^24.0.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", @@ -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/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). 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 6fd54838ce4..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", @@ -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/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/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/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/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 1fb0a09080b..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", @@ -112,8 +112,8 @@ "@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/profile-sync-controller": "^7.0.1", + "@metamask/keyring-controller": "^19.1.0", + "@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/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 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/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 a9f2b6dc5b8..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", @@ -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": "^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/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index a41be03d2a5..f9108a81a22 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']; @@ -247,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 @@ -424,6 +421,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), 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, 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/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}`; 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/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 1ce358fd59f..af30d1018a4 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,19 @@ 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)) + - `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)) + +### 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 @@ -1277,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/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/package.json b/packages/transaction-controller/package.json index 74b65af67ed..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.0.1", + "@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/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/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/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. */ 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 a68ef61a585..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", @@ -65,9 +65,9 @@ "@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", + "@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/teams.json b/teams.json index 7060b959eaf..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", @@ -17,6 +18,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 9ca303c8fa9..d934e6a8810 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:^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: @@ -2349,8 +2349,9 @@ __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/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" @@ -2370,6 +2371,7 @@ __metadata: webextension-polyfill: "npm:^0.12.0" peerDependencies: "@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 @@ -2460,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.0.1" + "@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" @@ -2469,7 +2471,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" @@ -2507,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 @@ -2580,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:^24.0.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:^46.0.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": ^24.0.0 + "@metamask/network-controller": ^22.0.0 + "@metamask/transaction-controller": ^46.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" @@ -2712,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" @@ -2750,7 +2786,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:^24.0.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" @@ -2764,7 +2800,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 @@ -2855,16 +2891,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 @@ -2894,38 +2930,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: @@ -2973,7 +3009,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: @@ -3299,7 +3335,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: @@ -3451,15 +3487,42 @@ __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/network-controller": "npm:^22.2.1" + "@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": ^24.0.0 + "@metamask/network-controller": ^22.0.0 + 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" dependencies: - "@metamask/accounts-controller": "npm:^23.0.1" + "@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" - "@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" @@ -3478,7 +3541,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 @@ -3542,8 +3605,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" @@ -3553,6 +3616,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 +3626,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" @@ -3595,8 +3660,8 @@ __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/profile-sync-controller": "npm:^7.0.1" + "@metamask/keyring-controller": "npm:^19.1.0" + "@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" @@ -3615,7 +3680,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 @@ -3763,7 +3828,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" @@ -3777,17 +3842,17 @@ __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.0.1" + "@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" - "@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" @@ -3811,7 +3876,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 @@ -3910,7 +3975,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: @@ -3974,7 +4039,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" @@ -4156,7 +4221,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: @@ -4167,7 +4232,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:^24.0.0" "@metamask/approval-controller": "npm:^7.1.3" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" @@ -4202,7 +4267,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 @@ -4221,12 +4286,12 @@ __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" "@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" @@ -4245,13 +4310,13 @@ __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 "@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" @@ -4262,7 +4327,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 @@ -4283,7 +4348,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: @@ -4687,6 +4752,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" @@ -5028,6 +5169,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" @@ -7054,6 +7205,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" @@ -11018,7 +11176,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: @@ -11581,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" @@ -11605,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"