From 950a2eb4d005c8fec75981927531603927b3fa4c Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:23:30 -0300 Subject: [PATCH 01/49] chore: add dependencies --- packages/assets-controllers/package.json | 4 + yarn.lock | 162 ++++++++++++++++++++++- 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 5b43ad21963..bfcc62f9069 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -61,10 +61,12 @@ "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/polling-controller": "^12.0.1", "@metamask/rpc-errors": "^7.0.1", + "@metamask/snaps-utils": "^8.5.2", "@metamask/utils": "^10.0.0", "@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", @@ -82,6 +84,8 @@ "@metamask/keyring-controller": "^18.0.0", "@metamask/network-controller": "^22.0.2", "@metamask/preferences-controller": "^14.0.0", + "@metamask/snaps-controllers": "^9.12.0", + "@metamask/snaps-sdk": "^6.10.0", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.191", "@types/node": "^16.18.54", diff --git a/yarn.lock b/yarn.lock index cd0a925162d..ad1a55f59aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2156,6 +2156,9 @@ __metadata: "@metamask/polling-controller": "npm:^12.0.1" "@metamask/preferences-controller": "npm:^14.0.0" "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/snaps-controllers": "npm:^9.12.0" + "@metamask/snaps-sdk": "npm:^6.10.0" + "@metamask/snaps-utils": "npm:^8.5.2" "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" "@types/jest": "npm:^27.4.1" @@ -2163,6 +2166,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" @@ -2911,7 +2915,7 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-middleware-stream@npm:^8.0.1, @metamask/json-rpc-middleware-stream@npm:^8.0.2, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": +"@metamask/json-rpc-middleware-stream@npm:^8.0.1, @metamask/json-rpc-middleware-stream@npm:^8.0.2, @metamask/json-rpc-middleware-stream@npm:^8.0.5, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": version: 0.0.0-use.local resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" dependencies: @@ -3409,6 +3413,27 @@ __metadata: languageName: node linkType: hard +"@metamask/providers@npm:^18.1.1": + version: 18.1.1 + resolution: "@metamask/providers@npm:18.1.1" + dependencies: + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.5" + "@metamask/object-multiplex": "npm:^2.0.0" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/safe-event-emitter": "npm:^3.1.1" + "@metamask/utils": "npm:^10.0.0" + detect-browser: "npm:^5.2.0" + extension-port-stream: "npm:^4.1.0" + fast-deep-equal: "npm:^3.1.3" + is-stream: "npm:^2.0.0" + readable-stream: "npm:^3.6.2" + peerDependencies: + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/dca428d84e490343d85921d4fb09216a0b64be59a036d7b4f7b5ca4e2581c29a4106d58ff9dfe0650dc2b9387dd2adad508fc61073a9fda8ebde8ee3a5137abe + languageName: node + linkType: hard + "@metamask/queued-request-controller@workspace:packages/queued-request-controller": version: 0.0.0-use.local resolution: "@metamask/queued-request-controller@workspace:packages/queued-request-controller" @@ -3567,6 +3592,44 @@ __metadata: languageName: node linkType: hard +"@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" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.5" + "@metamask/object-multiplex": "npm:^2.0.0" + "@metamask/permission-controller": "npm:^11.0.3" + "@metamask/phishing-controller": "npm:^12.0.2" + "@metamask/post-message-stream": "npm:^8.1.1" + "@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.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" + concat-stream: "npm:^2.0.0" + fast-deep-equal: "npm:^3.1.3" + get-npm-tarball-url: "npm:^2.0.3" + immer: "npm:^9.0.6" + nanoid: "npm:^3.1.31" + readable-stream: "npm:^3.6.2" + readable-web-to-node-stream: "npm:^3.0.2" + semver: "npm:^7.5.4" + tar-stream: "npm:^3.1.7" + peerDependencies: + "@metamask/snaps-execution-environments": ^6.10.0 + peerDependenciesMeta: + "@metamask/snaps-execution-environments": + optional: true + checksum: 10/bcf60b61de067f89439cb15acbdf6f808b4bcda8e1cbc9debd693ca2c545c9d38c4e6f380191c4703bd9d28d7dd41e4ce5111664d7b474d5e86e460bcefc3637 + languageName: node + linkType: hard + "@metamask/snaps-controllers@npm:^9.7.0": version: 9.7.0 resolution: "@metamask/snaps-controllers@npm:9.7.0" @@ -3616,6 +3679,18 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-registry@npm:^3.2.2": + version: 3.2.2 + resolution: "@metamask/snaps-registry@npm:3.2.2" + dependencies: + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^10.0.0" + "@noble/curves": "npm:^1.2.0" + "@noble/hashes": "npm:^1.3.2" + checksum: 10/ca8239e838bbb913435e166136bbc9bd7222c4bd87b1525fa7ae3cdf2e0b868b5d4d90a67d1ed49633d566bdef9243abdbf5f5937b85a85d24184087f555813e + languageName: node + linkType: hard + "@metamask/snaps-rpc-methods@npm:^11.1.1": version: 11.1.1 resolution: "@metamask/snaps-rpc-methods@npm:11.1.1" @@ -3632,6 +3707,22 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-rpc-methods@npm:^11.5.1": + version: 11.5.1 + resolution: "@metamask/snaps-rpc-methods@npm:11.5.1" + dependencies: + "@metamask/key-tree": "npm:^9.1.2" + "@metamask/permission-controller": "npm:^11.0.3" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/snaps-sdk": "npm:^6.10.0" + "@metamask/snaps-utils": "npm:^8.5.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^10.0.0" + "@noble/hashes": "npm:^1.3.1" + checksum: 10/0f999a5dd64f1b1123366f448ae833f0e95a415791600bb535959ba67d2269fbe3c4504d47f04db71bafa79a9a87d6b832fb2e2b5ef29567078c95bce2638f35 + languageName: node + linkType: hard + "@metamask/snaps-sdk@npm:^6.1.0, @metamask/snaps-sdk@npm:^6.5.0, @metamask/snaps-sdk@npm:^6.5.1": version: 6.5.1 resolution: "@metamask/snaps-sdk@npm:6.5.1" @@ -3645,6 +3736,19 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-sdk@npm:^6.10.0, @metamask/snaps-sdk@npm:^6.11.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/0f9b507139d1544b1b3d85ff8de81b800d543012d3ee9414c607c23abe9562e0dca48de089ed94be69f5ad981730a0f443371edfe6bc2d5ffb140b28e437bfd2 + languageName: node + linkType: hard + "@metamask/snaps-utils@npm:^7.8.1": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" @@ -3707,6 +3811,37 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-utils@npm:^8.5.0, @metamask/snaps-utils@npm:^8.5.2, @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" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/key-tree": "npm:^9.1.2" + "@metamask/permission-controller": "npm:^11.0.3" + "@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.11.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^10.0.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.1" + chalk: "npm:^4.1.2" + cron-parser: "npm:^4.5.0" + fast-deep-equal: "npm:^3.1.3" + fast-json-stable-stringify: "npm:^2.1.0" + fast-xml-parser: "npm:^4.4.1" + marked: "npm:^12.0.1" + rfdc: "npm:^1.3.0" + semver: "npm:^7.5.4" + ses: "npm:^1.1.0" + validate-npm-package-name: "npm:^5.0.0" + checksum: 10/c0f538f3f95e1875f6557b6ecc32f981bc4688d581af8cdc62c6c3ab8951c138286cd0b2d1cd82f769df24fcec10f71dcda67ae9a47edcff9ff73d52672df191 + languageName: node + linkType: hard + "@metamask/superstruct@npm:^3.0.0, @metamask/superstruct@npm:^3.1.0": version: 3.1.0 resolution: "@metamask/superstruct@npm:3.1.0" @@ -5392,6 +5527,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" @@ -5432,6 +5574,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:^5.0.0": version: 5.1.0 resolution: "bl@npm:5.1.0" @@ -11363,6 +11516,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" From 12a233aeb01d86a4e1d210a04a4f9b26f5d32a4e Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:58:32 -0300 Subject: [PATCH 02/49] feat: add MultichainBalancesController, BalancesTracker, and Poller --- .../BalancesTracker.ts | 125 ++++++ .../MultichainBalancesController.ts | 384 ++++++++++++++++++ .../MultichainBalancesController/Poller.ts | 28 ++ .../src/MultichainBalancesController/index.ts | 10 + packages/assets-controllers/src/index.ts | 10 + 5 files changed, 557 insertions(+) create mode 100644 packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts create mode 100644 packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts create mode 100644 packages/assets-controllers/src/MultichainBalancesController/Poller.ts create mode 100644 packages/assets-controllers/src/MultichainBalancesController/index.ts diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts new file mode 100644 index 00000000000..27ed2f96931 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -0,0 +1,125 @@ +import { Poller } from './Poller'; + +type BalanceInfo = { + lastUpdated: number; + blockTime: number; +}; + +const BALANCES_TRACKING_INTERVAL = 30 * 1000; // Every 30s 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().catch((error) => { + console.error('Error updating balances:', error); + }); + }, 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 tracker, false otherwise. + */ + isTracked(accountId: string) { + return accountId in this.#balances; + } + + /** + * 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]; + const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; + const hasNoBalanceYet = info.lastUpdated === 0; + if (hasNoBalanceYet || isOutdated) { + 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); + }), + ); + } +} diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts new file mode 100644 index 00000000000..499dda69ede --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -0,0 +1,384 @@ +import type { + AccountsControllerAccountAddedEvent, + AccountsControllerAccountRemovedEvent, + AccountsControllerListMultichainAccountsAction, +} from '@metamask/accounts-controller'; +import { + BaseController, + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { + BtcAccountType, + KeyringClient, + type Balance, + type CaipAssetType, + type InternalAccount, + isEvmAccountType, +} from '@metamask/keyring-api'; +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 { validate, Network } from 'bitcoin-address-validation'; +import type { Draft } from 'immer'; + +import { BalancesTracker } from './BalancesTracker'; + +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; + }; + }; + }; +}; + +/** + * Default state of the {@link MultichainBalancesController}. + */ +export const defaultState: MultichainBalancesControllerState = { 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. + */ +export type AllowedActions = + | HandleSnapRequest + | AccountsControllerListMultichainAccountsAction; + +/** + * Events that this controller is allowed to subscribe. + */ +export 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 multichainBalancesControllerMetadata = { + balances: { + persist: true, + anonymous: false, + }, +}; + +const BTC_TESTNET_ASSETS = ['bip122:000000000933ea01ad0ee984209779ba/slip44:0']; +const BTC_MAINNET_ASSETS = ['bip122:000000000019d6689c085ae165831e93/slip44:0']; +const BTC_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds + +// NOTE: We set an interval of half the average block time to mitigate when our interval +// is de-synchronized with the actual block time. +export const BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2; + +/** + * 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: MultichainBalancesControllerState; + }) { + super({ + messenger, + name: controllerName, + metadata: multichainBalancesControllerMetadata, + state: { + ...defaultState, + ...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, BALANCES_UPDATE_TIME); + } + } + + this.messagingSystem.subscribe( + 'AccountsController:accountAdded', + (account) => this.#handleOnAccountAdded(account), + ); + this.messagingSystem.subscribe( + 'AccountsController:accountRemoved', + (account) => this.#handleOnAccountRemoved(account), + ); + } + + /** + * Starts the polling process. + */ + async start(): Promise { + this.#tracker.start(); + } + + /** + * Stops the polling process. + */ + async stop(): Promise { + this.#tracker.stop(); + } + + /** + * 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. + * + * Currently, we only get balances for P2WPKH accounts, but this will change + * in the future when we start support other non-EVM account types. + * + * @returns A list of accounts that we should get balances for. + */ + #listAccounts(): InternalAccount[] { + const accounts = this.#listMultichainAccounts(); + + return accounts.filter((account) => account.type === BtcAccountType.P2wpkh); + } + + /** + * 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.#listMultichainAccounts().find( + (multichainAccount) => multichainAccount.id === accountId, + ); + + if (!account) { + throw new Error(`Unknown account: ${accountId}`); + } + if (!this.#isNonEvmAccount(account)) { + throw new Error(`Account is not a non-EVM 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); + const partialState: MultichainBalancesControllerState = { balances: {} }; + + if (account.metadata.snap) { + partialState.balances[account.id] = await this.#getBalances( + account.id, + account.metadata.snap.id, + validate(account.address, Network.mainnet) + ? BTC_MAINNET_ASSETS + : BTC_TESTNET_ASSETS, + ); + } + + this.update((state: Draft) => ({ + ...state, + balances: { + ...state.balances, + ...partialState.balances, + }, + })); + } + + /** + * 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) { + // 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() { + await this.#tracker.updateBalances(); + } + + /** + * 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) { + if (!this.#isNonEvmAccount(account)) { + // Nothing to do here for EVM accounts + return; + } + + this.#tracker.track(account.id, BTC_AVG_BLOCK_TIME); + // 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) { + if (this.#tracker.isTracked(accountId)) { + this.#tracker.untrack(accountId); + } + + if (accountId in this.state.balances) { + this.update((state: Draft) => { + delete state.balances[accountId]; + return state; + }); + } + } + + /** + * 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.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts new file mode 100644 index 00000000000..600e2ea615d --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts @@ -0,0 +1,28 @@ +export class Poller { + #interval: number; + + #callback: () => void; + + #handle: NodeJS.Timeout | undefined = undefined; + + constructor(callback: () => void, interval: number) { + this.#interval = interval; + this.#callback = callback; + } + + start() { + if (this.#handle) { + return; + } + + this.#handle = setInterval(this.#callback, this.#interval); + } + + stop() { + if (!this.#handle) { + return; + } + clearInterval(this.#handle); + this.#handle = undefined; + } +} diff --git a/packages/assets-controllers/src/MultichainBalancesController/index.ts b/packages/assets-controllers/src/MultichainBalancesController/index.ts new file mode 100644 index 00000000000..5ecc04e4710 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/index.ts @@ -0,0 +1,10 @@ +export { MultichainBalancesController } from './MultichainBalancesController'; +export type { + MultichainBalancesControllerState, + MultichainBalancesControllerGetStateAction, + MultichainBalancesControllerUpdateBalancesAction, + MultichainBalancesControllerStateChange, + MultichainBalancesControllerActions, + MultichainBalancesControllerEvents, + MultichainBalancesControllerMessenger, +} from './MultichainBalancesController'; diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index 664642bbad5..7bff0042293 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -146,3 +146,13 @@ export type { RatesControllerPollingStartedEvent, RatesControllerPollingStoppedEvent, } from './RatesController'; +export { MultichainBalancesController } from './MultichainBalancesController'; +export type { + MultichainBalancesControllerState, + MultichainBalancesControllerGetStateAction, + MultichainBalancesControllerUpdateBalancesAction, + MultichainBalancesControllerStateChange, + MultichainBalancesControllerActions, + MultichainBalancesControllerEvents, + MultichainBalancesControllerMessenger, +} from './MultichainBalancesController'; From c9b707cbb5e2f37d6a061f831ff6d07ab524cd03 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 22 Nov 2024 08:46:07 -0300 Subject: [PATCH 03/49] test: add unit test to MultichainBalancesController, BalancesTracker, and Poller --- .../BalancesTracker.test.ts | 135 +++++++++++++ .../MultichainBalancesController.test.ts | 181 ++++++++++++++++++ .../MultichainBalancesController.ts | 4 +- .../Poller.test.ts | 59 ++++++ 4 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts create mode 100644 packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts create mode 100644 packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts 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..584b9e87f0d --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts @@ -0,0 +1,135 @@ +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.SendMany], + 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 + }); +}); 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..d9b27b60fbf --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -0,0 +1,181 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import type { + Balance, + CaipAssetType, + InternalAccount, +} from '@metamask/keyring-api'; +import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { v4 as uuidv4 } from 'uuid'; + +import { BalancesTracker } from './BalancesTracker'; +import { + MultichainBalancesController, + defaultState, +} from './MultichainBalancesController'; +import type { + AllowedActions, + AllowedEvents, + MultichainBalancesControllerMessenger, + MultichainBalancesControllerState, +} from './MultichainBalancesController'; + +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.SendMany], + type: BtcAccountType.P2wpkh, +}; + +const mockBalanceResult = { + 'bip122:000000000933ea01ad0ee984209779ba/slip44:0': { + amount: '0.00000000', + unit: 'BTC', + }, +}; + +const setupController = ({ + state = defaultState, + mocks, +}: { + state?: MultichainBalancesControllerState; + mocks?: { + listMultichainAccounts?: InternalAccount[]; + handleRequestReturnValue?: Record; + }; +} = {}) => { + const controllerMessenger = new ControllerMessenger< + AllowedActions, + AllowedEvents + >(); + + 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], + ), + ); + + 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('update balances when calling updateBalances', async () => { + const { controller } = setupController(); + + await controller.updateBalances(); + + expect(controller.state).toStrictEqual({ + balances: { + [mockBtcAccount.id]: mockBalanceResult, + }, + }); + }); + + it('update 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('update 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: {}, + }); + }); +}); diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 499dda69ede..6c36a913a99 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -186,14 +186,14 @@ export class MultichainBalancesController extends BaseController< /** * Starts the polling process. */ - async start(): Promise { + start(): void { this.#tracker.start(); } /** * Stops the polling process. */ - async stop(): Promise { + stop(): void { this.#tracker.stop(); } 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..e79d4961a0c --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts @@ -0,0 +1,59 @@ +import { Poller } from './Poller'; + +jest.useFakeTimers(); + +const interval = 1000; +const intervalPlus100ms = interval + 100; + +describe('Poller', () => { + let callback: jest.Mock; + + beforeEach(() => { + callback = jest.fn(); + }); + + 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(); + + 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); + + 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(); + + 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 resumes before reaching out + // the `interval` timeout + jest.advanceTimersByTime(interval / 2); + poller.stop(); + expect(callback).not.toHaveBeenCalled(); + }); +}); From 0c15e21cb2acfcd858521590725744107639cc9e Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 25 Nov 2024 07:38:05 -0300 Subject: [PATCH 04/49] fix: dependencies version consistency --- packages/assets-controllers/package.json | 6 +++--- yarn.lock | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index ca0f5073d86..6df09cfe6b5 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -61,7 +61,7 @@ "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/polling-controller": "^12.0.1", "@metamask/rpc-errors": "^7.0.1", - "@metamask/snaps-utils": "^8.5.2", + "@metamask/snaps-utils": "^8.3.0", "@metamask/utils": "^10.0.0", "@types/bn.js": "^5.1.5", "@types/uuid": "^8.3.0", @@ -84,8 +84,8 @@ "@metamask/keyring-controller": "^19.0.0", "@metamask/network-controller": "^22.0.2", "@metamask/preferences-controller": "^15.0.0", - "@metamask/snaps-controllers": "^9.12.0", - "@metamask/snaps-sdk": "^6.10.0", + "@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/yarn.lock b/yarn.lock index 2bdad4dad2a..8285bbab627 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2156,9 +2156,9 @@ __metadata: "@metamask/polling-controller": "npm:^12.0.1" "@metamask/preferences-controller": "npm:^15.0.0" "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/snaps-controllers": "npm:^9.12.0" - "@metamask/snaps-sdk": "npm:^6.10.0" - "@metamask/snaps-utils": "npm:^8.5.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:^10.0.0" "@types/bn.js": "npm:^5.1.5" "@types/jest": "npm:^27.4.1" @@ -3544,7 +3544,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.12.0": +"@metamask/snaps-controllers@npm:^9.10.0": version: 9.13.0 resolution: "@metamask/snaps-controllers@npm:9.13.0" dependencies: @@ -3623,7 +3623,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.5.0, @metamask/snaps-utils@npm:^8.5.2, @metamask/snaps-utils@npm:^8.6.0": +"@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: From 27e2d6cd7ba8a6818dc1007028daebbc9ddf0e0d Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:17:01 -0300 Subject: [PATCH 05/49] test: update BtcMethod --- .../MultichainBalancesController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index d9b27b60fbf..399aed3e6ac 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -37,7 +37,7 @@ const mockBtcAccount = { lastSelected: 0, }, options: {}, - methods: [BtcMethod.SendMany], + methods: [BtcMethod.SendBitcoin], type: BtcAccountType.P2wpkh, }; From 189078927b8491ed19330226345f218a1efb175e Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:23:56 -0300 Subject: [PATCH 06/49] test: update BtcMethod --- .../src/MultichainBalancesController/BalancesTracker.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts index 584b9e87f0d..8854ecc422a 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts @@ -24,7 +24,7 @@ const mockBtcAccount = { lastSelected: 0, }, options: {}, - methods: [BtcMethod.SendMany], + methods: [BtcMethod.SendBitcoin], type: BtcAccountType.P2wpkh, }; From c07cadc9daf89409e983cce497024fc8d4e3d3d7 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:14:42 -0300 Subject: [PATCH 07/49] chore: add utils and constants --- .../MultichainBalancesController/constants.ts | 40 +++++++++++++ .../src/MultichainBalancesController/utils.ts | 60 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 packages/assets-controllers/src/MultichainBalancesController/constants.ts create mode 100644 packages/assets-controllers/src/MultichainBalancesController/utils.ts diff --git a/packages/assets-controllers/src/MultichainBalancesController/constants.ts b/packages/assets-controllers/src/MultichainBalancesController/constants.ts new file mode 100644 index 00000000000..e1c09332ec8 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/constants.ts @@ -0,0 +1,40 @@ +import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; + +const BTC_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 to mitigate when our interval + // is de-synchronized with the actual block time. + [BtcAccountType.P2wpkh]: BTC_AVG_BLOCK_TIME / 2, + [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, +}; + +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`, +} + +/** + * Maps network identifiers to their corresponding native asset types. + * Each network is mapped to an array containing its native asset for consistency. + */ +export const NetworkAssetsMap: 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/utils.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.ts new file mode 100644 index 00000000000..b5e28db1f2c --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.ts @@ -0,0 +1,60 @@ +import type { InternalAccount } from '@metamask/keyring-api'; +import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; +import { validate, Network } from 'bitcoin-address-validation'; + +import { MultichainNetworks } 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 `keyring-api`. + * + * @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 getScopeForAddress = (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}`); + } +}; From 0afee9f8b0a5ee24354a0ea1d54dcaa194d61929 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:57:00 -0300 Subject: [PATCH 08/49] refactor: construction and accounts tracking --- .../MultichainBalancesController.ts | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 6c36a913a99..6115a256e32 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -10,7 +10,6 @@ import { type RestrictedControllerMessenger, } from '@metamask/base-controller'; import { - BtcAccountType, KeyringClient, type Balance, type CaipAssetType, @@ -21,10 +20,11 @@ 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 { validate, Network } from 'bitcoin-address-validation'; import type { Draft } from 'immer'; import { BalancesTracker } from './BalancesTracker'; +import { BALANCE_UPDATE_INTERVALS } from './constants'; +import { getScopeForAddress } from './utils'; const controllerName = 'MultichainBalancesController'; @@ -57,7 +57,7 @@ export type MultichainBalancesControllerGetStateAction = >; /** - * Updates the balances of all supported accounts. + * Updates the balances of all supported accounts in {@link MultichainBalancesController}. */ export type MultichainBalancesControllerUpdateBalancesAction = { type: `${typeof controllerName}:updateBalances`; @@ -126,14 +126,6 @@ const multichainBalancesControllerMetadata = { }, }; -const BTC_TESTNET_ASSETS = ['bip122:000000000933ea01ad0ee984209779ba/slip44:0']; -const BTC_MAINNET_ASSETS = ['bip122:000000000019d6689c085ae165831e93/slip44:0']; -const BTC_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds - -// NOTE: We set an interval of half the average block time to mitigate when our interval -// is de-synchronized with the actual block time. -export const BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2; - /** * The MultichainBalancesController is responsible for fetching and caching account * balances. @@ -143,14 +135,22 @@ export class MultichainBalancesController extends BaseController< MultichainBalancesControllerState, MultichainBalancesControllerMessenger > { - #tracker: BalancesTracker; + readonly #tracker: BalancesTracker; + + // As a temporary solution, we are using a map, provided during the construction + // of the controller, to store the assets that are hardcoded in the client. + // In the future, this mapping should be dynamic to allow a client to + // register and unregister assets + readonly #networkAssetsMap: Record; constructor({ messenger, state, + networkAssetsMap, }: { messenger: MultichainBalancesControllerMessenger; state: MultichainBalancesControllerState; + networkAssetsMap: Record; }) { super({ messenger, @@ -166,10 +166,12 @@ export class MultichainBalancesController extends BaseController< async (accountId: string) => await this.#updateBalance(accountId), ); + this.#networkAssetsMap = networkAssetsMap; + // Register all non-EVM accounts into the tracker for (const account of this.#listAccounts()) { if (this.#isNonEvmAccount(account)) { - this.#tracker.track(account.id, BALANCES_UPDATE_TIME); + this.#trackAccount(account.id); } } @@ -197,6 +199,20 @@ export class MultichainBalancesController extends BaseController< this.#tracker.stop(); } + /** + * Tracks an account to get its balances. + * + * @param accountId - The account ID. + */ + #trackAccount(accountId: string): void { + const updateTime = + // @ts-expect-error - For the moment we are only tracking non-EVM accounts and this is + // checked with the method `#isNonEvmAccount`. We can ignore this error since + // eip155:eoa and eip155:erc4337 account type balances are not tracked here. + BALANCE_UPDATE_INTERVALS[this.#getAccount(accountId).type]; + this.#tracker.track(accountId, updateTime); + } + /** * Lists the multichain accounts coming from the `AccountsController`. * @@ -211,15 +227,14 @@ export class MultichainBalancesController extends BaseController< /** * Lists the accounts that we should get balances for. * - * Currently, we only get balances for P2WPKH accounts, but this will change - * in the future when we start support other non-EVM account types. + * Currently, we only get balances for non-EVM accounts. * * @returns A list of accounts that we should get balances for. */ #listAccounts(): InternalAccount[] { const accounts = this.#listMultichainAccounts(); - return accounts.filter((account) => account.type === BtcAccountType.P2wpkh); + return accounts.filter((account) => this.#isNonEvmAccount(account)); } /** @@ -253,13 +268,14 @@ export class MultichainBalancesController extends BaseController< const account = this.#getAccount(accountId); const partialState: MultichainBalancesControllerState = { balances: {} }; + const scope = getScopeForAddress(account); + const assetsList = this.#networkAssetsMap[scope]; + if (account.metadata.snap) { partialState.balances[account.id] = await this.#getBalances( account.id, account.metadata.snap.id, - validate(account.address, Network.mainnet) - ? BTC_MAINNET_ASSETS - : BTC_TESTNET_ASSETS, + assetsList, ); } @@ -317,7 +333,7 @@ export class MultichainBalancesController extends BaseController< return; } - this.#tracker.track(account.id, BTC_AVG_BLOCK_TIME); + this.#trackAccount(account.id); // NOTE: Unfortunately, we cannot update the balance right away here, because // messenger's events are running synchronously and fetching the balance is // asynchronous. From 9d6d4ec2df99e581dd079b556c0e6fd42c9c4b59 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:01:18 -0300 Subject: [PATCH 09/49] chore: add comments --- .../MultichainBalancesController.ts | 15 +++++---------- .../src/MultichainBalancesController/constants.ts | 12 ++++++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 6115a256e32..7fb1b8783ca 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -23,7 +23,7 @@ import type { Json, JsonRpcRequest } from '@metamask/utils'; import type { Draft } from 'immer'; import { BalancesTracker } from './BalancesTracker'; -import { BALANCE_UPDATE_INTERVALS } from './constants'; +import { BALANCE_UPDATE_INTERVALS, NETWORK_ASSETS_MAP } from './constants'; import { getScopeForAddress } from './utils'; const controllerName = 'MultichainBalancesController'; @@ -137,20 +137,17 @@ export class MultichainBalancesController extends BaseController< > { readonly #tracker: BalancesTracker; - // As a temporary solution, we are using a map, provided during the construction - // of the controller, to store the assets that are hardcoded in the client. - // In the future, this mapping should be dynamic to allow a client to - // register and unregister assets - readonly #networkAssetsMap: Record; + // As a temporary solution, we are using a map to store the assets + // that are hardcoded in the module. In the future, this mapping + // should be dynamic to allow a client to register and unregister assets + readonly #networkAssetsMap: Record = NETWORK_ASSETS_MAP; constructor({ messenger, state, - networkAssetsMap, }: { messenger: MultichainBalancesControllerMessenger; state: MultichainBalancesControllerState; - networkAssetsMap: Record; }) { super({ messenger, @@ -166,8 +163,6 @@ export class MultichainBalancesController extends BaseController< async (accountId: string) => await this.#updateBalance(accountId), ); - this.#networkAssetsMap = networkAssetsMap; - // Register all non-EVM accounts into the tracker for (const account of this.#listAccounts()) { if (this.#isNonEvmAccount(account)) { diff --git a/packages/assets-controllers/src/MultichainBalancesController/constants.ts b/packages/assets-controllers/src/MultichainBalancesController/constants.ts index e1c09332ec8..852a0c0d4f7 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/constants.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/constants.ts @@ -4,16 +4,20 @@ const BTC_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 to mitigate when our interval - // is de-synchronized with the actual block time. + // NOTE: We set an interval of half the average block time fot bitcoin + // to mitigate when our interval is de-synchronized with the actual block time. [BtcAccountType.P2wpkh]: BTC_AVG_BLOCK_TIME / 2, [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, }; +/** + * 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', @@ -31,7 +35,7 @@ export enum MultichainNativeAssets { * Maps network identifiers to their corresponding native asset types. * Each network is mapped to an array containing its native asset for consistency. */ -export const NetworkAssetsMap: Record = { +export const NETWORK_ASSETS_MAP: Record = { [MultichainNetworks.Solana]: [MultichainNativeAssets.Solana], [MultichainNetworks.SolanaTestnet]: [MultichainNativeAssets.SolanaTestnet], [MultichainNetworks.SolanaDevnet]: [MultichainNativeAssets.SolanaDevnet], From 84a029cfa606c1bba291ee05dbfe86103b06aac0 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:33:57 -0300 Subject: [PATCH 10/49] chore: small refactor to trackAccount --- .../src/MultichainBalancesController/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/utils.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.ts index b5e28db1f2c..f04477664a9 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/utils.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.ts @@ -7,7 +7,7 @@ import { MultichainNetworks } 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 `keyring-api`. + * once the new `account.scopes` is available in the `@metamask/keyring-api` module. * * @param account - Bitcoin account * @returns The scope for the given account. From b9192af06a368a6e57a440466f7c43ba8c7ab2a0 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:34:28 -0300 Subject: [PATCH 11/49] test: fix address validation mock --- .../MultichainBalancesController.test.ts | 2 +- .../MultichainBalancesController.ts | 24 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index 399aed3e6ac..67bf150ba7d 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -21,7 +21,7 @@ import type { } from './MultichainBalancesController'; const mockBtcAccount = { - address: '', + address: 'bc1qssdcp5kvwh6nghzg9tuk99xsflwkdv4hgvq58q', id: uuidv4(), metadata: { name: 'Bitcoin Account 1', diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 7fb1b8783ca..9f7badff34b 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -165,9 +165,7 @@ export class MultichainBalancesController extends BaseController< // Register all non-EVM accounts into the tracker for (const account of this.#listAccounts()) { - if (this.#isNonEvmAccount(account)) { - this.#trackAccount(account.id); - } + this.#trackAccount(account); } this.messagingSystem.subscribe( @@ -197,15 +195,20 @@ export class MultichainBalancesController extends BaseController< /** * Tracks an account to get its balances. * - * @param accountId - The account ID. + * @param account - The account to track. */ - #trackAccount(accountId: string): void { + #trackAccount(account: InternalAccount): void { + if (!this.#isNonEvmAccount(account)) { + // Nothing to do here for EVM accounts + return; + } + const updateTime = // @ts-expect-error - For the moment we are only tracking non-EVM accounts and this is // checked with the method `#isNonEvmAccount`. We can ignore this error since // eip155:eoa and eip155:erc4337 account type balances are not tracked here. - BALANCE_UPDATE_INTERVALS[this.#getAccount(accountId).type]; - this.#tracker.track(accountId, updateTime); + BALANCE_UPDATE_INTERVALS[this.#getAccount(account.id).type]; + this.#tracker.track(account.id, updateTime); } /** @@ -323,12 +326,7 @@ export class MultichainBalancesController extends BaseController< * @param account - The new account being added. */ async #handleOnAccountAdded(account: InternalAccount) { - if (!this.#isNonEvmAccount(account)) { - // Nothing to do here for EVM accounts - return; - } - - this.#trackAccount(account.id); + this.#trackAccount(account); // NOTE: Unfortunately, we cannot update the balance right away here, because // messenger's events are running synchronously and fetching the balance is // asynchronous. From 614d0725b3935a7f713e1af35f337c257cdeb253 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:59:31 -0300 Subject: [PATCH 12/49] test: add utils unit test --- .../utils.test.ts | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 packages/assets-controllers/src/MultichainBalancesController/utils.test.ts 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..c56a36183a8 --- /dev/null +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts @@ -0,0 +1,171 @@ +import { + BtcAccountType, + SolAccountType, + BtcMethod, + SolMethod, +} 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 } from './constants'; +import { + getScopeForBtcAddress, + getScopeForSolAddress, + getScopeForAddress, +} 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, + }, + 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', + }, + 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 = getScopeForAddress(account); + + expect(scope).toBe(MultichainNetworks.Bitcoin); + }); + + it('returns the scope for a Solana account', () => { + const account = { + ...mockSolAccount, + options: { scope: 'solana-scope' }, + }; + + const scope = getScopeForAddress(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(() => getScopeForAddress(account)).toThrow( + `Unsupported non-EVM account type: ${account.type}`, + ); + }); +}); From 2d31d3c8792127ae4115316a12b295cf46c20c7e Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:59:51 -0300 Subject: [PATCH 13/49] test: improve BalancesTracker unit test --- .../MultichainBalancesController/BalancesTracker.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts index 8854ecc422a..ed6409199f1 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.test.ts @@ -132,4 +132,12 @@ describe('BalancesTracker', () => { 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}`, + ); + }); }); From 425d18a86987f1ae95c5d5c4a6af02ccded227f1 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:00:00 -0300 Subject: [PATCH 14/49] test: improve Poller unit test --- .../Poller.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts index e79d4961a0c..60b6105226e 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts @@ -56,4 +56,25 @@ describe('Poller', () => { poller.stop(); 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(); + + 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); + + expect(callback).not.toHaveBeenCalled(); + }); }); From c8e3065992a5087c3eb9aabdbbe41062f59350ea Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:01:06 -0300 Subject: [PATCH 15/49] test: improve MultichainBalancesController unit test --- .../MultichainBalancesController.test.ts | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index 67bf150ba7d..bfa032dc79f 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -4,7 +4,7 @@ import type { CaipAssetType, InternalAccount, } from '@metamask/keyring-api'; -import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; +import { BtcAccountType, BtcMethod, EthAccountType, EthMethod } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import { v4 as uuidv4 } from 'uuid'; @@ -41,6 +41,27 @@ const mockBtcAccount = { 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, + }, + options: {}, + methods: [EthMethod.SignTypedDataV4, EthMethod.SignTransaction], + type: EthAccountType.Eoa, +}; + const mockBalanceResult = { 'bip122:000000000933ea01ad0ee984209779ba/slip44:0': { amount: '0.00000000', @@ -88,7 +109,7 @@ const setupController = ({ controllerMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', mockListMultichainAccounts.mockReturnValue( - mocks?.listMultichainAccounts ?? [mockBtcAccount], + mocks?.listMultichainAccounts ?? [mockBtcAccount, mockEthAccount], ), ); @@ -178,4 +199,22 @@ describe('BalancesController', () => { 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: {}, + }); + }); }); From e59a99bca617ca44118f5db49bbceeb9465833f6 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:32:27 -0300 Subject: [PATCH 16/49] refactor: BalancesTracker constructor --- .../BalancesTracker.ts | 6 +---- .../MultichainBalancesController.test.ts | 7 ++++- .../Poller.test.ts | 26 ++++++++++++++++--- .../MultichainBalancesController/Poller.ts | 10 ++++--- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index 27ed2f96931..7412d43ad9b 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -17,11 +17,7 @@ export class BalancesTracker { constructor(updateBalanceCallback: (accountId: string) => Promise) { this.#updateBalance = updateBalanceCallback; - this.#poller = new Poller(() => { - this.updateBalances().catch((error) => { - console.error('Error updating balances:', error); - }); - }, BALANCES_TRACKING_INTERVAL); + this.#poller = new Poller(this.updateBalances, BALANCES_TRACKING_INTERVAL); } /** diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index bfa032dc79f..1569fecf4ea 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -4,7 +4,12 @@ import type { CaipAssetType, InternalAccount, } from '@metamask/keyring-api'; -import { BtcAccountType, BtcMethod, EthAccountType, EthMethod } from '@metamask/keyring-api'; +import { + BtcAccountType, + BtcMethod, + EthAccountType, + EthMethod, +} from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import { v4 as uuidv4 } from 'uuid'; diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts index 60b6105226e..1438258bbd6 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts @@ -6,10 +6,10 @@ const interval = 1000; const intervalPlus100ms = interval + 100; describe('Poller', () => { - let callback: jest.Mock; + let callback: jest.Mock, []>; beforeEach(() => { - callback = jest.fn(); + callback = jest.fn().mockResolvedValue(undefined); }); afterEach(() => { @@ -22,6 +22,9 @@ describe('Poller', () => { jest.advanceTimersByTime(intervalPlus100ms); poller.stop(); + // Wait for all promises to resolve + await Promise.resolve(); + expect(callback).toHaveBeenCalledTimes(1); }); @@ -31,6 +34,9 @@ describe('Poller', () => { poller.stop(); jest.advanceTimersByTime(intervalPlus100ms); + // Wait for all promises to resolve + await Promise.resolve(); + expect(callback).not.toHaveBeenCalled(); }); @@ -44,16 +50,22 @@ describe('Poller', () => { 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 resumes before reaching out - // the `interval` timeout + // 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(); }); @@ -64,6 +76,9 @@ describe('Poller', () => { jest.advanceTimersByTime(intervalPlus100ms); poller.stop(); + // Wait for all promises to resolve + await Promise.resolve(); + expect(callback).toHaveBeenCalledTimes(1); }); @@ -75,6 +90,9 @@ describe('Poller', () => { poller.stop(); // Attempt to stop again jest.advanceTimersByTime(intervalPlus100ms); + // Wait for all promises to resolve + await Promise.resolve(); + expect(callback).not.toHaveBeenCalled(); }); }); diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts index 600e2ea615d..cb5a6612655 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts @@ -1,11 +1,11 @@ export class Poller { #interval: number; - #callback: () => void; + #callback: () => Promise; #handle: NodeJS.Timeout | undefined = undefined; - constructor(callback: () => void, interval: number) { + constructor(callback: () => Promise, interval: number) { this.#interval = interval; this.#callback = callback; } @@ -15,7 +15,11 @@ export class Poller { return; } - this.#handle = setInterval(this.#callback, this.#interval); + this.#handle = setInterval(() => { + this.#callback().catch((_error) => { + // Do nothing with the error for now + }); + }, this.#interval); } stop() { From 890d317c729ab23fc86255438032e1e6e5c17da0 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:35:54 -0300 Subject: [PATCH 17/49] test: add test to updateBalance --- .../MultichainBalancesController.test.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index 1569fecf4ea..b067f703939 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -152,7 +152,7 @@ describe('BalancesController', () => { expect(spyTracker).toHaveBeenCalledTimes(1); }); - it('update balances when calling updateBalances', async () => { + it('updates balances when calling updateBalances', async () => { const { controller } = setupController(); await controller.updateBalances(); @@ -164,7 +164,19 @@ describe('BalancesController', () => { }); }); - it('update balances when "AccountsController:accountAdded" is fired', async () => { + 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: { @@ -184,7 +196,7 @@ describe('BalancesController', () => { }); }); - it('update balances when "AccountsController:accountRemoved" is fired', async () => { + it('updates balances when "AccountsController:accountRemoved" is fired', async () => { const { controller, messenger, mockListMultichainAccounts } = setupController(); From 21059a91b3bbeeebdaf0518e33c302304db5eb83 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 3 Dec 2024 08:13:48 -0300 Subject: [PATCH 18/49] chore: add logs --- .../src/MultichainBalancesController/BalancesTracker.ts | 1 + .../MultichainBalancesController.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index 7412d43ad9b..ae7e067de7e 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -64,6 +64,7 @@ export class BalancesTracker { * @param blockTime - The block time (used when refreshing the account balances). */ track(accountId: string, blockTime: number) { + console.log('track', accountId, blockTime); // Do not overwrite current info if already being tracked! if (!this.isTracked(accountId)) { this.#balances[accountId] = { diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 9f7badff34b..151bfde965a 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -159,6 +159,7 @@ export class MultichainBalancesController extends BaseController< }, }); + console.log('Construct MultichainBalancesController'); this.#tracker = new BalancesTracker( async (accountId: string) => await this.#updateBalance(accountId), ); @@ -269,6 +270,8 @@ export class MultichainBalancesController extends BaseController< const scope = getScopeForAddress(account); const assetsList = this.#networkAssetsMap[scope]; + console.log({ account, scope, assetsList }); + if (account.metadata.snap) { partialState.balances[account.id] = await this.#getBalances( account.id, @@ -277,6 +280,8 @@ export class MultichainBalancesController extends BaseController< ); } + console.log({ partialState }); + this.update((state: Draft) => ({ ...state, balances: { @@ -295,6 +300,7 @@ export class MultichainBalancesController extends BaseController< async updateBalance(accountId: string) { // NOTE: No need to track the account here, since we start tracking those when // the "AccountsController:accountAdded" is fired. + console.log('update balance for account', accountId); await this.#tracker.updateBalance(accountId); } @@ -303,7 +309,9 @@ export class MultichainBalancesController extends BaseController< * anything, but it updates the state of the controller. */ async updateBalances() { + console.log('update balances'); await this.#tracker.updateBalances(); + console.log('STATE = ', this.state); } /** @@ -326,6 +334,7 @@ export class MultichainBalancesController extends BaseController< * @param account - The new account being added. */ async #handleOnAccountAdded(account: InternalAccount) { + console.log('handleOnAccountAdded', account.id); this.#trackAccount(account); // NOTE: Unfortunately, we cannot update the balance right away here, because // messenger's events are running synchronously and fetching the balance is From 7cc6bc03c5b6dfb6e13f7c714e90a87b4e5eedae Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:08:09 -0300 Subject: [PATCH 19/49] Revert "chore: add logs" This reverts commit 21059a91b3bbeeebdaf0518e33c302304db5eb83. --- .../src/MultichainBalancesController/BalancesTracker.ts | 1 - .../MultichainBalancesController.ts | 9 --------- 2 files changed, 10 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index ae7e067de7e..7412d43ad9b 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -64,7 +64,6 @@ export class BalancesTracker { * @param blockTime - The block time (used when refreshing the account balances). */ track(accountId: string, blockTime: number) { - console.log('track', accountId, blockTime); // Do not overwrite current info if already being tracked! if (!this.isTracked(accountId)) { this.#balances[accountId] = { diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 151bfde965a..9f7badff34b 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -159,7 +159,6 @@ export class MultichainBalancesController extends BaseController< }, }); - console.log('Construct MultichainBalancesController'); this.#tracker = new BalancesTracker( async (accountId: string) => await this.#updateBalance(accountId), ); @@ -270,8 +269,6 @@ export class MultichainBalancesController extends BaseController< const scope = getScopeForAddress(account); const assetsList = this.#networkAssetsMap[scope]; - console.log({ account, scope, assetsList }); - if (account.metadata.snap) { partialState.balances[account.id] = await this.#getBalances( account.id, @@ -280,8 +277,6 @@ export class MultichainBalancesController extends BaseController< ); } - console.log({ partialState }); - this.update((state: Draft) => ({ ...state, balances: { @@ -300,7 +295,6 @@ export class MultichainBalancesController extends BaseController< async updateBalance(accountId: string) { // NOTE: No need to track the account here, since we start tracking those when // the "AccountsController:accountAdded" is fired. - console.log('update balance for account', accountId); await this.#tracker.updateBalance(accountId); } @@ -309,9 +303,7 @@ export class MultichainBalancesController extends BaseController< * anything, but it updates the state of the controller. */ async updateBalances() { - console.log('update balances'); await this.#tracker.updateBalances(); - console.log('STATE = ', this.state); } /** @@ -334,7 +326,6 @@ export class MultichainBalancesController extends BaseController< * @param account - The new account being added. */ async #handleOnAccountAdded(account: InternalAccount) { - console.log('handleOnAccountAdded', account.id); this.#trackAccount(account); // NOTE: Unfortunately, we cannot update the balance right away here, because // messenger's events are running synchronously and fetching the balance is From 4556f0ae22b9b39e95e627de4df2e60559c87872 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:40:34 -0300 Subject: [PATCH 20/49] chore: update exports --- .../src/MultichainBalancesController/index.ts | 1 + packages/assets-controllers/src/index.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/index.ts b/packages/assets-controllers/src/MultichainBalancesController/index.ts index 5ecc04e4710..f9467d8c1c0 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/index.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/index.ts @@ -1,3 +1,4 @@ +export { BalancesTracker } from './BalancesTracker'; export { MultichainBalancesController } from './MultichainBalancesController'; export type { MultichainBalancesControllerState, diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index 64b653692a9..a456efd28cb 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -147,7 +147,10 @@ export type { RatesControllerPollingStartedEvent, RatesControllerPollingStoppedEvent, } from './RatesController'; -export { MultichainBalancesController } from './MultichainBalancesController'; +export { + BalancesTracker, + MultichainBalancesController, +} from './MultichainBalancesController'; export type { MultichainBalancesControllerState, MultichainBalancesControllerGetStateAction, From d069a64ea4d1aea41c912c6cf73cc64ff84244f0 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:45:19 -0300 Subject: [PATCH 21/49] chore: update exports --- .../src/MultichainBalancesController/index.ts | 6 ++++++ packages/assets-controllers/src/index.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/packages/assets-controllers/src/MultichainBalancesController/index.ts b/packages/assets-controllers/src/MultichainBalancesController/index.ts index f9467d8c1c0..4b000464b17 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/index.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/index.ts @@ -1,5 +1,11 @@ export { BalancesTracker } from './BalancesTracker'; export { MultichainBalancesController } from './MultichainBalancesController'; +export { + BALANCE_UPDATE_INTERVALS, + NETWORK_ASSETS_MAP, + MultichainNetworks, + MultichainNativeAssets, +} from './constants'; export type { MultichainBalancesControllerState, MultichainBalancesControllerGetStateAction, diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index a456efd28cb..6e4dcfbc1d8 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -150,6 +150,11 @@ export type { export { BalancesTracker, MultichainBalancesController, + // constants + BALANCE_UPDATE_INTERVALS, + NETWORK_ASSETS_MAP, + MultichainNetworks, + MultichainNativeAssets, } from './MultichainBalancesController'; export type { MultichainBalancesControllerState, From 9e30e90aab15bd5dfa6e1431aa1670b040cd4cc1 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:10:05 -0300 Subject: [PATCH 22/49] chore: update imports --- .../MultichainBalancesController.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 9f7badff34b..eac59da8919 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -22,8 +22,11 @@ import { HandlerType } from '@metamask/snaps-utils'; import type { Json, JsonRpcRequest } from '@metamask/utils'; import type { Draft } from 'immer'; -import { BalancesTracker } from './BalancesTracker'; -import { BALANCE_UPDATE_INTERVALS, NETWORK_ASSETS_MAP } from './constants'; +import { + BalancesTracker, + BALANCE_UPDATE_INTERVALS, + NETWORK_ASSETS_MAP, +} from '.'; import { getScopeForAddress } from './utils'; const controllerName = 'MultichainBalancesController'; From e1a582c92678432c65aa541d5348aa7ba666be3b Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:51:15 -0300 Subject: [PATCH 23/49] chore: add console.error to Poller --- .../src/MultichainBalancesController/Poller.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts index cb5a6612655..d12eb1baea3 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts @@ -16,9 +16,7 @@ export class Poller { } this.#handle = setInterval(() => { - this.#callback().catch((_error) => { - // Do nothing with the error for now - }); + this.#callback().catch(console.error); }, this.#interval); } From c3f273dba82851c1300ce1ed7e116b2c312a00c1 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:55:28 -0300 Subject: [PATCH 24/49] chore: state update --- .../MultichainBalancesController.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index eac59da8919..6c035d87490 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -280,13 +280,12 @@ export class MultichainBalancesController extends BaseController< ); } - this.update((state: Draft) => ({ - ...state, - balances: { + this.update((state: Draft) => { + state.balances = { ...state.balances, ...partialState.balances, - }, - })); + }; + }); } /** From a1e11fbbc344073671ec171a3cbb0d44a37f76d5 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:28:51 -0300 Subject: [PATCH 25/49] chore: add logs --- .../BalancesTracker.ts | 7 +++++++ .../MultichainBalancesController.ts | 20 +++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index 7412d43ad9b..9ddcb4c86a0 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -92,6 +92,10 @@ export class BalancesTracker { */ async updateBalance(accountId: string) { this.assertBeingTracked(accountId); + console.log( + 'MultichainBalancesController - BalancesTracker updateBalance', + { accountId }, + ); // We check if the balance is outdated (by comparing to the block time associated // with this kind of account). @@ -112,6 +116,9 @@ export class BalancesTracker { * is considered outdated). */ async updateBalances() { + console.log( + 'MultichainBalancesController - BalancesTracker updateBalances', + ); await Promise.allSettled( Object.keys(this.#balances).map(async (accountId) => { await this.updateBalance(accountId); diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 6c035d87490..84ced7ce097 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -203,6 +203,10 @@ export class MultichainBalancesController extends BaseController< #trackAccount(account: InternalAccount): void { if (!this.#isNonEvmAccount(account)) { // Nothing to do here for EVM accounts + console.log( + 'MultichainBalancesController - early return in #trackAccount', + account, + ); return; } @@ -280,6 +284,7 @@ export class MultichainBalancesController extends BaseController< ); } + console.log('MultichainBalancesController update state', { partialState }); this.update((state: Draft) => { state.balances = { ...state.balances, @@ -295,6 +300,7 @@ export class MultichainBalancesController extends BaseController< * @param accountId - The account ID. */ async updateBalance(accountId: string) { + console.log('MultichainBalancesController updateBalance', { accountId }); // NOTE: No need to track the account here, since we start tracking those when // the "AccountsController:accountAdded" is fired. await this.#tracker.updateBalance(accountId); @@ -305,6 +311,7 @@ export class MultichainBalancesController extends BaseController< * anything, but it updates the state of the controller. */ async updateBalances() { + console.log('MultichainBalancesController updateBalances'); await this.#tracker.updateBalances(); } @@ -328,6 +335,9 @@ export class MultichainBalancesController extends BaseController< * @param account - The new account being added. */ async #handleOnAccountAdded(account: InternalAccount) { + console.log('MultichainBalancesController handleOnAccountAdded', { + account, + }); this.#trackAccount(account); // NOTE: Unfortunately, we cannot update the balance right away here, because // messenger's events are running synchronously and fetching the balance is @@ -344,6 +354,9 @@ export class MultichainBalancesController extends BaseController< * @param accountId - The account ID being removed. */ async #handleOnAccountRemoved(accountId: string) { + console.log('MultichainBalancesController handleOnAccountAdded', { + accountId, + }); if (this.#tracker.isTracked(accountId)) { this.#tracker.untrack(accountId); } @@ -369,10 +382,9 @@ export class MultichainBalancesController extends BaseController< snapId: string, assetTypes: CaipAssetType[], ): Promise> { - return await this.#getClient(snapId).getAccountBalances( - accountId, - assetTypes, - ); + const keyringClient = this.#getClient(snapId); + console.log('MultichainBalancesController', { keyringClient }); + return await keyringClient.getAccountBalances(accountId, assetTypes); } /** From 3178c25e4d5c6c8a0881410a5bec58f71b97b1fd Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:59:24 -0300 Subject: [PATCH 26/49] chore: add more logs --- .../MultichainBalancesController.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 84ced7ce097..486356319c4 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -271,10 +271,14 @@ export class MultichainBalancesController extends BaseController< */ async #updateBalance(accountId: string) { const account = this.#getAccount(accountId); + console.log('MultichainBalancesController updateBalance', { account }); const partialState: MultichainBalancesControllerState = { balances: {} }; + console.log('MultichainBalancesController updateBalance', { partialState }); const scope = getScopeForAddress(account); + console.log('MultichainBalancesController updateBalance', { scope }); const assetsList = this.#networkAssetsMap[scope]; + console.log('MultichainBalancesController updateBalance', { assetsList }); if (account.metadata.snap) { partialState.balances[account.id] = await this.#getBalances( @@ -382,6 +386,11 @@ export class MultichainBalancesController extends BaseController< snapId: string, assetTypes: CaipAssetType[], ): Promise> { + console.log('MultichainBalancesController - getBalances', { + accountId, + snapId, + assetTypes, + }); const keyringClient = this.#getClient(snapId); console.log('MultichainBalancesController', { keyringClient }); return await keyringClient.getAccountBalances(accountId, assetTypes); From 73fa7bfb769c7f0f0b7296fb0687d0636a10222c Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:57:25 -0300 Subject: [PATCH 27/49] chore: add more logs --- .../MultichainBalancesController/BalancesTracker.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index 9ddcb4c86a0..c76fb68fe80 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -41,7 +41,7 @@ export class BalancesTracker { * @returns True if the account is being tracker, false otherwise. */ isTracked(accountId: string) { - return accountId in this.#balances; + return Object.prototype.hasOwnProperty.call(this.#balances, accountId); } /** @@ -91,6 +91,9 @@ export class BalancesTracker { * @throws If the account ID is not being tracked. */ async updateBalance(accountId: string) { + console.log( + 'MultichainBalancesController - BalancesTracker updateBalance start', + ); this.assertBeingTracked(accountId); console.log( 'MultichainBalancesController - BalancesTracker updateBalance', @@ -103,8 +106,12 @@ export class BalancesTracker { // 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]; + console.log({ info }); + const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; + console.log({ isOutdated }); const hasNoBalanceYet = info.lastUpdated === 0; + console.log({ hasNoBalanceYet }); if (hasNoBalanceYet || isOutdated) { await this.#updateBalance(accountId); this.#balances[accountId].lastUpdated = Date.now(); @@ -118,6 +125,7 @@ export class BalancesTracker { async updateBalances() { console.log( 'MultichainBalancesController - BalancesTracker updateBalances', + { balances: this.#balances }, ); await Promise.allSettled( Object.keys(this.#balances).map(async (accountId) => { From bb93981c84ea6cc88d109ebe116a1d6be4486dde Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:20:54 -0300 Subject: [PATCH 28/49] chore: update logs --- .../BalancesTracker.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index c76fb68fe80..ea63c7ef4e8 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -41,7 +41,15 @@ export class BalancesTracker { * @returns True if the account is being tracker, false otherwise. */ isTracked(accountId: string) { - return Object.prototype.hasOwnProperty.call(this.#balances, accountId); + console.log('MultichainBalancesController - BalancesTracker isTracked'); + const result = Object.prototype.hasOwnProperty.call( + this.#balances, + accountId, + ); + console.log('MultichainBalancesController - BalancesTracker isTracked', { + result, + }); + return result; } /** @@ -51,6 +59,10 @@ export class BalancesTracker { * @throws If the account ID is not being tracked. */ assertBeingTracked(accountId: string) { + console.log( + 'MultichainBalancesController - BalancesTracker assertBeingTracked', + { accountId }, + ); if (!this.isTracked(accountId)) { throw new Error(`Account is not being tracked: ${accountId}`); } From 243de73b3f256f8f03cf9d6740252af109960edc Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:59:42 -0300 Subject: [PATCH 29/49] chore: revert changes --- .../BalancesTracker.ts | 29 +---- .../MultichainBalancesController.ts | 114 +++++++++--------- .../MultichainBalancesController/Poller.ts | 8 +- .../MultichainBalancesController/constants.ts | 20 +-- .../src/MultichainBalancesController/utils.ts | 2 +- 5 files changed, 69 insertions(+), 104 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index ea63c7ef4e8..7412d43ad9b 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -41,15 +41,7 @@ export class BalancesTracker { * @returns True if the account is being tracker, false otherwise. */ isTracked(accountId: string) { - console.log('MultichainBalancesController - BalancesTracker isTracked'); - const result = Object.prototype.hasOwnProperty.call( - this.#balances, - accountId, - ); - console.log('MultichainBalancesController - BalancesTracker isTracked', { - result, - }); - return result; + return accountId in this.#balances; } /** @@ -59,10 +51,6 @@ export class BalancesTracker { * @throws If the account ID is not being tracked. */ assertBeingTracked(accountId: string) { - console.log( - 'MultichainBalancesController - BalancesTracker assertBeingTracked', - { accountId }, - ); if (!this.isTracked(accountId)) { throw new Error(`Account is not being tracked: ${accountId}`); } @@ -103,14 +91,7 @@ export class BalancesTracker { * @throws If the account ID is not being tracked. */ async updateBalance(accountId: string) { - console.log( - 'MultichainBalancesController - BalancesTracker updateBalance start', - ); this.assertBeingTracked(accountId); - console.log( - 'MultichainBalancesController - BalancesTracker updateBalance', - { accountId }, - ); // We check if the balance is outdated (by comparing to the block time associated // with this kind of account). @@ -118,12 +99,8 @@ export class BalancesTracker { // 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]; - console.log({ info }); - const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; - console.log({ isOutdated }); const hasNoBalanceYet = info.lastUpdated === 0; - console.log({ hasNoBalanceYet }); if (hasNoBalanceYet || isOutdated) { await this.#updateBalance(accountId); this.#balances[accountId].lastUpdated = Date.now(); @@ -135,10 +112,6 @@ export class BalancesTracker { * is considered outdated). */ async updateBalances() { - console.log( - 'MultichainBalancesController - BalancesTracker updateBalances', - { balances: this.#balances }, - ); await Promise.allSettled( Object.keys(this.#balances).map(async (accountId) => { await this.updateBalance(accountId); diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 486356319c4..16ee6655869 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -10,11 +10,13 @@ import { type RestrictedControllerMessenger, } from '@metamask/base-controller'; import { + BtcAccountType, KeyringClient, type Balance, type CaipAssetType, type InternalAccount, isEvmAccountType, + SolAccountType, } from '@metamask/keyring-api'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; @@ -27,7 +29,7 @@ import { BALANCE_UPDATE_INTERVALS, NETWORK_ASSETS_MAP, } from '.'; -import { getScopeForAddress } from './utils'; +import { getScopeForAccount } from './utils'; const controllerName = 'MultichainBalancesController'; @@ -60,7 +62,7 @@ export type MultichainBalancesControllerGetStateAction = >; /** - * Updates the balances of all supported accounts in {@link MultichainBalancesController}. + * Updates the balances of all supported accounts. */ export type MultichainBalancesControllerUpdateBalancesAction = { type: `${typeof controllerName}:updateBalances`; @@ -86,8 +88,7 @@ export type MultichainBalancesControllerActions = /** * Events emitted by {@link MultichainBalancesController}. */ -export type MultichainBalancesControllerEvents = - MultichainBalancesControllerStateChange; +export type MultichainBalancesControllerEvents = MultichainBalancesControllerStateChange; /** * Actions that this controller is allowed to call. @@ -116,19 +117,31 @@ export type MultichainBalancesControllerMessenger = >; /** - * {@link multichainBalancesController}'s metadata. + * {@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 multichainBalancesControllerMetadata = { +const balancesControllerMetadata = { balances: { persist: true, anonymous: false, }, }; +const BTC_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds +const SOLANA_AVG_BLOCK_TIME = 400; // 400 milliseconds + +// NOTE: We set an interval of half the average block time to mitigate when our interval +// is de-synchronized with the actual block time. +export const BTC_BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2; + +const BALANCE_CHECK_INTERVALS = { + [BtcAccountType.P2wpkh]: BTC_BALANCES_UPDATE_TIME, + [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, +}; + /** * The MultichainBalancesController is responsible for fetching and caching account * balances. @@ -138,12 +151,7 @@ export class MultichainBalancesController extends BaseController< MultichainBalancesControllerState, MultichainBalancesControllerMessenger > { - readonly #tracker: BalancesTracker; - - // As a temporary solution, we are using a map to store the assets - // that are hardcoded in the module. In the future, this mapping - // should be dynamic to allow a client to register and unregister assets - readonly #networkAssetsMap: Record = NETWORK_ASSETS_MAP; + #tracker: BalancesTracker; constructor({ messenger, @@ -155,7 +163,7 @@ export class MultichainBalancesController extends BaseController< super({ messenger, name: controllerName, - metadata: multichainBalancesControllerMetadata, + metadata: balancesControllerMetadata, state: { ...defaultState, ...state, @@ -168,7 +176,9 @@ export class MultichainBalancesController extends BaseController< // Register all non-EVM accounts into the tracker for (const account of this.#listAccounts()) { - this.#trackAccount(account); + if (this.#isNonEvmAccount(account)) { + this.#tracker.track(account.id, this.#getBlockTimeFor(account)); + } } this.messagingSystem.subscribe( @@ -184,38 +194,32 @@ export class MultichainBalancesController extends BaseController< /** * Starts the polling process. */ - start(): void { + async start(): Promise { this.#tracker.start(); } /** * Stops the polling process. */ - stop(): void { + async stop(): Promise { this.#tracker.stop(); } /** - * Tracks an account to get its balances. + * Gets the block time for a given account. * - * @param account - The account to track. + * @param account - The account to get the block time for. + * @returns The block time for the account. */ - #trackAccount(account: InternalAccount): void { - if (!this.#isNonEvmAccount(account)) { - // Nothing to do here for EVM accounts - console.log( - 'MultichainBalancesController - early return in #trackAccount', - account, - ); - return; + #getBlockTimeFor(account: InternalAccount): number { + if (account.type in BALANCE_CHECK_INTERVALS) { + return BALANCE_UPDATE_INTERVALS[ + account.type as keyof typeof BALANCE_UPDATE_INTERVALS + ]; } - - const updateTime = - // @ts-expect-error - For the moment we are only tracking non-EVM accounts and this is - // checked with the method `#isNonEvmAccount`. We can ignore this error since - // eip155:eoa and eip155:erc4337 account type balances are not tracked here. - BALANCE_UPDATE_INTERVALS[this.#getAccount(account.id).type]; - this.#tracker.track(account.id, updateTime); + throw new Error( + `Unsupported account type for balance tracking: ${account.type}`, + ); } /** @@ -232,14 +236,16 @@ export class MultichainBalancesController extends BaseController< /** * Lists the accounts that we should get balances for. * - * Currently, we only get balances for non-EVM accounts. - * * @returns A list of accounts that we should get balances for. */ #listAccounts(): InternalAccount[] { const accounts = this.#listMultichainAccounts(); - return accounts.filter((account) => this.#isNonEvmAccount(account)); + return accounts.filter( + (account) => + account.type === SolAccountType.DataAccount || + account.type === BtcAccountType.P2wpkh, + ); } /** @@ -271,24 +277,19 @@ export class MultichainBalancesController extends BaseController< */ async #updateBalance(accountId: string) { const account = this.#getAccount(accountId); - console.log('MultichainBalancesController updateBalance', { account }); const partialState: MultichainBalancesControllerState = { balances: {} }; - console.log('MultichainBalancesController updateBalance', { partialState }); - - const scope = getScopeForAddress(account); - console.log('MultichainBalancesController updateBalance', { scope }); - const assetsList = this.#networkAssetsMap[scope]; - console.log('MultichainBalancesController updateBalance', { assetsList }); if (account.metadata.snap) { + const scope = getScopeForAccount(account); + const assetTypes = NETWORK_ASSETS_MAP[scope]; + partialState.balances[account.id] = await this.#getBalances( account.id, account.metadata.snap.id, - assetsList, + assetTypes, ); } - console.log('MultichainBalancesController update state', { partialState }); this.update((state: Draft) => { state.balances = { ...state.balances, @@ -304,7 +305,6 @@ export class MultichainBalancesController extends BaseController< * @param accountId - The account ID. */ async updateBalance(accountId: string) { - console.log('MultichainBalancesController updateBalance', { accountId }); // NOTE: No need to track the account here, since we start tracking those when // the "AccountsController:accountAdded" is fired. await this.#tracker.updateBalance(accountId); @@ -315,7 +315,6 @@ export class MultichainBalancesController extends BaseController< * anything, but it updates the state of the controller. */ async updateBalances() { - console.log('MultichainBalancesController updateBalances'); await this.#tracker.updateBalances(); } @@ -339,10 +338,12 @@ export class MultichainBalancesController extends BaseController< * @param account - The new account being added. */ async #handleOnAccountAdded(account: InternalAccount) { - console.log('MultichainBalancesController handleOnAccountAdded', { - account, - }); - this.#trackAccount(account); + if (!this.#isNonEvmAccount(account)) { + // Nothing to do here for EVM accounts + return; + } + + this.#tracker.track(account.id, this.#getBlockTimeFor(account)); // NOTE: Unfortunately, we cannot update the balance right away here, because // messenger's events are running synchronously and fetching the balance is // asynchronous. @@ -358,9 +359,6 @@ export class MultichainBalancesController extends BaseController< * @param accountId - The account ID being removed. */ async #handleOnAccountRemoved(accountId: string) { - console.log('MultichainBalancesController handleOnAccountAdded', { - accountId, - }); if (this.#tracker.isTracked(accountId)) { this.#tracker.untrack(accountId); } @@ -386,14 +384,10 @@ export class MultichainBalancesController extends BaseController< snapId: string, assetTypes: CaipAssetType[], ): Promise> { - console.log('MultichainBalancesController - getBalances', { + return await this.#getClient(snapId).getAccountBalances( accountId, - snapId, assetTypes, - }); - const keyringClient = this.#getClient(snapId); - console.log('MultichainBalancesController', { keyringClient }); - return await keyringClient.getAccountBalances(accountId, assetTypes); + ); } /** diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts index d12eb1baea3..600e2ea615d 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts @@ -1,11 +1,11 @@ export class Poller { #interval: number; - #callback: () => Promise; + #callback: () => void; #handle: NodeJS.Timeout | undefined = undefined; - constructor(callback: () => Promise, interval: number) { + constructor(callback: () => void, interval: number) { this.#interval = interval; this.#callback = callback; } @@ -15,9 +15,7 @@ export class Poller { return; } - this.#handle = setInterval(() => { - this.#callback().catch(console.error); - }, this.#interval); + this.#handle = setInterval(this.#callback, this.#interval); } stop() { diff --git a/packages/assets-controllers/src/MultichainBalancesController/constants.ts b/packages/assets-controllers/src/MultichainBalancesController/constants.ts index 852a0c0d4f7..8b7d79cc9fa 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/constants.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/constants.ts @@ -1,15 +1,5 @@ import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; -const BTC_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 fot bitcoin - // to mitigate when our interval is de-synchronized with the actual block time. - [BtcAccountType.P2wpkh]: BTC_AVG_BLOCK_TIME / 2, - [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, -}; - /** * The network identifiers for supported networks in CAIP-2 format. * Note: This is a temporary workaround until we have a more robust @@ -31,6 +21,16 @@ export enum MultichainNativeAssets { SolanaTestnet = `${MultichainNetworks.SolanaTestnet}/slip44:501`, } +const BTC_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 fot bitcoin + // to mitigate when our interval is de-synchronized with the actual block time. + [BtcAccountType.P2wpkh]: BTC_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. diff --git a/packages/assets-controllers/src/MultichainBalancesController/utils.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.ts index f04477664a9..229e68a5232 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/utils.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.ts @@ -48,7 +48,7 @@ export const getScopeForSolAddress = (account: InternalAccount): string => { * @param account - The account to get the scope for. * @returns The scope for the given account. */ -export const getScopeForAddress = (account: InternalAccount): string => { +export const getScopeForAccount = (account: InternalAccount): string => { switch (account.type) { case BtcAccountType.P2wpkh: return getScopeForBtcAddress(account); From 7612528fe4865a5fdfd2e7f2beae3ebc2cb086b3 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:03:12 -0300 Subject: [PATCH 30/49] test: fix unit test issues --- .../MultichainBalancesController.ts | 4 ++-- .../src/MultichainBalancesController/utils.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 16ee6655869..4d38e308228 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -194,14 +194,14 @@ export class MultichainBalancesController extends BaseController< /** * Starts the polling process. */ - async start(): Promise { + start(): void { this.#tracker.start(); } /** * Stops the polling process. */ - async stop(): Promise { + stop(): void { this.#tracker.stop(); } diff --git a/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts index c56a36183a8..3e65f473a05 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts @@ -12,7 +12,7 @@ import { MultichainNetworks } from './constants'; import { getScopeForBtcAddress, getScopeForSolAddress, - getScopeForAddress, + getScopeForAccount, } from './utils'; const mockBtcAccount = { @@ -141,7 +141,7 @@ describe('getScopeForAddress', () => { }; (validate as jest.Mock).mockReturnValueOnce(true); - const scope = getScopeForAddress(account); + const scope = getScopeForAccount(account); expect(scope).toBe(MultichainNetworks.Bitcoin); }); @@ -152,7 +152,7 @@ describe('getScopeForAddress', () => { options: { scope: 'solana-scope' }, }; - const scope = getScopeForAddress(account); + const scope = getScopeForAccount(account); expect(scope).toBe('solana-scope'); }); @@ -164,7 +164,7 @@ describe('getScopeForAddress', () => { }; // @ts-expect-error - We're testing an error case. - expect(() => getScopeForAddress(account)).toThrow( + expect(() => getScopeForAccount(account)).toThrow( `Unsupported non-EVM account type: ${account.type}`, ); }); From 9e6771633a6105109245ed52be60311b8f655f9a Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:13:04 -0300 Subject: [PATCH 31/49] fix: lint --- .../MultichainBalancesController.ts | 3 ++- .../src/MultichainBalancesController/Poller.ts | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 4d38e308228..191a57d10e8 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -88,7 +88,8 @@ export type MultichainBalancesControllerActions = /** * Events emitted by {@link MultichainBalancesController}. */ -export type MultichainBalancesControllerEvents = MultichainBalancesControllerStateChange; +export type MultichainBalancesControllerEvents = + MultichainBalancesControllerStateChange; /** * Actions that this controller is allowed to call. diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts index 600e2ea615d..d12eb1baea3 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts @@ -1,11 +1,11 @@ export class Poller { #interval: number; - #callback: () => void; + #callback: () => Promise; #handle: NodeJS.Timeout | undefined = undefined; - constructor(callback: () => void, interval: number) { + constructor(callback: () => Promise, interval: number) { this.#interval = interval; this.#callback = callback; } @@ -15,7 +15,9 @@ export class Poller { return; } - this.#handle = setInterval(this.#callback, this.#interval); + this.#handle = setInterval(() => { + this.#callback().catch(console.error); + }, this.#interval); } stop() { From a49ed0d4f09032d874cbbcba51ca8c43f6fe0681 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 9 Dec 2024 08:29:41 -0300 Subject: [PATCH 32/49] chore: BalancesTracker method --- .../src/MultichainBalancesController/BalancesTracker.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index 7412d43ad9b..83af9160186 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -5,7 +5,7 @@ type BalanceInfo = { blockTime: number; }; -const BALANCES_TRACKING_INTERVAL = 30 * 1000; // Every 30s in milliseconds. +const BALANCES_TRACKING_INTERVAL = 5000; // Every 30s in milliseconds. export class BalancesTracker { #poller: Poller; @@ -17,7 +17,10 @@ export class BalancesTracker { constructor(updateBalanceCallback: (accountId: string) => Promise) { this.#updateBalance = updateBalanceCallback; - this.#poller = new Poller(this.updateBalances, BALANCES_TRACKING_INTERVAL); + this.#poller = new Poller( + () => this.updateBalances(), + BALANCES_TRACKING_INTERVAL, + ); } /** From c548615be55e0e5669d3ed836ae6892e819a2972 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 9 Dec 2024 08:30:06 -0300 Subject: [PATCH 33/49] chore: update constant --- .../MultichainBalancesController.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 191a57d10e8..0ff5fd90a25 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -131,18 +131,6 @@ const balancesControllerMetadata = { }, }; -const BTC_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds -const SOLANA_AVG_BLOCK_TIME = 400; // 400 milliseconds - -// NOTE: We set an interval of half the average block time to mitigate when our interval -// is de-synchronized with the actual block time. -export const BTC_BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2; - -const BALANCE_CHECK_INTERVALS = { - [BtcAccountType.P2wpkh]: BTC_BALANCES_UPDATE_TIME, - [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, -}; - /** * The MultichainBalancesController is responsible for fetching and caching account * balances. @@ -213,7 +201,7 @@ export class MultichainBalancesController extends BaseController< * @returns The block time for the account. */ #getBlockTimeFor(account: InternalAccount): number { - if (account.type in BALANCE_CHECK_INTERVALS) { + if (account.type in BALANCE_UPDATE_INTERVALS) { return BALANCE_UPDATE_INTERVALS[ account.type as keyof typeof BALANCE_UPDATE_INTERVALS ]; From 115e32927d91a9fcc1f4e0c83f3bcbead88d59ad Mon Sep 17 00:00:00 2001 From: Gustavo Antunes <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:20:04 -0300 Subject: [PATCH 34/49] chore: nit changes Co-authored-by: Charly Chevalier --- .../src/MultichainBalancesController/BalancesTracker.ts | 6 +++--- .../src/MultichainBalancesController/constants.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index 83af9160186..174246a60d2 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -5,7 +5,7 @@ type BalanceInfo = { blockTime: number; }; -const BALANCES_TRACKING_INTERVAL = 5000; // Every 30s in milliseconds. +const BALANCES_TRACKING_INTERVAL = 5000; // Every 5s in milliseconds. export class BalancesTracker { #poller: Poller; @@ -41,10 +41,10 @@ export class BalancesTracker { * Checks if an account ID is being tracked. * * @param accountId - The account ID. - * @returns True if the account is being tracker, false otherwise. + * @returns True if the account is being tracked, false otherwise. */ isTracked(accountId: string) { - return accountId in this.#balances; + return this.#balances.hasOwnProperty(accountId); } /** diff --git a/packages/assets-controllers/src/MultichainBalancesController/constants.ts b/packages/assets-controllers/src/MultichainBalancesController/constants.ts index 8b7d79cc9fa..acbd134c2d0 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/constants.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/constants.ts @@ -25,7 +25,7 @@ const BTC_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 fot bitcoin + // 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]: BTC_AVG_BLOCK_TIME / 2, [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, From 41ccd3bf5d3756fc07717a76d31effaa065ea4b1 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:21:17 -0300 Subject: [PATCH 35/49] refactor: updateBalance method --- .../MultichainBalancesController/BalancesTracker.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index 83af9160186..4fc41d545b4 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -102,9 +102,7 @@ export class BalancesTracker { // 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]; - const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; - const hasNoBalanceYet = info.lastUpdated === 0; - if (hasNoBalanceYet || isOutdated) { + if (this.#isBalanceOutdated(info)) { await this.#updateBalance(accountId); this.#balances[accountId].lastUpdated = Date.now(); } @@ -121,4 +119,13 @@ export class BalancesTracker { }), ); } + + #isBalanceOutdated({ lastUpdated, blockTime }: BalanceInfo) { + return ( + // Never been updated: + lastUpdated === 0 || + // Outdated: + Date.now() - lastUpdated >= blockTime + ); + } } From 017c657b1fe8cee52a607d13a4186947f4135f54 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:21:34 -0300 Subject: [PATCH 36/49] refactor: BITCOIN_AVG_BLOCK_TIME var name --- .../src/MultichainBalancesController/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/constants.ts b/packages/assets-controllers/src/MultichainBalancesController/constants.ts index 8b7d79cc9fa..8681339825d 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/constants.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/constants.ts @@ -21,13 +21,13 @@ export enum MultichainNativeAssets { SolanaTestnet = `${MultichainNetworks.SolanaTestnet}/slip44:501`, } -const BTC_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds +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 fot bitcoin // to mitigate when our interval is de-synchronized with the actual block time. - [BtcAccountType.P2wpkh]: BTC_AVG_BLOCK_TIME / 2, + [BtcAccountType.P2wpkh]: BITCOIN_AVG_BLOCK_TIME / 2, [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, }; From fe5d9ec31b94bbec89d72e1e86593a24c2b7e43c Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:10:18 -0300 Subject: [PATCH 37/49] chore: add error classes --- .../error.test.ts | 23 +++++++++++++++++++ .../src/MultichainBalancesController/error.ts | 13 +++++++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/assets-controllers/src/MultichainBalancesController/error.test.ts create mode 100644 packages/assets-controllers/src/MultichainBalancesController/error.ts 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'; + } +} From ae590d8a896fe9dbd08287097639fc5f703f54de Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:10:32 -0300 Subject: [PATCH 38/49] refactor: listAccounts --- .../MultichainBalancesController.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 0ff5fd90a25..efb8c1b6962 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -230,11 +230,7 @@ export class MultichainBalancesController extends BaseController< #listAccounts(): InternalAccount[] { const accounts = this.#listMultichainAccounts(); - return accounts.filter( - (account) => - account.type === SolAccountType.DataAccount || - account.type === BtcAccountType.P2wpkh, - ); + return accounts.filter((account) => this.#isNonEvmAccount(account)); } /** From b355d119270e80f01195d9fc67e36651c9711033 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:11:05 -0300 Subject: [PATCH 39/49] refactor: Poller class to use PollerError --- .../Poller.test.ts | 20 +++++++++++++++++++ .../MultichainBalancesController/Poller.ts | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts index 1438258bbd6..aba0e4041ba 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.test.ts @@ -1,3 +1,4 @@ +import { PollerError } from './error'; import { Poller } from './Poller'; jest.useFakeTimers(); @@ -95,4 +96,23 @@ describe('Poller', () => { 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 index d12eb1baea3..c0167790c8d 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/Poller.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/Poller.ts @@ -1,3 +1,5 @@ +import { PollerError } from './error'; + export class Poller { #interval: number; @@ -16,7 +18,9 @@ export class Poller { } this.#handle = setInterval(() => { - this.#callback().catch(console.error); + this.#callback().catch((err) => { + console.error(new PollerError(err.message)); + }); }, this.#interval); } From 936559748c14df0cc29353aa623a759161bd0c94 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:13:37 -0300 Subject: [PATCH 40/49] chore: add JSDocs to isBalanceOutdated method --- .../MultichainBalancesController/BalancesTracker.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index 4fc41d545b4..efef07cd2a4 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -120,7 +120,15 @@ export class BalancesTracker { ); } - #isBalanceOutdated({ lastUpdated, blockTime }: BalanceInfo) { + /** + * 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 || From 84157f0c741c1014f8e27c1db72c4e0cb89657d7 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:18:22 -0300 Subject: [PATCH 41/49] fix: lint issue with isTracked --- .../src/MultichainBalancesController/BalancesTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts index bdd0af61f82..661c229a82d 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/BalancesTracker.ts @@ -44,7 +44,7 @@ export class BalancesTracker { * @returns True if the account is being tracked, false otherwise. */ isTracked(accountId: string) { - return this.#balances.hasOwnProperty(accountId); + return Object.prototype.hasOwnProperty.call(this.#balances, accountId); } /** From ef8b0f57636656ad0ffb26db1212ab01b579b74e Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:26:16 -0300 Subject: [PATCH 42/49] fix: remove unused vars --- .../MultichainBalancesController.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index efb8c1b6962..0af37ca6b59 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -10,13 +10,11 @@ import { type RestrictedControllerMessenger, } from '@metamask/base-controller'; import { - BtcAccountType, KeyringClient, type Balance, type CaipAssetType, type InternalAccount, isEvmAccountType, - SolAccountType, } from '@metamask/keyring-api'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; From 985aed52e342351c0361de095af15eb0d2e4228e Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:38:06 -0300 Subject: [PATCH 43/49] test: fix import for MultichainBalancesController.test --- .../MultichainBalancesController.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index b067f703939..a89fb2d1a25 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -1,9 +1,5 @@ import { ControllerMessenger } from '@metamask/base-controller'; -import type { - Balance, - CaipAssetType, - InternalAccount, -} from '@metamask/keyring-api'; +import type { Balance, CaipAssetType } from '@metamask/keyring-api'; import { BtcAccountType, BtcMethod, @@ -11,6 +7,7 @@ import { EthMethod, } 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 { BalancesTracker } from './BalancesTracker'; From 80f42889e1cd30da2498379dbe3140fdb7b1bf02 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:48:17 -0300 Subject: [PATCH 44/49] chore: fix import in utils file --- .../src/MultichainBalancesController/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/utils.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.ts index 229e68a5232..72728b2299a 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/utils.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.ts @@ -1,5 +1,5 @@ -import type { InternalAccount } from '@metamask/keyring-api'; import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { validate, Network } from 'bitcoin-address-validation'; import { MultichainNetworks } from './constants'; From bcaee6c42b4c196809fe2c59cd611ca155794b28 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:52:13 -0300 Subject: [PATCH 45/49] refactor: apply review suggestions --- .../MultichainBalancesController.test.ts | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index a89fb2d1a25..68e72b2eb62 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -10,14 +10,16 @@ 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, - defaultState, + getDefaultMultichainBalancesControllerState, } from './MultichainBalancesController'; import type { - AllowedActions, - AllowedEvents, MultichainBalancesControllerMessenger, MultichainBalancesControllerState, } from './MultichainBalancesController'; @@ -71,8 +73,31 @@ const mockBalanceResult = { }, }; +/** + * 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 = defaultState, + state = getDefaultMultichainBalancesControllerState(), mocks, }: { state?: MultichainBalancesControllerState; @@ -81,10 +106,7 @@ const setupController = ({ handleRequestReturnValue?: Record; }; } = {}) => { - const controllerMessenger = new ControllerMessenger< - AllowedActions, - AllowedEvents - >(); + const controllerMessenger = getRootControllerMessenger(); const multichainBalancesControllerMessenger: MultichainBalancesControllerMessenger = controllerMessenger.getRestricted({ From 852b238c4fa12f758b428853f4b3300360c24f98 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:52:25 -0300 Subject: [PATCH 46/49] test: update unit tests --- .../MultichainBalancesController.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index e5ae0ff9d60..fc52d2c5d73 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -43,9 +43,16 @@ export type MultichainBalancesControllerState = { }; /** - * Default state of the {@link MultichainBalancesController}. + * 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 const defaultState: MultichainBalancesControllerState = { balances: {} }; +export function getDefaultMultichainBalancesControllerState(): MultichainBalancesControllerState { + return { balances: {} }; +} /** * Returns the state of the {@link MultichainBalancesController}. @@ -89,14 +96,14 @@ export type MultichainBalancesControllerEvents = /** * Actions that this controller is allowed to call. */ -export type AllowedActions = +type AllowedActions = | HandleSnapRequest | AccountsControllerListMultichainAccountsAction; /** * Events that this controller is allowed to subscribe. */ -export type AllowedEvents = +type AllowedEvents = | AccountsControllerAccountAddedEvent | AccountsControllerAccountRemovedEvent; @@ -139,17 +146,17 @@ export class MultichainBalancesController extends BaseController< constructor({ messenger, - state, + state = {}, }: { messenger: MultichainBalancesControllerMessenger; - state: MultichainBalancesControllerState; + state?: Partial; }) { super({ messenger, name: controllerName, metadata: balancesControllerMetadata, state: { - ...defaultState, + ...getDefaultMultichainBalancesControllerState(), ...state, }, }); @@ -255,27 +262,24 @@ export class MultichainBalancesController extends BaseController< * * @param accountId - The account ID. */ + async #updateBalance(accountId: string) { const account = this.#getAccount(accountId); - const partialState: MultichainBalancesControllerState = { balances: {} }; if (account.metadata.snap) { const scope = getScopeForAccount(account); const assetTypes = NETWORK_ASSETS_MAP[scope]; - partialState.balances[account.id] = await this.#getBalances( + const accountBalance = await this.#getBalances( account.id, account.metadata.snap.id, assetTypes, ); - } - this.update((state: Draft) => { - state.balances = { - ...state.balances, - ...partialState.balances, - }; - }); + this.update((state: Draft) => { + state.balances[accountId] = accountBalance; + }); + } } /** @@ -284,7 +288,7 @@ export class MultichainBalancesController extends BaseController< * * @param accountId - The account ID. */ - async updateBalance(accountId: string) { + 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); @@ -294,7 +298,7 @@ export class MultichainBalancesController extends BaseController< * Updates the balances of all supported accounts. This method doesn't return * anything, but it updates the state of the controller. */ - async updateBalances() { + async updateBalances(): Promise { await this.#tracker.updateBalances(); } @@ -317,7 +321,7 @@ export class MultichainBalancesController extends BaseController< * * @param account - The new account being added. */ - async #handleOnAccountAdded(account: InternalAccount) { + async #handleOnAccountAdded(account: InternalAccount): Promise { if (!this.#isNonEvmAccount(account)) { // Nothing to do here for EVM accounts return; @@ -338,7 +342,7 @@ export class MultichainBalancesController extends BaseController< * * @param accountId - The account ID being removed. */ - async #handleOnAccountRemoved(accountId: string) { + async #handleOnAccountRemoved(accountId: string): Promise { if (this.#tracker.isTracked(accountId)) { this.#tracker.untrack(accountId); } @@ -346,7 +350,6 @@ export class MultichainBalancesController extends BaseController< if (accountId in this.state.balances) { this.update((state: Draft) => { delete state.balances[accountId]; - return state; }); } } From d71ad47a11e677b5fdb29583e53f9cff04035882 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:50:31 -0300 Subject: [PATCH 47/49] test: improve coverage --- .../MultichainBalancesController.ts | 70 ++++++------------- .../utils.test.ts | 24 ++++++- .../src/MultichainBalancesController/utils.ts | 19 ++++- 3 files changed, 64 insertions(+), 49 deletions(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index fc52d2c5d73..9442607e564 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -19,12 +19,8 @@ import { HandlerType } from '@metamask/snaps-utils'; import type { Json, JsonRpcRequest } from '@metamask/utils'; import type { Draft } from 'immer'; -import { - BalancesTracker, - BALANCE_UPDATE_INTERVALS, - NETWORK_ASSETS_MAP, -} from '.'; -import { getScopeForAccount } from './utils'; +import { BalancesTracker, NETWORK_ASSETS_MAP } from '.'; +import { getScopeForAccount, getBlockTimeForAccount } from './utils'; const controllerName = 'MultichainBalancesController'; @@ -168,7 +164,7 @@ export class MultichainBalancesController extends BaseController< // Register all non-EVM accounts into the tracker for (const account of this.#listAccounts()) { if (this.#isNonEvmAccount(account)) { - this.#tracker.track(account.id, this.#getBlockTimeFor(account)); + this.#tracker.track(account.id, getBlockTimeForAccount(account.type)); } } @@ -197,20 +193,23 @@ export class MultichainBalancesController extends BaseController< } /** - * Gets the block time for a given account. + * Updates the balances of one account. This method doesn't return + * anything, but it updates the state of the controller. * - * @param account - The account to get the block time for. - * @returns The block time for the account. + * @param accountId - The account ID. */ - #getBlockTimeFor(account: InternalAccount): number { - if (account.type in BALANCE_UPDATE_INTERVALS) { - return BALANCE_UPDATE_INTERVALS[ - account.type as keyof typeof BALANCE_UPDATE_INTERVALS - ]; - } - throw new Error( - `Unsupported account type for balance tracking: ${account.type}`, - ); + 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(); } /** @@ -242,17 +241,14 @@ export class MultichainBalancesController extends BaseController< * @returns The non-EVM account. */ #getAccount(accountId: string): InternalAccount { - const account: InternalAccount | undefined = - this.#listMultichainAccounts().find( - (multichainAccount) => multichainAccount.id === accountId, - ); + const account: InternalAccount | undefined = this.#listAccounts().find( + (multichainAccount) => multichainAccount.id === accountId, + ); if (!account) { throw new Error(`Unknown account: ${accountId}`); } - if (!this.#isNonEvmAccount(account)) { - throw new Error(`Account is not a non-EVM account: ${accountId}`); - } + return account; } @@ -282,26 +278,6 @@ export class MultichainBalancesController extends BaseController< } } - /** - * 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(); - } - /** * Checks for non-EVM accounts. * @@ -327,7 +303,7 @@ export class MultichainBalancesController extends BaseController< return; } - this.#tracker.track(account.id, this.#getBlockTimeFor(account)); + 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. diff --git a/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts index 3e65f473a05..c566ad83741 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts @@ -8,11 +8,12 @@ import { KeyringTypes } from '@metamask/keyring-controller'; import { validate, Network } from 'bitcoin-address-validation'; import { v4 as uuidv4 } from 'uuid'; -import { MultichainNetworks } from './constants'; +import { MultichainNetworks, BALANCE_UPDATE_INTERVALS } from '.'; import { getScopeForBtcAddress, getScopeForSolAddress, getScopeForAccount, + getBlockTimeForAccount, } from './utils'; const mockBtcAccount = { @@ -169,3 +170,24 @@ describe('getScopeForAddress', () => { ); }); }); + +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 index 72728b2299a..205cca8fc33 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/utils.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.ts @@ -2,7 +2,7 @@ import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import { validate, Network } from 'bitcoin-address-validation'; -import { MultichainNetworks } from './constants'; +import { MultichainNetworks, BALANCE_UPDATE_INTERVALS } from './constants'; /** * Gets the scope for a specific and supported Bitcoin account. @@ -58,3 +58,20 @@ export const getScopeForAccount = (account: InternalAccount): string => { 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}`, + ); +}; From fca2211c3c40d4736cdce78ea772150eb52f5f65 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:27:41 -0300 Subject: [PATCH 48/49] chore: bump @metamask/keyring-api --- packages/assets-controllers/package.json | 2 +- yarn.lock | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index ff1aecce808..3053fc94017 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -81,7 +81,7 @@ "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", - "@metamask/keyring-api": "^12.0.0", + "@metamask/keyring-api": "^13.0.0", "@metamask/keyring-controller": "^19.0.2", "@metamask/keyring-internal-api": "^1.1.0", "@metamask/keyring-snap-client": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index c753ad48972..9ffa56c0f6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2383,7 +2383,7 @@ __metadata: "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-provider-http": "npm:^0.3.0" - "@metamask/keyring-api": "npm:^12.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-snap-client": "npm:^1.0.0" @@ -3193,18 +3193,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^12.0.0": - version: 12.0.0 - resolution: "@metamask/keyring-api@npm:12.0.0" - dependencies: - "@metamask/keyring-utils": "npm:^1.0.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.3.0" - bech32: "npm:^2.0.0" - checksum: 10/ba8b75c55d3fcb9f8b52c58ff141cba81f7c416c3fa684e089965717ea129d50e8df7a73e7ab1c96eaf59d70b6e2dd8a618434939b75ef0d3402b547b5196877 - languageName: node - linkType: hard - "@metamask/keyring-api@npm:^13.0.0": version: 13.0.0 resolution: "@metamask/keyring-api@npm:13.0.0" From cfe20becd4ec886a36ccf41f9a71ad4990e0ca8c Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:09:55 -0300 Subject: [PATCH 49/49] test: update mocks to have account scopes --- .../MultichainBalancesController.test.ts | 5 +++++ .../src/MultichainBalancesController/utils.test.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index 68e72b2eb62..87f200ab550 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -5,6 +5,9 @@ import { BtcMethod, EthAccountType, EthMethod, + BtcScopes, + EthScopes, + SolScopes, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; @@ -40,6 +43,7 @@ const mockBtcAccount = { }, lastSelected: 0, }, + scopes: [BtcScopes.Namespace], options: {}, methods: [BtcMethod.SendBitcoin], type: BtcAccountType.P2wpkh, @@ -61,6 +65,7 @@ const mockEthAccount = { }, lastSelected: 0, }, + scopes: [EthScopes.Namespace], options: {}, methods: [EthMethod.SignTypedDataV4, EthMethod.SignTransaction], type: EthAccountType.Eoa, diff --git a/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts index c566ad83741..099ccf23c80 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/utils.test.ts @@ -3,6 +3,8 @@ import { SolAccountType, BtcMethod, SolMethod, + BtcScopes, + SolScopes, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import { validate, Network } from 'bitcoin-address-validation'; @@ -32,6 +34,7 @@ const mockBtcAccount = { }, lastSelected: 0, }, + scopes: [BtcScopes.Namespace], options: {}, methods: [BtcMethod.SendBitcoin], type: BtcAccountType.P2wpkh, @@ -56,6 +59,7 @@ const mockSolAccount = { options: { scope: 'solana-scope', }, + scopes: [SolScopes.Namespace], methods: [SolMethod.SendAndConfirmTransaction], type: SolAccountType.DataAccount, };