diff --git a/examples/example-controllers/package.json b/examples/example-controllers/package.json index 4a00b8bb31f..c4322bb6860 100644 --- a/examples/example-controllers/package.json +++ b/examples/example-controllers/package.json @@ -47,12 +47,12 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/utils": "^11.0.1" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/controller-utils": "^11.4.4", + "@metamask/controller-utils": "^11.4.5", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/package.json b/package.json index 369dc834727..93cc727ff10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "277.0.0", + "version": "280.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 5e09e43d8ab..756016ca684 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -48,10 +48,10 @@ }, "dependencies": { "@ethereumjs/util": "^8.1.0", - "@metamask/base-controller": "^7.1.0", - "@metamask/eth-snap-keyring": "^7.0.0", + "@metamask/base-controller": "^7.1.1", + "@metamask/eth-snap-keyring": "^8.0.0", "@metamask/keyring-api": "^13.0.0", - "@metamask/keyring-internal-api": "^1.1.0", + "@metamask/keyring-internal-api": "^2.0.0", "@metamask/snaps-sdk": "^6.7.0", "@metamask/snaps-utils": "^8.3.0", "@metamask/utils": "^11.0.1", diff --git a/packages/address-book-controller/package.json b/packages/address-book-controller/package.json index f3536eedb6d..748222c7563 100644 --- a/packages/address-book-controller/package.json +++ b/packages/address-book-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/utils": "^11.0.1" }, "devDependencies": { diff --git a/packages/announcement-controller/package.json b/packages/announcement-controller/package.json index 12a8961e28a..5a3feb68707 100644 --- a/packages/announcement-controller/package.json +++ b/packages/announcement-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0" + "@metamask/base-controller": "^7.1.1" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/approval-controller/CHANGELOG.md b/packages/approval-controller/CHANGELOG.md index 31a4ceb6ee8..cd4fb7b7a20 100644 --- a/packages/approval-controller/CHANGELOG.md +++ b/packages/approval-controller/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.1.2] + ### Changed +- Bump `nanoid` from `^3.1.31` to `^3.3.8` ([#5073](https://github.com/MetaMask/core/pull/5073)) +- Bump `@metamask/utils` from `^10.0.0` to `^11.0.1` ([#5080](https://github.com/MetaMask/core/pull/5080)) +- Bump `@metamask/rpc-errors` from `^7.0.0` to `^7.0.2` ([#5080](https://github.com/MetaMask/core/pull/5080)) - Bump `@metamask/base-controller` from `^7.0.0` to `^7.1.0` ([#5079](https://github.com/MetaMask/core/pull/5079)) ## [7.1.1] @@ -257,7 +262,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.1.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.1.2...HEAD +[7.1.2]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.1.1...@metamask/approval-controller@7.1.2 [7.1.1]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.1.0...@metamask/approval-controller@7.1.1 [7.1.0]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.0.4...@metamask/approval-controller@7.1.0 [7.0.4]: https://github.com/MetaMask/core/compare/@metamask/approval-controller@7.0.3...@metamask/approval-controller@7.0.4 diff --git a/packages/approval-controller/package.json b/packages/approval-controller/package.json index 934114e0b42..644d4c72b2f 100644 --- a/packages/approval-controller/package.json +++ b/packages/approval-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/approval-controller", - "version": "7.1.1", + "version": "7.1.2", "description": "Manages requests that require user approval", "keywords": [ "MetaMask", @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/rpc-errors": "^7.0.2", "@metamask/utils": "^11.0.1", "nanoid": "^3.3.8" diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 68ff0ac490e..8f55b87a865 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -54,17 +54,19 @@ "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@metamask/abi-utils": "^2.0.3", - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/contract-metadata": "^2.4.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/controller-utils": "^11.4.5", "@metamask/eth-query": "^4.0.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/polling-controller": "^12.0.2", "@metamask/rpc-errors": "^7.0.2", + "@metamask/snaps-utils": "^8.3.0", "@metamask/utils": "^11.0.1", "@types/bn.js": "^5.1.5", "@types/uuid": "^8.3.0", "async-mutex": "^0.5.0", + "bitcoin-address-validation": "^2.2.3", "bn.js": "^5.2.1", "cockatiel": "^3.1.2", "immer": "^9.0.6", @@ -76,14 +78,18 @@ "devDependencies": { "@babel/runtime": "^7.23.9", "@metamask/accounts-controller": "^20.0.2", - "@metamask/approval-controller": "^7.1.1", + "@metamask/approval-controller": "^7.1.2", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", + "@metamask/keyring-api": "^13.0.0", "@metamask/keyring-controller": "^19.0.2", - "@metamask/keyring-internal-api": "^1.1.0", + "@metamask/keyring-internal-api": "^2.0.0", + "@metamask/keyring-snap-client": "^2.0.0", "@metamask/network-controller": "^22.1.1", "@metamask/preferences-controller": "^15.0.1", "@metamask/providers": "^18.1.1", + "@metamask/snaps-controllers": "^9.10.0", + "@metamask/snaps-sdk": "^6.7.0", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.191", "@types/node": "^16.18.54", diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts new file mode 100644 index 00000000000..ed6409199f1 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts @@ -0,0 +1,143 @@ +import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { v4 as uuidv4 } from 'uuid'; + +import { BalancesTracker } from './BalancesTracker'; +import { Poller } from './Poller'; + +const MOCK_TIMESTAMP = 1709983353; + +const mockBtcAccount = { + address: '', + id: uuidv4(), + metadata: { + name: 'Bitcoin Account 1', + importTime: Date.now(), + keyring: { + type: KeyringTypes.snap, + }, + snap: { + id: 'mock-btc-snap', + name: 'mock-btc-snap', + enabled: true, + }, + lastSelected: 0, + }, + options: {}, + methods: [BtcMethod.SendBitcoin], + type: BtcAccountType.P2wpkh, +}; + +/** + * Sets up a BalancesTracker instance for testing. + * @returns The BalancesTracker instance and a mock update balance function. + */ +function setupTracker() { + const mockUpdateBalance = jest.fn(); + const tracker = new BalancesTracker(mockUpdateBalance); + + return { + tracker, + mockUpdateBalance, + }; +} + +describe('BalancesTracker', () => { + it('starts polling when calling start', async () => { + const { tracker } = setupTracker(); + const spyPoller = jest.spyOn(Poller.prototype, 'start'); + + tracker.start(); + expect(spyPoller).toHaveBeenCalledTimes(1); + }); + + it('stops polling when calling stop', async () => { + const { tracker } = setupTracker(); + const spyPoller = jest.spyOn(Poller.prototype, 'stop'); + + tracker.start(); + tracker.stop(); + expect(spyPoller).toHaveBeenCalledTimes(1); + }); + + it('is not tracking if none accounts have been registered', async () => { + const { tracker, mockUpdateBalance } = setupTracker(); + + tracker.start(); + await tracker.updateBalances(); + + expect(mockUpdateBalance).not.toHaveBeenCalled(); + }); + + it('tracks account balances', async () => { + const { tracker, mockUpdateBalance } = setupTracker(); + + tracker.start(); + // We must track account IDs explicitly + tracker.track(mockBtcAccount.id, 0); + // Trigger balances refresh (not waiting for the Poller here) + await tracker.updateBalances(); + + expect(mockUpdateBalance).toHaveBeenCalledWith(mockBtcAccount.id); + }); + + it('untracks account balances', async () => { + const { tracker, mockUpdateBalance } = setupTracker(); + + tracker.start(); + tracker.track(mockBtcAccount.id, 0); + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledWith(mockBtcAccount.id); + + tracker.untrack(mockBtcAccount.id); + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledTimes(1); // No second call after untracking + }); + + it('tracks account after being registered', async () => { + const { tracker } = setupTracker(); + + tracker.start(); + tracker.track(mockBtcAccount.id, 0); + expect(tracker.isTracked(mockBtcAccount.id)).toBe(true); + }); + + it('does not track account if not registered', async () => { + const { tracker } = setupTracker(); + + tracker.start(); + expect(tracker.isTracked(mockBtcAccount.id)).toBe(false); + }); + + it('does not refresh balance if they are considered up-to-date', async () => { + const { tracker, mockUpdateBalance } = setupTracker(); + + const blockTime = 10 * 60 * 1000; // 10 minutes in milliseconds. + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date(MOCK_TIMESTAMP).getTime()); + + tracker.start(); + tracker.track(mockBtcAccount.id, blockTime); + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledTimes(1); + + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledTimes(1); // No second call since the balances is already still up-to-date + + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date(MOCK_TIMESTAMP + blockTime).getTime()); + + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledTimes(2); // Now the balance will update + }); + + it('throws an error if trying to update balance of an untracked account', async () => { + const { tracker } = setupTracker(); + + await expect(tracker.updateBalance(mockBtcAccount.id)).rejects.toThrow( + `Account is not being tracked: ${mockBtcAccount.id}`, + ); + }); +}); diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts new file mode 100644 index 00000000000..661c229a82d --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -0,0 +1,139 @@ +import { Poller } from './Poller'; + +type BalanceInfo = { + lastUpdated: number; + blockTime: number; +}; + +const BALANCES_TRACKING_INTERVAL = 5000; // Every 5s in milliseconds. + +export class BalancesTracker { + #poller: Poller; + + #updateBalance: (accountId: string) => Promise; + + #balances: Record = {}; + + constructor(updateBalanceCallback: (accountId: string) => Promise) { + this.#updateBalance = updateBalanceCallback; + + this.#poller = new Poller( + () => this.updateBalances(), + BALANCES_TRACKING_INTERVAL, + ); + } + + /** + * Starts the tracking process. + */ + start(): void { + this.#poller.start(); + } + + /** + * Stops the tracking process. + */ + stop(): void { + this.#poller.stop(); + } + + /** + * Checks if an account ID is being tracked. + * + * @param accountId - The account ID. + * @returns True if the account is being tracked, false otherwise. + */ + isTracked(accountId: string) { + return Object.prototype.hasOwnProperty.call(this.#balances, accountId); + } + + /** + * Asserts that an account ID is being tracked. + * + * @param accountId - The account ID. + * @throws If the account ID is not being tracked. + */ + assertBeingTracked(accountId: string) { + if (!this.isTracked(accountId)) { + throw new Error(`Account is not being tracked: ${accountId}`); + } + } + + /** + * Starts tracking a new account ID. This method has no effect on already tracked + * accounts. + * + * @param accountId - The account ID. + * @param blockTime - The block time (used when refreshing the account balances). + */ + track(accountId: string, blockTime: number) { + // Do not overwrite current info if already being tracked! + if (!this.isTracked(accountId)) { + this.#balances[accountId] = { + lastUpdated: 0, + blockTime, + }; + } + } + + /** + * Stops tracking a tracked account ID. + * + * @param accountId - The account ID. + * @throws If the account ID is not being tracked. + */ + untrack(accountId: string) { + this.assertBeingTracked(accountId); + delete this.#balances[accountId]; + } + + /** + * Update the balances for a tracked account ID. + * + * @param accountId - The account ID. + * @throws If the account ID is not being tracked. + */ + async updateBalance(accountId: string) { + this.assertBeingTracked(accountId); + + // We check if the balance is outdated (by comparing to the block time associated + // with this kind of account). + // + // This might not be super accurate, but we could probably compute this differently + // and try to sync with the "real block time"! + const info = this.#balances[accountId]; + if (this.#isBalanceOutdated(info)) { + await this.#updateBalance(accountId); + this.#balances[accountId].lastUpdated = Date.now(); + } + } + + /** + * Update the balances of all tracked accounts (only if the balances + * is considered outdated). + */ + async updateBalances() { + await Promise.allSettled( + Object.keys(this.#balances).map(async (accountId) => { + await this.updateBalance(accountId); + }), + ); + } + + /** + * Checks if the balance is outdated according to the provided data. + * + * @param param - The balance info. + * @param param.lastUpdated - The last updated timestamp. + * @param param.blockTime - The block time. + * @returns True if the balance is outdated, false otherwise. + */ + #isBalanceOutdated({ lastUpdated, blockTime }: BalanceInfo): boolean { + return ( + // Never been updated: + lastUpdated === 0 || + // Outdated: + Date.now() - lastUpdated >= blockTime + ); + } +} diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts new file mode 100644 index 00000000000..87f200ab550 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -0,0 +1,261 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import type { Balance, CaipAssetType } from '@metamask/keyring-api'; +import { + BtcAccountType, + BtcMethod, + EthAccountType, + EthMethod, + BtcScopes, + EthScopes, + SolScopes, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import { v4 as uuidv4 } from 'uuid'; + +import type { + ExtractAvailableAction, + ExtractAvailableEvent, +} from '../../../base-controller/tests/helpers'; +import { BalancesTracker } from './BalancesTracker'; +import { + MultichainBalancesController, + getDefaultMultichainBalancesControllerState, +} from './MultichainBalancesController'; +import type { + MultichainBalancesControllerMessenger, + MultichainBalancesControllerState, +} from './MultichainBalancesController'; + +const mockBtcAccount = { + address: 'bc1qssdcp5kvwh6nghzg9tuk99xsflwkdv4hgvq58q', + id: uuidv4(), + metadata: { + name: 'Bitcoin Account 1', + importTime: Date.now(), + keyring: { + type: KeyringTypes.snap, + }, + snap: { + id: 'mock-btc-snap', + name: 'mock-btc-snap', + enabled: true, + }, + lastSelected: 0, + }, + scopes: [BtcScopes.Namespace], + options: {}, + methods: [BtcMethod.SendBitcoin], + type: BtcAccountType.P2wpkh, +}; + +const mockEthAccount = { + address: '0x807dE1cf8f39E83258904b2f7b473E5C506E4aC1', + id: uuidv4(), + metadata: { + name: 'Ethereum Account 1', + importTime: Date.now(), + keyring: { + type: KeyringTypes.snap, + }, + snap: { + id: 'mock-eth-snap', + name: 'mock-eth-snap', + enabled: true, + }, + lastSelected: 0, + }, + scopes: [EthScopes.Namespace], + options: {}, + methods: [EthMethod.SignTypedDataV4, EthMethod.SignTransaction], + type: EthAccountType.Eoa, +}; + +const mockBalanceResult = { + 'bip122:000000000933ea01ad0ee984209779ba/slip44:0': { + amount: '0.00000000', + unit: 'BTC', + }, +}; + +/** + * The union of actions that the root messenger allows. + */ +type RootAction = ExtractAvailableAction; + +/** + * The union of events that the root messenger allows. + */ +type RootEvent = ExtractAvailableEvent; + +/** + * Constructs the unrestricted messenger. This can be used to call actions and + * publish events within the tests for this controller. + * + * @returns The unrestricted messenger suited for PetNamesController. + */ +function getRootControllerMessenger(): ControllerMessenger< + RootAction, + RootEvent +> { + return new ControllerMessenger(); +} + +const setupController = ({ + state = getDefaultMultichainBalancesControllerState(), + mocks, +}: { + state?: MultichainBalancesControllerState; + mocks?: { + listMultichainAccounts?: InternalAccount[]; + handleRequestReturnValue?: Record; + }; +} = {}) => { + const controllerMessenger = getRootControllerMessenger(); + + const multichainBalancesControllerMessenger: MultichainBalancesControllerMessenger = + controllerMessenger.getRestricted({ + name: 'MultichainBalancesController', + allowedActions: [ + 'SnapController:handleRequest', + 'AccountsController:listMultichainAccounts', + ], + allowedEvents: [ + 'AccountsController:accountAdded', + 'AccountsController:accountRemoved', + ], + }); + + const mockSnapHandleRequest = jest.fn(); + controllerMessenger.registerActionHandler( + 'SnapController:handleRequest', + mockSnapHandleRequest.mockReturnValue( + mocks?.handleRequestReturnValue ?? mockBalanceResult, + ), + ); + + const mockListMultichainAccounts = jest.fn(); + controllerMessenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + mockListMultichainAccounts.mockReturnValue( + mocks?.listMultichainAccounts ?? [mockBtcAccount, mockEthAccount], + ), + ); + + const controller = new MultichainBalancesController({ + messenger: multichainBalancesControllerMessenger, + state, + }); + + return { + controller, + messenger: controllerMessenger, + mockSnapHandleRequest, + mockListMultichainAccounts, + }; +}; + +describe('BalancesController', () => { + it('initialize with default state', () => { + const { controller } = setupController({}); + expect(controller.state).toStrictEqual({ balances: {} }); + }); + + it('starts tracking when calling start', async () => { + const spyTracker = jest.spyOn(BalancesTracker.prototype, 'start'); + const { controller } = setupController(); + controller.start(); + expect(spyTracker).toHaveBeenCalledTimes(1); + }); + + it('stops tracking when calling stop', async () => { + const spyTracker = jest.spyOn(BalancesTracker.prototype, 'stop'); + const { controller } = setupController(); + controller.start(); + controller.stop(); + expect(spyTracker).toHaveBeenCalledTimes(1); + }); + + it('updates balances when calling updateBalances', async () => { + const { controller } = setupController(); + + await controller.updateBalances(); + + expect(controller.state).toStrictEqual({ + balances: { + [mockBtcAccount.id]: mockBalanceResult, + }, + }); + }); + + it('updates the balance for a specific account when calling updateBalance', async () => { + const { controller } = setupController(); + + await controller.updateBalance(mockBtcAccount.id); + + expect(controller.state).toStrictEqual({ + balances: { + [mockBtcAccount.id]: mockBalanceResult, + }, + }); + }); + + it('updates balances when "AccountsController:accountAdded" is fired', async () => { + const { controller, messenger, mockListMultichainAccounts } = + setupController({ + mocks: { + listMultichainAccounts: [], + }, + }); + + controller.start(); + mockListMultichainAccounts.mockReturnValue([mockBtcAccount]); + messenger.publish('AccountsController:accountAdded', mockBtcAccount); + await controller.updateBalances(); + + expect(controller.state).toStrictEqual({ + balances: { + [mockBtcAccount.id]: mockBalanceResult, + }, + }); + }); + + it('updates balances when "AccountsController:accountRemoved" is fired', async () => { + const { controller, messenger, mockListMultichainAccounts } = + setupController(); + + controller.start(); + await controller.updateBalances(); + expect(controller.state).toStrictEqual({ + balances: { + [mockBtcAccount.id]: mockBalanceResult, + }, + }); + + messenger.publish('AccountsController:accountRemoved', mockBtcAccount.id); + mockListMultichainAccounts.mockReturnValue([]); + await controller.updateBalances(); + + expect(controller.state).toStrictEqual({ + balances: {}, + }); + }); + + it('does not track balances for EVM accounts', async () => { + const { controller, messenger, mockListMultichainAccounts } = + setupController({ + mocks: { + listMultichainAccounts: [], + }, + }); + + controller.start(); + mockListMultichainAccounts.mockReturnValue([mockEthAccount]); + messenger.publish('AccountsController:accountAdded', mockEthAccount); + await controller.updateBalances(); + + expect(controller.state).toStrictEqual({ + balances: {}, + }); + }); +}); diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts new file mode 100644 index 00000000000..9442607e564 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -0,0 +1,369 @@ +import type { + AccountsControllerAccountAddedEvent, + AccountsControllerAccountRemovedEvent, + AccountsControllerListMultichainAccountsAction, +} from '@metamask/accounts-controller'; +import { + BaseController, + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { isEvmAccountType } from '@metamask/keyring-api'; +import type { Balance, CaipAssetType } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import { KeyringClient } from '@metamask/keyring-snap-client'; +import type { HandleSnapRequest } from '@metamask/snaps-controllers'; +import type { SnapId } from '@metamask/snaps-sdk'; +import { HandlerType } from '@metamask/snaps-utils'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; +import type { Draft } from 'immer'; + +import { BalancesTracker, NETWORK_ASSETS_MAP } from '.'; +import { getScopeForAccount, getBlockTimeForAccount } from './utils'; + +const controllerName = 'MultichainBalancesController'; + +/** + * State used by the {@link MultichainBalancesController} to cache account balances. + */ +export type MultichainBalancesControllerState = { + balances: { + [account: string]: { + [asset: string]: { + amount: string; + unit: string; + }; + }; + }; +}; + +/** + * Constructs the default {@link MultichainBalancesController} state. This allows + * consumers to provide a partial state object when initializing the controller + * and also helps in constructing complete state objects for this controller in + * tests. + * + * @returns The default {@link MultichainBalancesController} state. + */ +export function getDefaultMultichainBalancesControllerState(): MultichainBalancesControllerState { + return { balances: {} }; +} + +/** + * Returns the state of the {@link MultichainBalancesController}. + */ +export type MultichainBalancesControllerGetStateAction = + ControllerGetStateAction< + typeof controllerName, + MultichainBalancesControllerState + >; + +/** + * Updates the balances of all supported accounts. + */ +export type MultichainBalancesControllerUpdateBalancesAction = { + type: `${typeof controllerName}:updateBalances`; + handler: MultichainBalancesController['updateBalances']; +}; + +/** + * Event emitted when the state of the {@link MultichainBalancesController} changes. + */ +export type MultichainBalancesControllerStateChange = + ControllerStateChangeEvent< + typeof controllerName, + MultichainBalancesControllerState + >; + +/** + * Actions exposed by the {@link MultichainBalancesController}. + */ +export type MultichainBalancesControllerActions = + | MultichainBalancesControllerGetStateAction + | MultichainBalancesControllerUpdateBalancesAction; + +/** + * Events emitted by {@link MultichainBalancesController}. + */ +export type MultichainBalancesControllerEvents = + MultichainBalancesControllerStateChange; + +/** + * Actions that this controller is allowed to call. + */ +type AllowedActions = + | HandleSnapRequest + | AccountsControllerListMultichainAccountsAction; + +/** + * Events that this controller is allowed to subscribe. + */ +type AllowedEvents = + | AccountsControllerAccountAddedEvent + | AccountsControllerAccountRemovedEvent; + +/** + * Messenger type for the MultichainBalancesController. + */ +export type MultichainBalancesControllerMessenger = + RestrictedControllerMessenger< + typeof controllerName, + MultichainBalancesControllerActions | AllowedActions, + MultichainBalancesControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] + >; + +/** + * {@link MultichainBalancesController}'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. + */ +const balancesControllerMetadata = { + balances: { + persist: true, + anonymous: false, + }, +}; + +/** + * The MultichainBalancesController is responsible for fetching and caching account + * balances. + */ +export class MultichainBalancesController extends BaseController< + typeof controllerName, + MultichainBalancesControllerState, + MultichainBalancesControllerMessenger +> { + #tracker: BalancesTracker; + + constructor({ + messenger, + state = {}, + }: { + messenger: MultichainBalancesControllerMessenger; + state?: Partial; + }) { + super({ + messenger, + name: controllerName, + metadata: balancesControllerMetadata, + state: { + ...getDefaultMultichainBalancesControllerState(), + ...state, + }, + }); + + this.#tracker = new BalancesTracker( + async (accountId: string) => await this.#updateBalance(accountId), + ); + + // Register all non-EVM accounts into the tracker + for (const account of this.#listAccounts()) { + if (this.#isNonEvmAccount(account)) { + this.#tracker.track(account.id, getBlockTimeForAccount(account.type)); + } + } + + this.messagingSystem.subscribe( + 'AccountsController:accountAdded', + (account) => this.#handleOnAccountAdded(account), + ); + this.messagingSystem.subscribe( + 'AccountsController:accountRemoved', + (account) => this.#handleOnAccountRemoved(account), + ); + } + + /** + * Starts the polling process. + */ + start(): void { + this.#tracker.start(); + } + + /** + * Stops the polling process. + */ + stop(): void { + this.#tracker.stop(); + } + + /** + * Updates the balances of one account. This method doesn't return + * anything, but it updates the state of the controller. + * + * @param accountId - The account ID. + */ + async updateBalance(accountId: string): Promise { + // NOTE: No need to track the account here, since we start tracking those when + // the "AccountsController:accountAdded" is fired. + await this.#tracker.updateBalance(accountId); + } + + /** + * Updates the balances of all supported accounts. This method doesn't return + * anything, but it updates the state of the controller. + */ + async updateBalances(): Promise { + await this.#tracker.updateBalances(); + } + + /** + * Lists the multichain accounts coming from the `AccountsController`. + * + * @returns A list of multichain accounts. + */ + #listMultichainAccounts(): InternalAccount[] { + return this.messagingSystem.call( + 'AccountsController:listMultichainAccounts', + ); + } + + /** + * Lists the accounts that we should get balances for. + * + * @returns A list of accounts that we should get balances for. + */ + #listAccounts(): InternalAccount[] { + const accounts = this.#listMultichainAccounts(); + + return accounts.filter((account) => this.#isNonEvmAccount(account)); + } + + /** + * Get a non-EVM account from its ID. + * + * @param accountId - The account ID. + * @returns The non-EVM account. + */ + #getAccount(accountId: string): InternalAccount { + const account: InternalAccount | undefined = this.#listAccounts().find( + (multichainAccount) => multichainAccount.id === accountId, + ); + + if (!account) { + throw new Error(`Unknown account: ${accountId}`); + } + + return account; + } + + /** + * Updates the balances of one account. This method doesn't return + * anything, but it updates the state of the controller. + * + * @param accountId - The account ID. + */ + + async #updateBalance(accountId: string) { + const account = this.#getAccount(accountId); + + if (account.metadata.snap) { + const scope = getScopeForAccount(account); + const assetTypes = NETWORK_ASSETS_MAP[scope]; + + const accountBalance = await this.#getBalances( + account.id, + account.metadata.snap.id, + assetTypes, + ); + + this.update((state: Draft) => { + state.balances[accountId] = accountBalance; + }); + } + } + + /** + * Checks for non-EVM accounts. + * + * @param account - The new account to be checked. + * @returns True if the account is a non-EVM account, false otherwise. + */ + #isNonEvmAccount(account: InternalAccount): boolean { + return ( + !isEvmAccountType(account.type) && + // Non-EVM accounts are backed by a Snap for now + account.metadata.snap !== undefined + ); + } + + /** + * Handles changes when a new account has been added. + * + * @param account - The new account being added. + */ + async #handleOnAccountAdded(account: InternalAccount): Promise { + if (!this.#isNonEvmAccount(account)) { + // Nothing to do here for EVM accounts + return; + } + + this.#tracker.track(account.id, getBlockTimeForAccount(account.type)); + // NOTE: Unfortunately, we cannot update the balance right away here, because + // messenger's events are running synchronously and fetching the balance is + // asynchronous. + // Updating the balance here would resume at some point but the event emitter + // will not `await` this (so we have no real control "when" the balance will + // really be updated), see: + // - https://github.com/MetaMask/core/blob/v213.0.0/packages/accounts-controller/src/AccountsController.ts#L1036-L1039 + } + + /** + * Handles changes when a new account has been removed. + * + * @param accountId - The account ID being removed. + */ + async #handleOnAccountRemoved(accountId: string): Promise { + if (this.#tracker.isTracked(accountId)) { + this.#tracker.untrack(accountId); + } + + if (accountId in this.state.balances) { + this.update((state: Draft) => { + delete state.balances[accountId]; + }); + } + } + + /** + * Get the balances for an account. + * + * @param accountId - ID of the account to get balances for. + * @param snapId - ID of the Snap which manages the account. + * @param assetTypes - Array of asset types to get balances for. + * @returns A map of asset types to balances. + */ + async #getBalances( + accountId: string, + snapId: string, + assetTypes: CaipAssetType[], + ): Promise> { + return await this.#getClient(snapId).getAccountBalances( + accountId, + assetTypes, + ); + } + + /** + * Gets a `KeyringClient` for a Snap. + * + * @param snapId - ID of the Snap to get the client for. + * @returns A `KeyringClient` for the Snap. + */ + #getClient(snapId: string): KeyringClient { + return new KeyringClient({ + send: async (request: JsonRpcRequest) => + (await this.messagingSystem.call('SnapController:handleRequest', { + snapId: snapId as SnapId, + origin: 'metamask', + handler: HandlerType.OnKeyringRequest, + request, + })) as Promise, + }); + } +} diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts new file mode 100644 index 00000000000..aba0e4041ba --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts @@ -0,0 +1,118 @@ +import { PollerError } from './error'; +import { Poller } from './Poller'; + +jest.useFakeTimers(); + +const interval = 1000; +const intervalPlus100ms = interval + 100; + +describe('Poller', () => { + let callback: jest.Mock, []>; + + beforeEach(() => { + callback = jest.fn().mockResolvedValue(undefined); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls the callback function after the specified interval', async () => { + const poller = new Poller(callback, interval); + poller.start(); + jest.advanceTimersByTime(intervalPlus100ms); + poller.stop(); + + // Wait for all promises to resolve + await Promise.resolve(); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('does not call the callback function if stopped before the interval', async () => { + const poller = new Poller(callback, interval); + poller.start(); + poller.stop(); + jest.advanceTimersByTime(intervalPlus100ms); + + // Wait for all promises to resolve + await Promise.resolve(); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('calls the callback function multiple times if started and stopped multiple times', async () => { + const poller = new Poller(callback, interval); + poller.start(); + jest.advanceTimersByTime(intervalPlus100ms); + poller.stop(); + jest.advanceTimersByTime(intervalPlus100ms); + poller.start(); + jest.advanceTimersByTime(intervalPlus100ms); + poller.stop(); + + // Wait for all promises to resolve + await Promise.resolve(); + + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('does not call the callback if the poller is stopped before the interval has passed', async () => { + const poller = new Poller(callback, interval); + poller.start(); + // Wait for some time, but stop before reaching the `interval` timeout + jest.advanceTimersByTime(interval / 2); + poller.stop(); + + // Wait for all promises to resolve + await Promise.resolve(); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('does not start a new interval if already running', async () => { + const poller = new Poller(callback, interval); + poller.start(); + poller.start(); // Attempt to start again + jest.advanceTimersByTime(intervalPlus100ms); + poller.stop(); + + // Wait for all promises to resolve + await Promise.resolve(); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('can stop multiple times without issues', async () => { + const poller = new Poller(callback, interval); + poller.start(); + jest.advanceTimersByTime(interval / 2); + poller.stop(); + poller.stop(); // Attempt to stop again + jest.advanceTimersByTime(intervalPlus100ms); + + // Wait for all promises to resolve + await Promise.resolve(); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('catches and logs a PollerError when callback throws an error', async () => { + const mockCallback = jest.fn().mockRejectedValue(new Error('Test error')); + const poller = new Poller(mockCallback, 1000); + const spyConsoleError = jest.spyOn(console, 'error'); + + poller.start(); + + // Fast-forward time to trigger the interval + jest.advanceTimersByTime(1000); + + // Wait for the promise to be handled + await Promise.resolve(); + + expect(mockCallback).toHaveBeenCalled(); + expect(spyConsoleError).toHaveBeenCalledWith(new PollerError('Test error')); + + poller.stop(); + }); +}); diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts new file mode 100644 index 00000000000..c0167790c8d --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts @@ -0,0 +1,34 @@ +import { PollerError } from './error'; + +export class Poller { + #interval: number; + + #callback: () => Promise; + + #handle: NodeJS.Timeout | undefined = undefined; + + constructor(callback: () => Promise, interval: number) { + this.#interval = interval; + this.#callback = callback; + } + + start() { + if (this.#handle) { + return; + } + + this.#handle = setInterval(() => { + this.#callback().catch((err) => { + console.error(new PollerError(err.message)); + }); + }, this.#interval); + } + + stop() { + if (!this.#handle) { + return; + } + clearInterval(this.#handle); + this.#handle = undefined; + } +} diff --git a/packages/assets-controllers/src/MultichainBalancesController/constants.ts b/packages/assets-controllers/src/MultichainBalancesController/constants.ts new file mode 100644 index 00000000000..81aebf8fbf8 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/constants.ts @@ -0,0 +1,44 @@ +import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; + +/** + * The network identifiers for supported networks in CAIP-2 format. + * Note: This is a temporary workaround until we have a more robust + * solution for network identifiers. + */ +export enum MultichainNetworks { + Bitcoin = 'bip122:000000000019d6689c085ae165831e93', + BitcoinTestnet = 'bip122:000000000933ea01ad0ee984209779ba', + Solana = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + SolanaDevnet = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', + SolanaTestnet = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z', +} + +export enum MultichainNativeAssets { + Bitcoin = `${MultichainNetworks.Bitcoin}/slip44:0`, + BitcoinTestnet = `${MultichainNetworks.BitcoinTestnet}/slip44:0`, + Solana = `${MultichainNetworks.Solana}/slip44:501`, + SolanaDevnet = `${MultichainNetworks.SolanaDevnet}/slip44:501`, + SolanaTestnet = `${MultichainNetworks.SolanaTestnet}/slip44:501`, +} + +const BITCOIN_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds +const SOLANA_AVG_BLOCK_TIME = 400; // 400 milliseconds + +export const BALANCE_UPDATE_INTERVALS = { + // NOTE: We set an interval of half the average block time for bitcoin + // to mitigate when our interval is de-synchronized with the actual block time. + [BtcAccountType.P2wpkh]: BITCOIN_AVG_BLOCK_TIME / 2, + [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, +}; + +/** + * Maps network identifiers to their corresponding native asset types. + * Each network is mapped to an array containing its native asset for consistency. + */ +export const NETWORK_ASSETS_MAP: Record = { + [MultichainNetworks.Solana]: [MultichainNativeAssets.Solana], + [MultichainNetworks.SolanaTestnet]: [MultichainNativeAssets.SolanaTestnet], + [MultichainNetworks.SolanaDevnet]: [MultichainNativeAssets.SolanaDevnet], + [MultichainNetworks.Bitcoin]: [MultichainNativeAssets.Bitcoin], + [MultichainNetworks.BitcoinTestnet]: [MultichainNativeAssets.BitcoinTestnet], +}; diff --git a/packages/assets-controllers/src/MultichainBalancesController/error.test.ts b/packages/assets-controllers/src/MultichainBalancesController/error.test.ts new file mode 100644 index 00000000000..d94b5a37125 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/error.test.ts @@ -0,0 +1,23 @@ +import { BalancesTrackerError, PollerError } from './error'; + +describe('BalancesTrackerError', () => { + it('creates an instance of BalancesTrackerError with the correct message and name', () => { + const message = 'Test BalancesTrackerError message'; + const error = new BalancesTrackerError(message); + + expect(error).toBeInstanceOf(BalancesTrackerError); + expect(error.message).toBe(message); + expect(error.name).toBe('BalancesTrackerError'); + }); +}); + +describe('PollerError', () => { + it('creates an instance of PollerError with the correct message and name', () => { + const message = 'Test PollerError message'; + const error = new PollerError(message); + + expect(error).toBeInstanceOf(PollerError); + expect(error.message).toBe(message); + expect(error.name).toBe('PollerError'); + }); +}); diff --git a/packages/assets-controllers/src/MultichainBalancesController/error.ts b/packages/assets-controllers/src/MultichainBalancesController/error.ts new file mode 100644 index 00000000000..22229fb8e80 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/error.ts @@ -0,0 +1,13 @@ +export class BalancesTrackerError extends Error { + constructor(message: string) { + super(message); + this.name = 'BalancesTrackerError'; + } +} + +export class PollerError extends Error { + constructor(message: string) { + super(message); + this.name = 'PollerError'; + } +} diff --git a/packages/assets-controllers/src/MultichainBalancesController/index.ts b/packages/assets-controllers/src/MultichainBalancesController/index.ts new file mode 100644 index 00000000000..4b000464b17 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/index.ts @@ -0,0 +1,17 @@ +export { BalancesTracker } from './BalancesTracker'; +export { MultichainBalancesController } from './MultichainBalancesController'; +export { + BALANCE_UPDATE_INTERVALS, + NETWORK_ASSETS_MAP, + MultichainNetworks, + MultichainNativeAssets, +} from './constants'; +export type { + MultichainBalancesControllerState, + MultichainBalancesControllerGetStateAction, + MultichainBalancesControllerUpdateBalancesAction, + MultichainBalancesControllerStateChange, + MultichainBalancesControllerActions, + MultichainBalancesControllerEvents, + MultichainBalancesControllerMessenger, +} from './MultichainBalancesController'; diff --git a/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts new file mode 100644 index 00000000000..099ccf23c80 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts @@ -0,0 +1,197 @@ +import { + BtcAccountType, + SolAccountType, + BtcMethod, + SolMethod, + BtcScopes, + SolScopes, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { validate, Network } from 'bitcoin-address-validation'; +import { v4 as uuidv4 } from 'uuid'; + +import { MultichainNetworks, BALANCE_UPDATE_INTERVALS } from '.'; +import { + getScopeForBtcAddress, + getScopeForSolAddress, + getScopeForAccount, + getBlockTimeForAccount, +} from './utils'; + +const mockBtcAccount = { + address: 'bc1qssdcp5kvwh6nghzg9tuk99xsflwkdv4hgvq58q', + id: uuidv4(), + metadata: { + name: 'Bitcoin Account 1', + importTime: Date.now(), + keyring: { + type: KeyringTypes.snap, + }, + snap: { + id: 'mock-btc-snap', + name: 'mock-btc-snap', + enabled: true, + }, + lastSelected: 0, + }, + scopes: [BtcScopes.Namespace], + options: {}, + methods: [BtcMethod.SendBitcoin], + type: BtcAccountType.P2wpkh, +}; + +const mockSolAccount = { + address: 'nicktrLHhYzLmoVbuZQzHUTicd2sfP571orwo9jfc8c', + id: uuidv4(), + metadata: { + name: 'Solana Account 1', + importTime: Date.now(), + keyring: { + type: KeyringTypes.snap, + }, + snap: { + id: 'mock-sol-snap', + name: 'mock-sol-snap', + enabled: true, + }, + lastSelected: 0, + }, + options: { + scope: 'solana-scope', + }, + scopes: [SolScopes.Namespace], + methods: [SolMethod.SendAndConfirmTransaction], + type: SolAccountType.DataAccount, +}; + +jest.mock('bitcoin-address-validation', () => ({ + validate: jest.fn(), + Network: { + mainnet: 'mainnet', + testnet: 'testnet', + }, +})); + +describe('getScopeForBtcAddress', () => { + it('returns Bitcoin scope for a valid mainnet address', () => { + const account = { + ...mockBtcAccount, + address: 'valid-mainnet-address', + }; + (validate as jest.Mock).mockReturnValueOnce(true); + + const scope = getScopeForBtcAddress(account); + + expect(scope).toBe(MultichainNetworks.Bitcoin); + expect(validate).toHaveBeenCalledWith(account.address, Network.mainnet); + }); + + it('returns BitcoinTestnet scope for a valid testnet address', () => { + const account = { + ...mockBtcAccount, + address: 'valid-testnet-address', + }; + (validate as jest.Mock) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + + const scope = getScopeForBtcAddress(account); + + expect(scope).toBe(MultichainNetworks.BitcoinTestnet); + expect(validate).toHaveBeenCalledWith(account.address, Network.mainnet); + expect(validate).toHaveBeenCalledWith(account.address, Network.testnet); + }); + + it('throws an error for an invalid address', () => { + const account = { + ...mockBtcAccount, + address: 'invalid-address', + }; + (validate as jest.Mock) + .mockReturnValueOnce(false) + .mockReturnValueOnce(false); + + expect(() => getScopeForBtcAddress(account)).toThrow( + `Invalid Bitcoin address: ${account.address}`, + ); + expect(validate).toHaveBeenCalledWith(account.address, Network.mainnet); + expect(validate).toHaveBeenCalledWith(account.address, Network.testnet); + }); +}); + +describe('getScopeForSolAddress', () => { + it('returns the scope for a valid Solana account', () => { + const scope = getScopeForSolAddress(mockSolAccount); + + expect(scope).toBe('solana-scope'); + }); + + it('throws an error if the Solana account scope is undefined', () => { + const account = { + ...mockSolAccount, + options: {}, + }; + + expect(() => getScopeForSolAddress(account)).toThrow( + 'Solana account scope is undefined', + ); + }); +}); + +describe('getScopeForAddress', () => { + it('returns the scope for a Bitcoin account', () => { + const account = { + ...mockBtcAccount, + address: 'valid-mainnet-address', + }; + (validate as jest.Mock).mockReturnValueOnce(true); + + const scope = getScopeForAccount(account); + + expect(scope).toBe(MultichainNetworks.Bitcoin); + }); + + it('returns the scope for a Solana account', () => { + const account = { + ...mockSolAccount, + options: { scope: 'solana-scope' }, + }; + + const scope = getScopeForAccount(account); + + expect(scope).toBe('solana-scope'); + }); + + it('throws an error for an unsupported account type', () => { + const account = { + ...mockSolAccount, + type: 'unsupported-type', + }; + + // @ts-expect-error - We're testing an error case. + expect(() => getScopeForAccount(account)).toThrow( + `Unsupported non-EVM account type: ${account.type}`, + ); + }); +}); + +describe('getBlockTimeForAccount', () => { + it('returns the block time for a supported Bitcoin account', () => { + const blockTime = getBlockTimeForAccount(BtcAccountType.P2wpkh); + expect(blockTime).toBe(BALANCE_UPDATE_INTERVALS[BtcAccountType.P2wpkh]); + }); + + it('returns the block time for a supported Solana account', () => { + const blockTime = getBlockTimeForAccount(SolAccountType.DataAccount); + expect(blockTime).toBe( + BALANCE_UPDATE_INTERVALS[SolAccountType.DataAccount], + ); + }); + + it('throws an error for an unsupported account type', () => { + const unsupportedAccountType = 'unsupported-type'; + expect(() => getBlockTimeForAccount(unsupportedAccountType)).toThrow( + `Unsupported account type for balance tracking: ${unsupportedAccountType}`, + ); + }); +}); diff --git a/packages/assets-controllers/src/MultichainBalancesController/utils.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.ts new file mode 100644 index 00000000000..205cca8fc33 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.ts @@ -0,0 +1,77 @@ +import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import { validate, Network } from 'bitcoin-address-validation'; + +import { MultichainNetworks, BALANCE_UPDATE_INTERVALS } from './constants'; + +/** + * Gets the scope for a specific and supported Bitcoin account. + * Note: This is a temporary method and will be replaced by a more robust solution + * once the new `account.scopes` is available in the `@metamask/keyring-api` module. + * + * @param account - Bitcoin account + * @returns The scope for the given account. + */ +export const getScopeForBtcAddress = (account: InternalAccount): string => { + if (validate(account.address, Network.mainnet)) { + return MultichainNetworks.Bitcoin; + } + + if (validate(account.address, Network.testnet)) { + return MultichainNetworks.BitcoinTestnet; + } + + throw new Error(`Invalid Bitcoin address: ${account.address}`); +}; + +/** + * Gets the scope for a specific and supported Solana account. + * Note: This is a temporary method and will be replaced by a more robust solution + * once the new `account.scopes` is available in the `keyring-api`. + * + * @param account - Solana account + * @returns The scope for the given account. + */ +export const getScopeForSolAddress = (account: InternalAccount): string => { + // For Solana accounts, we know we have a `scope` on the account's `options` bag. + if (!account.options.scope) { + throw new Error('Solana account scope is undefined'); + } + return account.options.scope as string; +}; + +/** + * Get the scope for a given address. + * Note: This is a temporary method and will be replaced by a more robust solution + * once the new `account.scopes` is available in the `keyring-api`. + * + * @param account - The account to get the scope for. + * @returns The scope for the given account. + */ +export const getScopeForAccount = (account: InternalAccount): string => { + switch (account.type) { + case BtcAccountType.P2wpkh: + return getScopeForBtcAddress(account); + case SolAccountType.DataAccount: + return getScopeForSolAddress(account); + default: + throw new Error(`Unsupported non-EVM account type: ${account.type}`); + } +}; + +/** + * Gets the block time for a given account. + * + * @param accountType - The account type to get the block time for. + * @returns The block time for the account. + */ +export const getBlockTimeForAccount = (accountType: string): number => { + if (accountType in BALANCE_UPDATE_INTERVALS) { + return BALANCE_UPDATE_INTERVALS[ + accountType as keyof typeof BALANCE_UPDATE_INTERVALS + ]; + } + throw new Error( + `Unsupported account type for balance tracking: ${accountType}`, + ); +}; diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index d9a8857aabb..410054b59e9 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -148,3 +148,21 @@ export type { RatesControllerPollingStartedEvent, RatesControllerPollingStoppedEvent, } from './RatesController'; +export { + BalancesTracker, + MultichainBalancesController, + // constants + BALANCE_UPDATE_INTERVALS, + NETWORK_ASSETS_MAP, + MultichainNetworks, + MultichainNativeAssets, +} from './MultichainBalancesController'; +export type { + MultichainBalancesControllerState, + MultichainBalancesControllerGetStateAction, + MultichainBalancesControllerUpdateBalancesAction, + MultichainBalancesControllerStateChange, + MultichainBalancesControllerActions, + MultichainBalancesControllerEvents, + MultichainBalancesControllerMessenger, +} from './MultichainBalancesController'; diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md index 78b46ea5479..7e6a8261960 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.1.1] + +### Changed + +- Bump `@metamask/utils` from `^10.0.0` to `^11.0.1` ([#5080](https://github.com/MetaMask/core/pull/5080)) + ## [7.1.0] ### Changed @@ -280,7 +286,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.1.1...HEAD +[7.1.1]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.1.0...@metamask/base-controller@7.1.1 [7.1.0]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.0.2...@metamask/base-controller@7.1.0 [7.0.2]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.0.1...@metamask/base-controller@7.0.2 [7.0.1]: https://github.com/MetaMask/core/compare/@metamask/base-controller@7.0.0...@metamask/base-controller@7.0.1 diff --git a/packages/base-controller/package.json b/packages/base-controller/package.json index 8b2c45c1f77..f1134abc881 100644 --- a/packages/base-controller/package.json +++ b/packages/base-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/base-controller", - "version": "7.1.0", + "version": "7.1.1", "description": "Provides scaffolding for controllers as well a communication system for all controllers", "keywords": [ "MetaMask", diff --git a/packages/chain-controller/package.json b/packages/chain-controller/package.json index 1224c7f6772..5e116fb01e0 100644 --- a/packages/chain-controller/package.json +++ b/packages/chain-controller/package.json @@ -47,9 +47,9 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/chain-api": "^0.1.0", - "@metamask/keyring-internal-api": "^1.1.0", + "@metamask/keyring-internal-api": "^2.0.0", "@metamask/keyring-utils": "^1.0.0", "@metamask/snaps-controllers": "^9.10.0", "@metamask/snaps-sdk": "^6.7.0", diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json index 29202699d78..786e1161db7 100644 --- a/packages/composable-controller/package.json +++ b/packages/composable-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0" + "@metamask/base-controller": "^7.1.1" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/controller-utils/CHANGELOG.md b/packages/controller-utils/CHANGELOG.md index 2bc867e5b33..5774eba9cab 100644 --- a/packages/controller-utils/CHANGELOG.md +++ b/packages/controller-utils/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.4.5] + +### Changed + +- Bump `@metamask/utils` from `^10.0.0` to `^11.0.1` ([#5080](https://github.com/MetaMask/core/pull/5080)) + ## [11.4.4] ### Fixed @@ -438,7 +444,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.4...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.5...HEAD +[11.4.5]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.4...@metamask/controller-utils@11.4.5 [11.4.4]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.3...@metamask/controller-utils@11.4.4 [11.4.3]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.2...@metamask/controller-utils@11.4.3 [11.4.2]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@11.4.1...@metamask/controller-utils@11.4.2 diff --git a/packages/controller-utils/package.json b/packages/controller-utils/package.json index 3fb36cf2abf..8f67f9cfc2b 100644 --- a/packages/controller-utils/package.json +++ b/packages/controller-utils/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/controller-utils", - "version": "11.4.4", + "version": "11.4.5", "description": "Data and convenience functions shared by multiple packages", "keywords": [ "MetaMask", diff --git a/packages/ens-controller/package.json b/packages/ens-controller/package.json index a2701967581..03fdaaf1069 100644 --- a/packages/ens-controller/package.json +++ b/packages/ens-controller/package.json @@ -48,8 +48,8 @@ }, "dependencies": { "@ethersproject/providers": "^5.7.0", - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/utils": "^11.0.1", "punycode": "^2.1.1" }, diff --git a/packages/gas-fee-controller/package.json b/packages/gas-fee-controller/package.json index 7161bccfedf..5e2853f34c1 100644 --- a/packages/gas-fee-controller/package.json +++ b/packages/gas-fee-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/eth-query": "^4.0.0", "@metamask/ethjs-unit": "^0.3.0", "@metamask/polling-controller": "^12.0.2", diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 493c7709f76..6718b477c07 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -49,13 +49,13 @@ "dependencies": { "@ethereumjs/util": "^8.1.0", "@keystonehq/metamask-airgapped-keyring": "^0.14.1", - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/browser-passworder": "^4.3.0", "@metamask/eth-hd-keyring": "^7.0.4", "@metamask/eth-sig-util": "^8.0.0", "@metamask/eth-simple-keyring": "^6.0.5", "@metamask/keyring-api": "^13.0.0", - "@metamask/keyring-internal-api": "^1.1.0", + "@metamask/keyring-internal-api": "^2.0.0", "@metamask/message-manager": "^11.0.3", "@metamask/utils": "^11.0.1", "async-mutex": "^0.5.0", diff --git a/packages/logging-controller/package.json b/packages/logging-controller/package.json index 692567481be..6c876367298 100644 --- a/packages/logging-controller/package.json +++ b/packages/logging-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/packages/message-manager/package.json b/packages/message-manager/package.json index 84deb4a416e..37cd315da25 100644 --- a/packages/message-manager/package.json +++ b/packages/message-manager/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/eth-sig-util": "^8.0.0", "@metamask/utils": "^11.0.1", "@types/uuid": "^8.3.0", diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 9b2ac4fef98..a1dedd8ee86 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/api-specs": "^0.10.12", - "@metamask/controller-utils": "^11.4.4", + "@metamask/controller-utils": "^11.4.5", "@metamask/eth-json-rpc-filters": "^9.0.0", "@metamask/rpc-errors": "^7.0.2", "@metamask/utils": "^11.0.1", @@ -57,7 +57,7 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/network-controller": "^22.1.1", - "@metamask/permission-controller": "^11.0.4", + "@metamask/permission-controller": "^11.0.5", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/name-controller/package.json b/packages/name-controller/package.json index eef8daa783a..b724d0c73b2 100644 --- a/packages/name-controller/package.json +++ b/packages/name-controller/package.json @@ -48,8 +48,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/utils": "^11.0.1", "async-mutex": "^0.5.0" }, diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 2201233c5ee..295c9f493ab 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/eth-block-tracker": "^11.0.3", "@metamask/eth-json-rpc-infura": "^10.0.0", "@metamask/eth-json-rpc-middleware": "^15.0.1", diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index bb31123f60b..8965b28be6a 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -100,8 +100,8 @@ }, "dependencies": { "@contentful/rich-text-html-renderer": "^16.5.2", - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/utils": "^11.0.1", "bignumber.js": "^9.1.2", "firebase": "^10.11.0", @@ -113,7 +113,7 @@ "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.0.2", - "@metamask/profile-sync-controller": "^3.2.0", + "@metamask/profile-sync-controller": "^3.3.0", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", "contentful": "^10.15.0", diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index 8dc8a25c525..d50033d54e6 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -7,8 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.0.5] + ### Changed +- Remove redundant caveat validator calls ([#5062](https://github.com/MetaMask/core/pull/5062)) + - In some cases, caveats were being validated multiple times or without the + possibility of being changed. + - The intended purpose of permission and caveat validators has also been + documented. See `ARCHITECTURE.md`. +- Bump `nanoid` from `^3.1.31` to `^3.3.8` ([#5073](https://github.com/MetaMask/core/pull/5073)) +- Bump `@metamask/utils` from `^10.0.0` to `^11.0.1` ([#5080](https://github.com/MetaMask/core/pull/5080)) +- Bump `@metamask/rpc-errors` from `^7.0.0` to `^7.0.2` ([#5080](https://github.com/MetaMask/core/pull/5080)) - Bump `@metamask/base-controller` from `^7.0.0` to `^7.1.0` ([#5079](https://github.com/MetaMask/core/pull/5079)) ## [11.0.4] @@ -311,7 +321,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.4...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.5...HEAD +[11.0.5]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.4...@metamask/permission-controller@11.0.5 [11.0.4]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.3...@metamask/permission-controller@11.0.4 [11.0.3]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.2...@metamask/permission-controller@11.0.3 [11.0.2]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@11.0.1...@metamask/permission-controller@11.0.2 diff --git a/packages/permission-controller/package.json b/packages/permission-controller/package.json index a881bbf08c8..613b88c4b56 100644 --- a/packages/permission-controller/package.json +++ b/packages/permission-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/permission-controller", - "version": "11.0.4", + "version": "11.0.5", "description": "Mediates access to JSON-RPC methods, used to interact with pieces of the MetaMask stack, via middleware for json-rpc-engine", "keywords": [ "MetaMask", @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/json-rpc-engine": "^10.0.2", "@metamask/rpc-errors": "^7.0.2", "@metamask/utils": "^11.0.1", @@ -58,7 +58,7 @@ "nanoid": "^3.3.8" }, "devDependencies": { - "@metamask/approval-controller": "^7.1.1", + "@metamask/approval-controller": "^7.1.2", "@metamask/auto-changelog": "^3.4.4", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/permission-log-controller/package.json b/packages/permission-log-controller/package.json index 82808fbac8c..c127b4e6f78 100644 --- a/packages/permission-log-controller/package.json +++ b/packages/permission-log-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/json-rpc-engine": "^10.0.2", "@metamask/utils": "^11.0.1" }, diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json index 5d240f28261..ed3c6597fae 100644 --- a/packages/phishing-controller/package.json +++ b/packages/phishing-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@noble/hashes": "^1.4.0", "@types/punycode": "^2.1.0", "ethereum-cryptography": "^2.1.2", diff --git a/packages/polling-controller/package.json b/packages/polling-controller/package.json index fbb207cc18f..dddd08b1931 100644 --- a/packages/polling-controller/package.json +++ b/packages/polling-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/utils": "^11.0.1", "@types/uuid": "^8.3.0", "fast-json-stable-stringify": "^2.1.0", diff --git a/packages/preferences-controller/package.json b/packages/preferences-controller/package.json index c178161299f..4dbaf3fb422 100644 --- a/packages/preferences-controller/package.json +++ b/packages/preferences-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4" + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index 189bd555d4e..238397f17cd 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.3.0] + +### Added + +- Add a `customProvider` option to the sdk `JwtBearerAuth` class ([#5105](https://github.com/MetaMask/core/pull/5105)) + +### Changed + +- Bump `eslint` to `^9.11.1` and migrate to flat config ([#4727](https://github.com/MetaMask/core/pull/4727)) +- Bump `@metamask/keyring-api` from `^12.0.0` to `^13.0.0` and `@metamask/keyring-internal-api` from `^1.0.0` to `^1.1.0` ([#5066](https://github.com/MetaMask/core/pull/5066)) + ## [3.2.0] ### Added @@ -389,7 +400,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@3.2.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.3.0...HEAD +[3.3.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.2.0...@metamask/profile-sync-controller@3.3.0 [3.2.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.1.1...@metamask/profile-sync-controller@3.2.0 [3.1.1]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.1.0...@metamask/profile-sync-controller@3.1.1 [3.1.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.0.0...@metamask/profile-sync-controller@3.1.0 diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index b1ab48f856e..a5ae286be33 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": "3.2.0", + "version": "3.3.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", @@ -100,7 +100,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/keyring-api": "^13.0.0", "@metamask/keyring-controller": "^19.0.2", "@metamask/network-controller": "^22.1.1", @@ -117,7 +117,7 @@ "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/accounts-controller": "^20.0.2", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-internal-api": "^1.1.0", + "@metamask/keyring-internal-api": "^2.0.0", "@metamask/providers": "^18.1.1", "@metamask/snaps-controllers": "^9.10.0", "@types/jest": "^27.4.1", diff --git a/packages/profile-sync-controller/src/sdk/__fixtures__/test-utils.ts b/packages/profile-sync-controller/src/sdk/__fixtures__/test-utils.ts index e163369bc57..421fa6b3618 100644 --- a/packages/profile-sync-controller/src/sdk/__fixtures__/test-utils.ts +++ b/packages/profile-sync-controller/src/sdk/__fixtures__/test-utils.ts @@ -1,3 +1,5 @@ +import type { Eip1193Provider } from 'ethers'; + import { Env, Platform } from '../../shared/env'; import { JwtBearerAuth } from '../authentication'; import type { @@ -5,6 +7,7 @@ import type { AuthStorageOptions, } from '../authentication-jwt-bearer/types'; import { AuthType } from '../authentication-jwt-bearer/types'; +import { SNAP_ORIGIN } from '../utils/messaging-signing-snap-requests'; // Alias mocking variables with ANY to test runtime safety. // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -42,6 +45,7 @@ const mockAuthOptions = () => { * @param mockPublicKey - provide the mock public key * @param authOptionsOverride - overrides * @param authOptionsOverride.signing - override auth signing + * @param authOptionsOverride.customProvider - override custom provider * @returns Auth instance */ export function arrangeAuth( @@ -49,6 +53,7 @@ export function arrangeAuth( mockPublicKey: string, authOptionsOverride?: { signing?: AuthSigningOptions; + customProvider?: Eip1193Provider; }, ) { const authOptionsMock = mockAuthOptions(); @@ -67,6 +72,7 @@ export function arrangeAuth( type: AuthType.SRP, }, { + customProvider: authOptionsOverride?.customProvider, storage: { getLoginResponse: authOptionsMock.mockGetLoginResponse, setLoginResponse: authOptionsMock.mockSetLoginResponse, @@ -103,3 +109,16 @@ export function arrangeAuth( throw new Error('Unable to arrange auth mock for invalid auth type'); } + +/** + * Mock utility - creates a mock provider + * @returns mock provider + */ +export const arrangeMockProvider = () => { + const mockRequest = jest.fn().mockResolvedValue({ [SNAP_ORIGIN]: {} }); + const mockProvider: Eip1193Provider = { + request: mockRequest, + }; + + return { mockProvider, mockRequest }; +}; diff --git a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts index 8d766e75f3f..652acea2459 100644 --- a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts +++ b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts @@ -1,3 +1,5 @@ +import type { Eip1193Provider } from 'ethers'; + import { ValidationError } from '../errors'; import { getMetaMaskProviderEIP6963 } from '../utils/eip-6963-metamask-provider'; import { @@ -32,37 +34,49 @@ const getDefaultEIP6963Provider = async () => { return provider; }; -const defaultEIP6963SigningOptions: AuthSigningOptions = { +const getDefaultEIP6963SigningOptions = ( + customProvider?: Eip1193Provider, +): AuthSigningOptions => ({ getIdentifier: async (): Promise => { - const provider = await getDefaultEIP6963Provider(); + const provider = customProvider ?? (await getDefaultEIP6963Provider()); return await MESSAGE_SIGNING_SNAP.getPublicKey(provider); }, signMessage: async (message: string): Promise => { - const provider = await getDefaultEIP6963Provider(); + const provider = customProvider ?? (await getDefaultEIP6963Provider()); if (!message.startsWith('metamask:')) { throw new ValidationError('message must start with "metamask:"'); } const formattedMessage = message as `metamask:${string}`; return await MESSAGE_SIGNING_SNAP.signMessage(provider, formattedMessage); }, -}; +}); export class SRPJwtBearerAuth implements IBaseAuth { #config: AuthConfig; #options: Required; + #customProvider?: Eip1193Provider; + constructor( config: AuthConfig & { type: AuthType.SRP }, - options: JwtBearerAuth_SRP_Options, + options: JwtBearerAuth_SRP_Options & { customProvider?: Eip1193Provider }, ) { this.#config = config; + this.#customProvider = options.customProvider; this.#options = { storage: options.storage, - signing: options.signing ?? defaultEIP6963SigningOptions, + signing: + options.signing ?? + getDefaultEIP6963SigningOptions(this.#customProvider), }; } + setCustomProvider(provider: Eip1193Provider) { + this.#customProvider = provider; + this.#options.signing = getDefaultEIP6963SigningOptions(provider); + } + async getAccessToken(): Promise { const session = await this.#getAuthSession(); if (session) { @@ -92,7 +106,8 @@ export class SRPJwtBearerAuth implements IBaseAuth { } async isSnapConnected(): Promise { - const provider = await getMetaMaskProviderEIP6963(); + const provider = + this.#customProvider ?? (await getDefaultEIP6963Provider()); if (!provider) { return false; } @@ -102,7 +117,9 @@ export class SRPJwtBearerAuth implements IBaseAuth { } async connectSnap(): Promise { - const provider = await getDefaultEIP6963Provider(); + const provider = + this.#customProvider ?? (await getDefaultEIP6963Provider()); + const res = await connectSnap(provider); return res; } diff --git a/packages/profile-sync-controller/src/sdk/authentication.test.ts b/packages/profile-sync-controller/src/sdk/authentication.test.ts index 5adc2edbd3b..d902ccf04a8 100644 --- a/packages/profile-sync-controller/src/sdk/authentication.test.ts +++ b/packages/profile-sync-controller/src/sdk/authentication.test.ts @@ -5,7 +5,7 @@ import { arrangeAuthAPIs, } from './__fixtures__/mock-auth'; import type { MockVariable } from './__fixtures__/test-utils'; -import { arrangeAuth } from './__fixtures__/test-utils'; +import { arrangeAuth, arrangeMockProvider } from './__fixtures__/test-utils'; import { JwtBearerAuth } from './authentication'; import type { LoginResponse, Pair } from './authentication-jwt-bearer/types'; import { @@ -143,6 +143,28 @@ describe('Authentication - constructor()', () => { ); }).toThrow(UnsupportedAuthTypeError); }); + + it('supports using a custom provider as a constructor option', async () => { + const { auth } = arrangeAuth('SRP', MOCK_SRP, { + customProvider: arrangeMockProvider().mockProvider, + }); + + await auth.connectSnap(); + const isSnapConnected = await auth.isSnapConnected(); + + expect(isSnapConnected).toBe(true); + }); + + it('supports using a custom provider set at a later point in time', async () => { + const { auth } = arrangeAuth('SRP', MOCK_SRP); + + auth.setCustomProvider(arrangeMockProvider().mockProvider); + + await auth.connectSnap(); + const isSnapConnected = await auth.isSnapConnected(); + + expect(isSnapConnected).toBe(true); + }); }); describe('Authentication - SRP Flow - getAccessToken() & getUserProfile()', () => { diff --git a/packages/profile-sync-controller/src/sdk/authentication.ts b/packages/profile-sync-controller/src/sdk/authentication.ts index 119310cc7db..36d4c23e7a6 100644 --- a/packages/profile-sync-controller/src/sdk/authentication.ts +++ b/packages/profile-sync-controller/src/sdk/authentication.ts @@ -1,3 +1,5 @@ +import type { Eip1193Provider } from 'ethers'; + import type { Env } from '../shared/env'; import { SIWEJwtBearerAuth } from './authentication-jwt-bearer/flow-siwe'; import { SRPJwtBearerAuth } from './authentication-jwt-bearer/flow-srp'; @@ -44,6 +46,11 @@ export class JwtBearerAuth implements SIWEInterface, SRPInterface { throw new UnsupportedAuthTypeError('unsupported auth type'); } + setCustomProvider(provider: Eip1193Provider) { + this.#assertSRP(this.#type, this.#sdk); + this.#sdk.setCustomProvider(provider); + } + async getAccessToken(): Promise { return await this.#sdk.getAccessToken(); } diff --git a/packages/profile-sync-controller/src/sdk/utils/messaging-signing-snap-requests.test.ts b/packages/profile-sync-controller/src/sdk/utils/messaging-signing-snap-requests.test.ts index b7db331ce83..7bebccbb9da 100644 --- a/packages/profile-sync-controller/src/sdk/utils/messaging-signing-snap-requests.test.ts +++ b/packages/profile-sync-controller/src/sdk/utils/messaging-signing-snap-requests.test.ts @@ -1,6 +1,7 @@ -import type { Eip1193Provider } from 'ethers'; - -import type { MockVariable } from '../__fixtures__/test-utils'; +import { + arrangeMockProvider, + type MockVariable, +} from '../__fixtures__/test-utils'; import type { Snap } from './messaging-signing-snap-requests'; import { MESSAGE_SIGNING_SNAP, @@ -83,16 +84,3 @@ describe('MESSAGE_SIGNING_SNAP.signMessage() tests', () => { expect(mockRequest).toHaveBeenCalled(); }); }); - -/** - * Mock utility - creates a mock provider - * @returns mock provider - */ -function arrangeMockProvider() { - const mockRequest = jest.fn(); - const mockProvider: Eip1193Provider = { - request: mockRequest, - }; - - return { mockProvider, mockRequest }; -} diff --git a/packages/queued-request-controller/package.json b/packages/queued-request-controller/package.json index dd484be8b1f..445ade0e630 100644 --- a/packages/queued-request-controller/package.json +++ b/packages/queued-request-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/json-rpc-engine": "^10.0.2", "@metamask/rpc-errors": "^7.0.2", "@metamask/swappable-obj-proxy": "^2.3.0", diff --git a/packages/rate-limit-controller/package.json b/packages/rate-limit-controller/package.json index 99ff957075f..660c4e5d796 100644 --- a/packages/rate-limit-controller/package.json +++ b/packages/rate-limit-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/rpc-errors": "^7.0.2", "@metamask/utils": "^11.0.1" }, diff --git a/packages/remote-feature-flag-controller/CHANGELOG.md b/packages/remote-feature-flag-controller/CHANGELOG.md index 72458566d88..20df3a0a4ca 100644 --- a/packages/remote-feature-flag-controller/CHANGELOG.md +++ b/packages/remote-feature-flag-controller/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.0] + ### Changed +- Improve user segmentation with BigInt-based random generation ([#5110](https://github.com/MetaMask/core/pull/5110)) +- Change getMetaMetricsId to only sync func type ([#5108](https://github.com/MetaMask/core/pull/5108)) - Bump `@metamask/base-controller` from `^7.0.0` to `^7.1.0` ([#5079](https://github.com/MetaMask/core/pull/5079)) ## [1.2.0] @@ -38,7 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release of the RemoteFeatureFlagController. ([#4931](https://github.com/MetaMask/core/pull/4931)) - This controller manages the retrieval and caching of remote feature flags. It fetches feature flags from a remote API, caches them, and provides methods to access and manage these flags. The controller ensures that feature flags are refreshed based on a specified interval and handles cases where the controller is disabled or the network is unavailable. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.2.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.3.0...HEAD +[1.3.0]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.2.0...@metamask/remote-feature-flag-controller@1.3.0 [1.2.0]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.1.0...@metamask/remote-feature-flag-controller@1.2.0 [1.1.0]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.0.0...@metamask/remote-feature-flag-controller@1.1.0 [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/remote-feature-flag-controller@1.0.0 diff --git a/packages/remote-feature-flag-controller/package.json b/packages/remote-feature-flag-controller/package.json index efbb16bbd6e..38128e1b2df 100644 --- a/packages/remote-feature-flag-controller/package.json +++ b/packages/remote-feature-flag-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/remote-feature-flag-controller", - "version": "1.2.0", + "version": "1.3.0", "description": "The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags", "keywords": [ "MetaMask", @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/utils": "^11.0.1", "cockatiel": "^3.1.2", "uuid": "^8.3.2" @@ -55,7 +55,7 @@ "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@metamask/auto-changelog": "^3.4.4", - "@metamask/controller-utils": "^11.4.4", + "@metamask/controller-utils": "^11.4.5", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts index 6ec5933e02a..6a8b96f3cd3 100644 --- a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts +++ b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts @@ -1,14 +1,28 @@ +import { v4 as uuidV4 } from 'uuid'; + import { generateDeterministicRandomNumber, isFeatureFlagWithScopeValue, } from './user-segmentation-utils'; -const MOCK_METRICS_IDS = [ - '123e4567-e89b-4456-a456-426614174000', - '987fcdeb-51a2-4c4b-9876-543210fedcba', - 'a1b2c3d4-e5f6-4890-abcd-ef1234567890', - 'f9e8d7c6-b5a4-4210-9876-543210fedcba', -]; +const MOCK_METRICS_IDS = { + MOBILE_VALID: '123e4567-e89b-4456-a456-426614174000', + EXTENSION_VALID: + '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420', + MOBILE_MIN: '00000000-0000-4000-8000-000000000000', + MOBILE_MAX: 'ffffffff-ffff-4fff-bfff-ffffffffffff', + EXTENSION_MIN: `0x${'0'.repeat(64) as string}`, + EXTENSION_MAX: `0x${'f'.repeat(64) as string}`, + UUID_V3: '00000000-0000-3000-8000-000000000000', + INVALID_HEX_NO_PREFIX: + '86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420', + INVALID_HEX_SHORT: + '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d13642', + INVALID_HEX_LONG: + '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d1364200', + INVALID_HEX_INVALID_CHARS: + '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d13642g', +}; const MOCK_FEATURE_FLAGS = { VALID: { @@ -28,26 +42,139 @@ const MOCK_FEATURE_FLAGS = { describe('user-segmentation-utils', () => { describe('generateDeterministicRandomNumber', () => { - it('generates consistent numbers for the same input', () => { - const result1 = generateDeterministicRandomNumber(MOCK_METRICS_IDS[0]); - const result2 = generateDeterministicRandomNumber(MOCK_METRICS_IDS[0]); + describe('Mobile client new implementation (uuidv4)', () => { + it('generates consistent results for same uuidv4', () => { + const result1 = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.MOBILE_VALID, + ); + const result2 = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.MOBILE_VALID, + ); + expect(result1).toBe(result2); + }); - expect(result1).toBe(result2); - }); + it('handles minimum uuidv4 value', () => { + const result = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.MOBILE_MIN, + ); + expect(result).toBe(0); + }); + + it('handles maximum uuidv4 value', () => { + const result = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.MOBILE_MAX, + ); + // For practical purposes, 0.999999 is functionally equivalent to 1 in this context + // the small deviation from exactly 1.0 is a limitation of floating-point arithmetic, not a bug in the logic. + expect(result).toBeCloseTo(1, 5); + }); - it('generates numbers between 0 and 1', () => { - MOCK_METRICS_IDS.forEach((id) => { - const result = generateDeterministicRandomNumber(id); + it('results a random number between 0 and 1', () => { + const result = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.MOBILE_VALID, + ); expect(result).toBeGreaterThanOrEqual(0); expect(result).toBeLessThanOrEqual(1); }); }); - it('generates different numbers for different inputs', () => { - const result1 = generateDeterministicRandomNumber(MOCK_METRICS_IDS[0]); - const result2 = generateDeterministicRandomNumber(MOCK_METRICS_IDS[1]); + describe('Mobile client old implementation and Extension client (hex string)', () => { + it('generates consistent results for same hex', () => { + const result1 = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.EXTENSION_VALID, + ); + const result2 = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.EXTENSION_VALID, + ); + expect(result1).toBe(result2); + }); + + it('handles minimum hex value', () => { + const result = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.EXTENSION_MIN, + ); + expect(result).toBe(0); + }); + + it('handles maximum hex value', () => { + const result = generateDeterministicRandomNumber( + MOCK_METRICS_IDS.EXTENSION_MAX, + ); + expect(result).toBe(1); + }); + }); + + describe('Distribution validation', () => { + it('produces uniform distribution across 1000 samples', () => { + const samples = 1000; + const buckets = 10; + const tolerance = 0.3; + const distribution = new Array(buckets).fill(0); + + // Generate samples using valid UUIDs + Array.from({ length: samples }).forEach(() => { + const uuid = uuidV4(); + const value = generateDeterministicRandomNumber(uuid); + const bucketIndex = Math.floor(value * buckets); + // Handle edge case where value === 1 + distribution[ + bucketIndex === buckets ? buckets - 1 : bucketIndex + ] += 1; + }); + + // Check distribution + const expectedPerBucket = samples / buckets; + const allowedDeviation = expectedPerBucket * tolerance; + + distribution.forEach((count) => { + const minExpected = Math.floor(expectedPerBucket - allowedDeviation); + const maxExpected = Math.ceil(expectedPerBucket + allowedDeviation); + expect(count).toBeGreaterThanOrEqual(minExpected); + expect(count).toBeLessThanOrEqual(maxExpected); + }); + }); + }); + + describe('MetaMetrics ID validation', () => { + it('throws an error if the MetaMetrics ID is empty', () => { + expect(() => generateDeterministicRandomNumber('')).toThrow( + 'MetaMetrics ID cannot be empty', + ); + }); + + it('throws an error if the MetaMetrics ID is not a valid UUIDv4', () => { + expect(() => + generateDeterministicRandomNumber(MOCK_METRICS_IDS.UUID_V3), + ).toThrow('Invalid UUID version. Expected v4, got v3'); + }); - expect(result1).not.toBe(result2); + it('throws an error if the MetaMetrics ID is not a valid hex string', () => { + expect(() => + generateDeterministicRandomNumber( + MOCK_METRICS_IDS.INVALID_HEX_NO_PREFIX, + ), + ).toThrow('Hex ID must start with 0x prefix'); + }); + + it('throws an error if the MetaMetrics ID is a short hex string', () => { + expect(() => + generateDeterministicRandomNumber(MOCK_METRICS_IDS.INVALID_HEX_SHORT), + ).toThrow('Invalid hex ID length. Expected 64 characters, got 63'); + }); + + it('throws an error if the MetaMetrics ID is a long hex string', () => { + expect(() => + generateDeterministicRandomNumber(MOCK_METRICS_IDS.INVALID_HEX_LONG), + ).toThrow('Invalid hex ID length. Expected 64 characters, got 65'); + }); + + it('throws an error if the MetaMetrics ID contains invalid hex characters', () => { + expect(() => + generateDeterministicRandomNumber( + MOCK_METRICS_IDS.INVALID_HEX_INVALID_CHARS, + ), + ).toThrow('Hex ID contains invalid characters'); + }); }); }); diff --git a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts index 481d0d21c0e..b14056c453c 100644 --- a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts +++ b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts @@ -1,25 +1,77 @@ import type { Json } from '@metamask/utils'; +import { validate as uuidValidate, version as uuidVersion } from 'uuid'; import type { FeatureFlagScopeValue } from '../remote-feature-flag-controller-types'; -/* eslint-disable no-bitwise */ +/** + * Converts a UUID string to a BigInt by removing dashes and converting to hexadecimal. + * @param uuid - The UUID string to convert + * @returns The UUID as a BigInt value + */ +function uuidStringToBigInt(uuid: string): bigint { + return BigInt(`0x${uuid.replace(/-/gu, '')}`); +} + +const MIN_UUID_V4 = '00000000-0000-4000-8000-000000000000'; +const MAX_UUID_V4 = 'ffffffff-ffff-4fff-bfff-ffffffffffff'; +const MIN_UUID_V4_BIGINT = uuidStringToBigInt(MIN_UUID_V4); +const MAX_UUID_V4_BIGINT = uuidStringToBigInt(MAX_UUID_V4); +const UUID_V4_VALUE_RANGE_BIGINT = MAX_UUID_V4_BIGINT - MIN_UUID_V4_BIGINT; + /** * Generates a deterministic random number between 0 and 1 based on a metaMetricsId. * This is useful for A/B testing and feature flag rollouts where we want * consistent group assignment for the same user. - * - * @param metaMetricsId - The unique identifier used to generate the deterministic random number - * @returns A number between 0 and 1 that is deterministic for the given metaMetricsId + * @param metaMetricsId - The unique identifier used to generate the deterministic random number. Must be either: + * - A UUIDv4 string (e.g., '123e4567-e89b-12d3-a456-426614174000' + * - A hex string with '0x' prefix (e.g., '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420') + * @returns A number between 0 and 1, deterministically generated from the input ID. + * The same input will always produce the same output. */ export function generateDeterministicRandomNumber( metaMetricsId: string, ): number { - const hash = [...metaMetricsId].reduce((acc, char) => { - const chr = char.charCodeAt(0); - return ((acc << 5) - acc + chr) | 0; - }, 0); + if (!metaMetricsId) { + throw new Error('MetaMetrics ID cannot be empty'); + } + + let idValue: bigint; + let maxValue: bigint; + + // uuidv4 format + if (uuidValidate(metaMetricsId)) { + if (uuidVersion(metaMetricsId) !== 4) { + throw new Error( + `Invalid UUID version. Expected v4, got v${uuidVersion(metaMetricsId)}`, + ); + } + idValue = uuidStringToBigInt(metaMetricsId) - MIN_UUID_V4_BIGINT; + maxValue = UUID_V4_VALUE_RANGE_BIGINT; + } else { + // hex format with 0x prefix + if (!metaMetricsId.startsWith('0x')) { + throw new Error('Hex ID must start with 0x prefix'); + } + + const cleanId = metaMetricsId.slice(2); + const EXPECTED_HEX_LENGTH = 64; // 32 bytes = 64 hex characters + + if (cleanId.length !== EXPECTED_HEX_LENGTH) { + throw new Error( + `Invalid hex ID length. Expected ${EXPECTED_HEX_LENGTH} characters, got ${cleanId.length}`, + ); + } + + if (!/^[0-9a-f]+$/iu.test(cleanId)) { + throw new Error('Hex ID contains invalid characters'); + } + + idValue = BigInt(`0x${cleanId}`); + maxValue = BigInt(`0x${'f'.repeat(cleanId.length)}`); + } - return (hash >>> 0) / 0xffffffff; + // Use BigInt division first, then convert to number to maintain precision + return Number((idValue * BigInt(1_000_000)) / maxValue) / 1_000_000; } /** diff --git a/packages/selected-network-controller/package.json b/packages/selected-network-controller/package.json index cdbda423500..443c76980bf 100644 --- a/packages/selected-network-controller/package.json +++ b/packages/selected-network-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/json-rpc-engine": "^10.0.2", "@metamask/swappable-obj-proxy": "^2.3.0", "@metamask/utils": "^11.0.1" @@ -55,7 +55,7 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/network-controller": "^22.1.1", - "@metamask/permission-controller": "^11.0.4", + "@metamask/permission-controller": "^11.0.5", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "immer": "^9.0.6", diff --git a/packages/signature-controller/package.json b/packages/signature-controller/package.json index 8531d85f860..f8f44b9ec37 100644 --- a/packages/signature-controller/package.json +++ b/packages/signature-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/eth-sig-util": "^8.0.0", "@metamask/utils": "^11.0.1", "jsonschema": "^1.4.1", @@ -56,7 +56,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/approval-controller": "^7.1.1", + "@metamask/approval-controller": "^7.1.2", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.0.2", "@metamask/logging-controller": "^6.0.3", diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 8ce6c0cc48e..0dfc3094646 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -53,8 +53,8 @@ "@ethersproject/abi": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/eth-query": "^4.0.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/nonce-tracker": "^6.0.0", @@ -70,7 +70,7 @@ "devDependencies": { "@babel/runtime": "^7.23.9", "@metamask/accounts-controller": "^20.0.2", - "@metamask/approval-controller": "^7.1.1", + "@metamask/approval-controller": "^7.1.2", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-block-tracker": "^11.0.3", "@metamask/eth-json-rpc-provider": "^4.1.7", diff --git a/packages/user-operation-controller/package.json b/packages/user-operation-controller/package.json index dd34719e67e..7e16ea14386 100644 --- a/packages/user-operation-controller/package.json +++ b/packages/user-operation-controller/package.json @@ -48,8 +48,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", - "@metamask/controller-utils": "^11.4.4", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/eth-query": "^4.0.0", "@metamask/polling-controller": "^12.0.2", "@metamask/rpc-errors": "^7.0.2", @@ -61,7 +61,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/approval-controller": "^7.1.1", + "@metamask/approval-controller": "^7.1.2", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-block-tracker": "^11.0.3", "@metamask/gas-fee-controller": "^22.0.2", diff --git a/scripts/create-package/utils.test.ts b/scripts/create-package/utils.test.ts index 1c5615fb95b..834f25ad7bf 100644 --- a/scripts/create-package/utils.test.ts +++ b/scripts/create-package/utils.test.ts @@ -87,9 +87,10 @@ describe('create-package/utils', () => { nodeVersions: '>=18.0.0', }; - (fs.promises.stat as jest.Mock).mockResolvedValueOnce({ - isDirectory: () => false, - }); + const mockError = new Error('Not found') as NodeJS.ErrnoException; + mockError.code = 'ENOENT'; + + jest.spyOn(fs.promises, 'stat').mockRejectedValue(mockError); (fsUtils.readAllFiles as jest.Mock).mockResolvedValueOnce({ 'src/index.ts': 'export default 42;', @@ -170,9 +171,7 @@ describe('create-package/utils', () => { nodeVersions: '20.0.0', }; - (fs.promises.stat as jest.Mock).mockResolvedValueOnce({ - isDirectory: () => true, - }); + (fs.promises.stat as jest.Mock).mockResolvedValue({}); await expect( finalizeAndWriteData(packageData, monorepoFileData), @@ -181,5 +180,34 @@ describe('create-package/utils', () => { expect(fs.promises.mkdir).not.toHaveBeenCalled(); expect(fs.promises.writeFile).not.toHaveBeenCalled(); }); + + it('throws if fs.stat fails with an error other than ENOENT', async () => { + const mockError = new Error('Permission denied') as NodeJS.ErrnoException; + mockError.code = 'EACCES'; + + jest.spyOn(fs.promises, 'stat').mockRejectedValue(mockError); + + const packageData: PackageData = { + name: '@metamask/foo', + description: 'A foo package.', + directoryName: 'foo', + nodeVersions: '20.0.0', + currentYear: '2023', + }; + + const monorepoFileData = { + tsConfig: { + references: [{ path: './packages/bar' }], + }, + tsConfigBuild: { + references: [{ path: './packages/bar' }], + }, + nodeVersions: '20.0.0', + }; + + await expect( + finalizeAndWriteData(packageData, monorepoFileData), + ).rejects.toThrow('Permission denied'); + }); }); }); diff --git a/scripts/create-package/utils.ts b/scripts/create-package/utils.ts index 85c24d10a8d..935342ec2aa 100644 --- a/scripts/create-package/utils.ts +++ b/scripts/create-package/utils.ts @@ -94,8 +94,13 @@ export async function finalizeAndWriteData( monorepoFileData: MonorepoFileData, ) { const packagePath = path.join(PACKAGES_PATH, packageData.directoryName); - if ((await fs.stat(packagePath)).isDirectory()) { + try { + await fs.stat(packagePath); throw new Error(`The package directory already exists: ${packagePath}`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } } console.log('Writing package and monorepo files...'); @@ -136,7 +141,7 @@ async function writeJsonFile( ): Promise { await fs.writeFile( filePath, - prettierFormat(fileContent, { ...prettierRc, parser: 'json' }), + await prettierFormat(fileContent, { ...prettierRc, parser: 'json' }), ); } diff --git a/yarn.lock b/yarn.lock index c92dafbd90a..62f430f3b91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2261,11 +2261,11 @@ __metadata: dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/eth-snap-keyring": "npm:^7.0.0" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/eth-snap-keyring": "npm:^8.0.0" "@metamask/keyring-api": "npm:^13.0.0" "@metamask/keyring-controller": "npm:^19.0.2" - "@metamask/keyring-internal-api": "npm:^1.1.0" + "@metamask/keyring-internal-api": "npm:^2.0.0" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.10.0" "@metamask/snaps-sdk": "npm:^6.7.0" @@ -2307,8 +2307,8 @@ __metadata: resolution: "@metamask/address-book-controller@workspace:packages/address-book-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2325,7 +2325,7 @@ __metadata: resolution: "@metamask/announcement-controller@workspace:packages/announcement-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -2343,12 +2343,12 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^7.1.1, @metamask/approval-controller@workspace:packages/approval-controller": +"@metamask/approval-controller@npm:^7.1.1, @metamask/approval-controller@npm:^7.1.2, @metamask/approval-controller@workspace:packages/approval-controller": version: 0.0.0-use.local resolution: "@metamask/approval-controller@workspace:packages/approval-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" @@ -2376,21 +2376,26 @@ __metadata: "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" "@metamask/accounts-controller": "npm:^20.0.2" - "@metamask/approval-controller": "npm:^7.1.1" + "@metamask/approval-controller": "npm:^7.1.2" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-provider-http": "npm:^0.3.0" + "@metamask/keyring-api": "npm:^13.0.0" "@metamask/keyring-controller": "npm:^19.0.2" - "@metamask/keyring-internal-api": "npm:^1.1.0" + "@metamask/keyring-internal-api": "npm:^2.0.0" + "@metamask/keyring-snap-client": "npm:^2.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/network-controller": "npm:^22.1.1" "@metamask/polling-controller": "npm:^12.0.2" "@metamask/preferences-controller": "npm:^15.0.1" "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-controllers": "npm:^9.10.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/utils": "npm:^11.0.1" "@types/bn.js": "npm:^5.1.5" "@types/jest": "npm:^27.4.1" @@ -2398,6 +2403,7 @@ __metadata: "@types/node": "npm:^16.18.54" "@types/uuid": "npm:^8.3.0" async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" bn.js: "npm:^5.2.1" cockatiel: "npm:^3.1.2" deepmerge: "npm:^4.2.2" @@ -2457,7 +2463,7 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^7.0.2, @metamask/base-controller@npm:^7.1.0, @metamask/base-controller@workspace:packages/base-controller": +"@metamask/base-controller@npm:^7.0.2, @metamask/base-controller@npm:^7.1.1, @metamask/base-controller@workspace:packages/base-controller": version: 0.0.0-use.local resolution: "@metamask/base-controller@workspace:packages/base-controller" dependencies: @@ -2518,9 +2524,9 @@ __metadata: resolution: "@metamask/chain-controller@workspace:packages/chain-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/chain-api": "npm:^0.1.0" - "@metamask/keyring-internal-api": "npm:^1.1.0" + "@metamask/keyring-internal-api": "npm:^2.0.0" "@metamask/keyring-utils": "npm:^1.0.0" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.10.0" @@ -2548,7 +2554,7 @@ __metadata: resolution: "@metamask/composable-controller@workspace:packages/composable-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/json-rpc-engine": "npm:^10.0.2" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2569,7 +2575,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.4.4, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.5, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -2684,8 +2690,8 @@ __metadata: dependencies: "@ethersproject/providers": "npm:^5.7.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/network-controller": "npm:^22.1.1" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" @@ -2909,15 +2915,15 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^7.0.0": - version: 7.1.0 - resolution: "@metamask/eth-snap-keyring@npm:7.1.0" +"@metamask/eth-snap-keyring@npm:^8.0.0": + version: 8.0.0 + resolution: "@metamask/eth-snap-keyring@npm:8.0.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/eth-sig-util": "npm:^8.1.2" "@metamask/keyring-api": "npm:^13.0.0" - "@metamask/keyring-internal-api": "npm:^1.1.0" - "@metamask/keyring-internal-snap-client": "npm:^1.1.0" + "@metamask/keyring-internal-api": "npm:^2.0.0" + "@metamask/keyring-internal-snap-client": "npm:^2.0.0" "@metamask/keyring-utils": "npm:^1.0.0" "@metamask/snaps-controllers": "npm:^9.10.0" "@metamask/snaps-sdk": "npm:^6.7.0" @@ -2930,7 +2936,7 @@ __metadata: peerDependencies: "@metamask/keyring-api": ^13.0.0 "@metamask/providers": ^18.3.1 - checksum: 10/0cfa24d5ad2f0ec83d096de906c48994994b2af999cdd7ab6a472aa9c96c1ceab1f800588f4cdc6f9a2c6821417772321c1a031fead53a5c61c792a6adea839f + checksum: 10/51bc9f703109acd662aac36986568cb25995307a2607f4634067cdd2353afda2eb620438993452f1376f6c7323660556df6c3e02c0c0b9e3dbe2d11d56d2a93f languageName: node linkType: hard @@ -3083,8 +3089,8 @@ __metadata: resolution: "@metamask/example-controllers@workspace:examples/example-controllers" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3103,8 +3109,8 @@ __metadata: dependencies: "@babel/runtime": "npm:^7.23.9" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-unit": "npm:^0.3.0" "@metamask/network-controller": "npm:^22.1.1" @@ -3211,13 +3217,13 @@ __metadata: "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/eth-hd-keyring": "npm:^7.0.4" "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/eth-simple-keyring": "npm:^6.0.5" "@metamask/keyring-api": "npm:^13.0.0" - "@metamask/keyring-internal-api": "npm:^1.1.0" + "@metamask/keyring-internal-api": "npm:^2.0.0" "@metamask/message-manager": "npm:^11.0.3" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/utils": "npm:^11.0.1" @@ -3237,24 +3243,24 @@ __metadata: languageName: unknown linkType: soft -"@metamask/keyring-internal-api@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask/keyring-internal-api@npm:1.1.0" +"@metamask/keyring-internal-api@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/keyring-internal-api@npm:2.0.0" dependencies: "@metamask/keyring-api": "npm:^13.0.0" "@metamask/keyring-utils": "npm:^1.0.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.0.1" - checksum: 10/5db127cfe319c289b95d55d08b6737820f00761564e219d756a55e030b83043fb43d18bac3b63fcc69cd2e03129f831410ce960ba954d8183d976d87a6781b8c + checksum: 10/778a2172bc7e5607c78effc0e3bb9dfaec1e12b8cfd82912248a4d72b0f64c48a1f74598396a06ecedbc2594b60ec384e105c2a194883aff5b43352f4301ed27 languageName: node linkType: hard -"@metamask/keyring-internal-snap-client@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask/keyring-internal-snap-client@npm:1.1.0" +"@metamask/keyring-internal-snap-client@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/keyring-internal-snap-client@npm:2.0.0" dependencies: "@metamask/keyring-api": "npm:^13.0.0" - "@metamask/keyring-snap-client": "npm:^1.1.0" + "@metamask/keyring-snap-client": "npm:^2.0.0" "@metamask/keyring-utils": "npm:^1.0.0" "@metamask/snaps-controllers": "npm:^9.10.0" "@metamask/snaps-sdk": "npm:^6.7.0" @@ -3262,13 +3268,13 @@ __metadata: webextension-polyfill: "npm:^0.12.0" peerDependencies: "@metamask/providers": ^18.3.1 - checksum: 10/58e0f00cc3798b156b7e2b3766216a8b355970391a33c9fb4b31cffc7c06d77cf63be275e5e217e0e05c517ed01dd8745b202882d6b7dd14891115d1dd022b7a + checksum: 10/51c1d9a376af9e0984ae68b3cfe1ee04f710470899a1e5394221148069e9433a2cdd776620ad7e14cebd27d027f3761ad494c373213e9c41bb606dc8a879433c languageName: node linkType: hard -"@metamask/keyring-snap-client@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask/keyring-snap-client@npm:1.1.0" +"@metamask/keyring-snap-client@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/keyring-snap-client@npm:2.0.0" dependencies: "@metamask/keyring-api": "npm:^13.0.0" "@metamask/keyring-utils": "npm:^1.0.0" @@ -3278,7 +3284,7 @@ __metadata: webextension-polyfill: "npm:^0.12.0" peerDependencies: "@metamask/providers": ^18.3.1 - checksum: 10/f6e72d94d2fefd24619eca8c6fe838e448adbc42553656a4ffac44c32266836eb8223b30881c70434ce88e433c6fce12dbdaf9920dca7542b54334802cff72b7 + checksum: 10/64fe458beeba190fec5891bcdf2f16ba410131cbd56a04b9276df5f43636b28e0ef9d1b4b9d30acefe4c87a253cc5908954e47666953f6f27a8cc2481758638f languageName: node linkType: hard @@ -3297,8 +3303,8 @@ __metadata: resolution: "@metamask/logging-controller@workspace:packages/logging-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -3315,8 +3321,8 @@ __metadata: resolution: "@metamask/message-manager@workspace:packages/message-manager" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" @@ -3345,10 +3351,10 @@ __metadata: dependencies: "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/eth-json-rpc-filters": "npm:^9.0.0" "@metamask/network-controller": "npm:^22.1.1" - "@metamask/permission-controller": "npm:^11.0.4" + "@metamask/permission-controller": "npm:^11.0.5" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" @@ -3370,8 +3376,8 @@ __metadata: resolution: "@metamask/name-controller@workspace:packages/name-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" async-mutex: "npm:^0.5.0" @@ -3390,8 +3396,8 @@ __metadata: dependencies: "@json-rpc-specification/meta-schema": "npm:^1.0.6" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@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" @@ -3444,10 +3450,10 @@ __metadata: "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/keyring-controller": "npm:^19.0.2" - "@metamask/profile-sync-controller": "npm:^3.2.0" + "@metamask/profile-sync-controller": "npm:^3.3.0" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" "@types/readable-stream": "npm:^2.3.0" @@ -3500,14 +3506,14 @@ __metadata: languageName: node linkType: hard -"@metamask/permission-controller@npm:^11.0.3, @metamask/permission-controller@npm:^11.0.4, @metamask/permission-controller@workspace:packages/permission-controller": +"@metamask/permission-controller@npm:^11.0.3, @metamask/permission-controller@npm:^11.0.5, @metamask/permission-controller@workspace:packages/permission-controller": version: 0.0.0-use.local resolution: "@metamask/permission-controller@workspace:packages/permission-controller" dependencies: - "@metamask/approval-controller": "npm:^7.1.1" + "@metamask/approval-controller": "npm:^7.1.2" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/json-rpc-engine": "npm:^10.0.2" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.0.1" @@ -3532,7 +3538,7 @@ __metadata: resolution: "@metamask/permission-log-controller@workspace:packages/permission-log-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/json-rpc-engine": "npm:^10.0.2" "@metamask/utils": "npm:^11.0.1" "@types/deep-freeze-strict": "npm:^1.1.0" @@ -3553,8 +3559,8 @@ __metadata: resolution: "@metamask/phishing-controller@workspace:packages/phishing-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@noble/hashes": "npm:^1.4.0" "@types/jest": "npm:^27.4.1" "@types/punycode": "npm:^2.1.0" @@ -3577,8 +3583,8 @@ __metadata: resolution: "@metamask/polling-controller@workspace:packages/polling-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/network-controller": "npm:^22.1.1" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" @@ -3612,8 +3618,8 @@ __metadata: resolution: "@metamask/preferences-controller@workspace:packages/preferences-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/keyring-controller": "npm:^19.0.2" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3628,7 +3634,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/profile-sync-controller@npm:^3.2.0, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": +"@metamask/profile-sync-controller@npm:^3.3.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: @@ -3636,10 +3642,10 @@ __metadata: "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/accounts-controller": "npm:^20.0.2" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/keyring-api": "npm:^13.0.0" "@metamask/keyring-controller": "npm:^19.0.2" - "@metamask/keyring-internal-api": "npm:^1.1.0" + "@metamask/keyring-internal-api": "npm:^2.0.0" "@metamask/network-controller": "npm:^22.1.1" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.10.0" @@ -3697,8 +3703,8 @@ __metadata: resolution: "@metamask/queued-request-controller@workspace:packages/queued-request-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/json-rpc-engine": "npm:^10.0.2" "@metamask/network-controller": "npm:^22.1.1" "@metamask/rpc-errors": "npm:^7.0.2" @@ -3727,7 +3733,7 @@ __metadata: resolution: "@metamask/rate-limit-controller@workspace:packages/rate-limit-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" @@ -3746,8 +3752,8 @@ __metadata: dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" cockatiel: "npm:^3.1.2" @@ -3794,10 +3800,10 @@ __metadata: resolution: "@metamask/selected-network-controller@workspace:packages/selected-network-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/json-rpc-engine": "npm:^10.0.2" "@metamask/network-controller": "npm:^22.1.1" - "@metamask/permission-controller": "npm:^11.0.4" + "@metamask/permission-controller": "npm:^11.0.5" "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" @@ -3821,10 +3827,10 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/signature-controller@workspace:packages/signature-controller" dependencies: - "@metamask/approval-controller": "npm:^7.1.1" + "@metamask/approval-controller": "npm:^7.1.2" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/keyring-controller": "npm:^19.0.2" "@metamask/logging-controller": "npm:^6.0.3" @@ -3856,8 +3862,8 @@ __metadata: linkType: hard "@metamask/snaps-controllers@npm:^9.10.0": - version: 9.12.0 - resolution: "@metamask/snaps-controllers@npm:9.12.0" + version: 9.13.0 + resolution: "@metamask/snaps-controllers@npm:9.13.0" dependencies: "@metamask/approval-controller": "npm:^7.1.1" "@metamask/base-controller": "npm:^7.0.2" @@ -3870,8 +3876,8 @@ __metadata: "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/snaps-registry": "npm:^3.2.2" "@metamask/snaps-rpc-methods": "npm:^11.5.1" - "@metamask/snaps-sdk": "npm:^6.10.0" - "@metamask/snaps-utils": "npm:^8.5.0" + "@metamask/snaps-sdk": "npm:^6.11.0" + "@metamask/snaps-utils": "npm:^8.6.0" "@metamask/utils": "npm:^10.0.0" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -3885,11 +3891,11 @@ __metadata: semver: "npm:^7.5.4" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.9.2 + "@metamask/snaps-execution-environments": ^6.10.0 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/8d411ff2cfd43e62fe780092e935a1d977379488407b56cca1390edfa9408871cbaf3599f6e6ee999340d46fd3650f225a3270ceec9492c6f2dc4d93538c25ae + checksum: 10/bcf60b61de067f89439cb15acbdf6f808b4bcda8e1cbc9debd693ca2c545c9d38c4e6f380191c4703bd9d28d7dd41e4ce5111664d7b474d5e86e460bcefc3637 languageName: node linkType: hard @@ -3921,22 +3927,22 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.10.0, @metamask/snaps-sdk@npm:^6.7.0": - version: 6.10.0 - resolution: "@metamask/snaps-sdk@npm:6.10.0" +"@metamask/snaps-sdk@npm:^6.10.0, @metamask/snaps-sdk@npm:^6.11.0, @metamask/snaps-sdk@npm:^6.7.0": + version: 6.11.0 + resolution: "@metamask/snaps-sdk@npm:6.11.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" - checksum: 10/02f04536328a64ff1e9e48fb6b109698d6d83f42af5666a9758ccb1e7a1e67c0c2e296ef2fef419dd3d1c8f26bbf30b9f31911a1baa66f044f21cd0ecb7a11a7 + checksum: 10/0f9b507139d1544b1b3d85ff8de81b800d543012d3ee9414c607c23abe9562e0dca48de089ed94be69f5ad981730a0f443371edfe6bc2d5ffb140b28e437bfd2 languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.5.0": - version: 8.5.2 - resolution: "@metamask/snaps-utils@npm:8.5.2" +"@metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.5.0, @metamask/snaps-utils@npm:^8.6.0": + version: 8.6.0 + resolution: "@metamask/snaps-utils@npm:8.6.0" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -3946,7 +3952,7 @@ __metadata: "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.2" - "@metamask/snaps-sdk": "npm:^6.10.0" + "@metamask/snaps-sdk": "npm:^6.11.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" "@noble/hashes": "npm:^1.3.1" @@ -3961,7 +3967,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/e5d1344f948473e82d71007d2570272073cf070f40aa7746692a6d5e6f02cfce66a747cf50f439d32b29a3f6588486182453b26973f0d0c1d9f47914591d5790 + checksum: 10/c0f538f3f95e1875f6557b6ecc32f981bc4688d581af8cdc62c6c3ab8951c138286cd0b2d1cd82f769df24fcec10f71dcda67ae9a47edcff9ff73d52672df191 languageName: node linkType: hard @@ -3991,10 +3997,10 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/accounts-controller": "npm:^20.0.2" - "@metamask/approval-controller": "npm:^7.1.1" + "@metamask/approval-controller": "npm:^7.1.2" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/eth-block-tracker": "npm:^11.0.3" "@metamask/eth-json-rpc-provider": "npm:^4.1.7" "@metamask/eth-query": "npm:^4.0.0" @@ -4037,10 +4043,10 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/user-operation-controller@workspace:packages/user-operation-controller" dependencies: - "@metamask/approval-controller": "npm:^7.1.1" + "@metamask/approval-controller": "npm:^7.1.2" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" - "@metamask/controller-utils": "npm:^11.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" "@metamask/eth-block-tracker": "npm:^11.0.3" "@metamask/eth-query": "npm:^4.0.0" "@metamask/gas-fee-controller": "npm:^22.0.2" @@ -6280,6 +6286,13 @@ __metadata: languageName: node linkType: hard +"base58-js@npm:^1.0.0": + version: 1.0.5 + resolution: "base58-js@npm:1.0.5" + checksum: 10/46c1b39d3a70bca0a47d56069c74a25d547680afd0f28609c90f280f5d614f5de36db5df993fa334db24008a68ab784a72fcdaa13eb40078e03c8999915a1100 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -6320,6 +6333,17 @@ __metadata: languageName: node linkType: hard +"bitcoin-address-validation@npm:^2.2.3": + version: 2.2.3 + resolution: "bitcoin-address-validation@npm:2.2.3" + dependencies: + base58-js: "npm:^1.0.0" + bech32: "npm:^2.0.0" + sha256-uint8array: "npm:^0.10.3" + checksum: 10/01603b5edf610ecf0843ae546534313f1cffabc8e7435a3678bc9788f18a54e51302218a539794aafd49beb5be70b5d1d507eb7442cb33970fcd665592a71305 + languageName: node + linkType: hard + "bl@npm:^4.0.3": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -12058,6 +12082,13 @@ __metadata: languageName: node linkType: hard +"sha256-uint8array@npm:^0.10.3": + version: 0.10.7 + resolution: "sha256-uint8array@npm:0.10.7" + checksum: 10/e427f9d2f9c521dea552f033d3f0c3bd641ab214d214dd41bde3c805edde393519cf982b3eee7d683b32e5f28fa23b2278d25935940e13fbe831b216a37832be + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0"