From e7cfce4dfc75698867a1673becb6432cedb66fa9 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Thu, 9 Jan 2025 16:01:36 +0000 Subject: [PATCH 01/16] chore: created package and added controller --- .github/CODEOWNERS | 1 + README.md | 6 + .../CHANGELOG.md | 10 + .../LICENSE | 20 + .../README.md | 15 + .../jest.config.js | 26 + .../package.json | 78 +++ .../MultichainTransactionsController.test.ts | 344 ++++++++++++ .../src/MultichainTransactionsController.ts | 499 ++++++++++++++++++ .../src/constants.ts | 45 ++ .../src/index.ts | 12 + .../tsconfig.build.json | 15 + .../tsconfig.json | 13 + .../typedoc.json | 7 + tsconfig.build.json | 7 +- tsconfig.json | 5 +- yarn.lock | 23 + 17 files changed, 1122 insertions(+), 4 deletions(-) create mode 100644 packages/multichain-transactions-controller/CHANGELOG.md create mode 100644 packages/multichain-transactions-controller/LICENSE create mode 100644 packages/multichain-transactions-controller/README.md create mode 100644 packages/multichain-transactions-controller/jest.config.js create mode 100644 packages/multichain-transactions-controller/package.json create mode 100644 packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts create mode 100644 packages/multichain-transactions-controller/src/MultichainTransactionsController.ts create mode 100644 packages/multichain-transactions-controller/src/constants.ts create mode 100644 packages/multichain-transactions-controller/src/index.ts create mode 100644 packages/multichain-transactions-controller/tsconfig.build.json create mode 100644 packages/multichain-transactions-controller/tsconfig.json create mode 100644 packages/multichain-transactions-controller/typedoc.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f9e06177f48..c2025590372 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,6 +59,7 @@ /packages/permission-log-controller @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers /packages/selected-network-controller @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers @MetaMask/metamask-assets /packages/profile-sync-controller @MetaMask/notifications @MetaMask/identity +/packages/multichain-transactions-controller @MetaMask/solana-wallet-snap-team @MetaMask/cet-btc-team ## Package Release related /packages/accounts-controller/package.json @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers diff --git a/README.md b/README.md index 9dff752fea5..a277d388b44 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/logging-controller`](packages/logging-controller) - [`@metamask/message-manager`](packages/message-manager) - [`@metamask/multichain`](packages/multichain) +- [`@metamask/multichain-transactions-controller`](packages/multichain-transactions-controller) - [`@metamask/name-controller`](packages/name-controller) - [`@metamask/network-controller`](packages/network-controller) - [`@metamask/notification-services-controller`](packages/notification-services-controller) @@ -50,6 +51,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/profile-sync-controller`](packages/profile-sync-controller) - [`@metamask/queued-request-controller`](packages/queued-request-controller) - [`@metamask/rate-limit-controller`](packages/rate-limit-controller) +- [`@metamask/remote-feature-flag-controller`](packages/remote-feature-flag-controller) - [`@metamask/selected-network-controller`](packages/selected-network-controller) - [`@metamask/signature-controller`](packages/signature-controller) - [`@metamask/transaction-controller`](packages/transaction-controller) @@ -82,6 +84,7 @@ linkStyle default opacity:0.5 logging_controller(["@metamask/logging-controller"]); message_manager(["@metamask/message-manager"]); multichain(["@metamask/multichain"]); + multichain_transactions_controller(["@metamask/multichain-transactions-controller"]); name_controller(["@metamask/name-controller"]); network_controller(["@metamask/network-controller"]); notification_services_controller(["@metamask/notification-services-controller"]); @@ -93,6 +96,7 @@ linkStyle default opacity:0.5 profile_sync_controller(["@metamask/profile-sync-controller"]); queued_request_controller(["@metamask/queued-request-controller"]); rate_limit_controller(["@metamask/rate-limit-controller"]); + remote_feature_flag_controller(["@metamask/remote-feature-flag-controller"]); selected_network_controller(["@metamask/selected-network-controller"]); signature_controller(["@metamask/signature-controller"]); transaction_controller(["@metamask/transaction-controller"]); @@ -167,6 +171,8 @@ linkStyle default opacity:0.5 queued_request_controller --> network_controller; queued_request_controller --> selected_network_controller; rate_limit_controller --> base_controller; + remote_feature_flag_controller --> base_controller; + remote_feature_flag_controller --> controller_utils; selected_network_controller --> base_controller; selected_network_controller --> json_rpc_engine; selected_network_controller --> network_controller; diff --git a/packages/multichain-transactions-controller/CHANGELOG.md b/packages/multichain-transactions-controller/CHANGELOG.md new file mode 100644 index 00000000000..b518709c7b8 --- /dev/null +++ b/packages/multichain-transactions-controller/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/multichain-transactions-controller/LICENSE b/packages/multichain-transactions-controller/LICENSE new file mode 100644 index 00000000000..7d002dced3a --- /dev/null +++ b/packages/multichain-transactions-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/multichain-transactions-controller/README.md b/packages/multichain-transactions-controller/README.md new file mode 100644 index 00000000000..5ae3333ab00 --- /dev/null +++ b/packages/multichain-transactions-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/multichain-transactions-controller` + +This package is responsible for getting transactions from our Bitcoin and Solana snaps. + +## Installation + +`yarn add @metamask/multichain-transactions-controller` + +or + +`npm install @metamask/multichain-transactions-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/multichain-transactions-controller/jest.config.js b/packages/multichain-transactions-controller/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/multichain-transactions-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/multichain-transactions-controller/package.json b/packages/multichain-transactions-controller/package.json new file mode 100644 index 00000000000..65e2faa9441 --- /dev/null +++ b/packages/multichain-transactions-controller/package.json @@ -0,0 +1,78 @@ +{ + "name": "@metamask/multichain-transactions-controller", + "version": "0.0.0", + "description": "This package is responsible for getting transactions from our Bitcoin and Solana snaps.", + "keywords": [ + "MetaMask", + "Bitcoin", + "Solana" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/multichain-transactions-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/multichain-transactions-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/multichain-transactions-controller", + "publish:preview": "yarn npm publish --tag preview", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^7.1.0", + "@metamask/keyring-api": "^12.0.0", + "@metamask/polling-controller": "^12.0.2" + }, + "devDependencies": { + "@metamask/accounts-controller": "^20.0.2", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/keyring-controller": "^19.0.2", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2" + }, + "peerDependencies": { + "@metamask/accounts-controller": "^20.0.0", + "@metamask/keyring-controller": "^19.0.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts new file mode 100644 index 00000000000..ff1e7e0319b --- /dev/null +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts @@ -0,0 +1,344 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import type { Transaction, CaipAssetType } from '@metamask/keyring-api'; +import { type InternalAccount } from '@metamask/keyring-internal-api'; +import { + BtcAccountType, + BtcMethod, + EthAccountType, + EthMethod, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { v4 as uuidv4 } from 'uuid'; +import { + MultichainTransactionsController, + AllowedActions, + AllowedEvents, + MultichainTransactionsControllerState, + defaultState, + MultichainTransactionsControllerMessenger, +} from './MultichainTransactionsController'; + +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 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 mockTransactionResult = { + data: [ + { + id: '123', + account: mockBtcAccount.id, + chain: 'bip122:000000000019d6689c085ae165831e93', + type: 'send', + status: 'confirmed', + timestamp: Date.now(), + from: [], + to: [], + fees: [], + events: [ + { + status: 'confirmed', + timestamp: Date.now(), + }, + ], + }, + ], + next: null, +}; + +const setupController = ({ + state = defaultState, + mocks, +}: { + state?: MultichainTransactionsControllerState; + mocks?: { + listMultichainAccounts?: InternalAccount[]; + handleRequestReturnValue?: Record; + }; +} = {}) => { + const controllerMessenger = new ControllerMessenger< + AllowedActions, + AllowedEvents + >(); + + const multichainTransactionsControllerMessenger: MultichainTransactionsControllerMessenger = + controllerMessenger.getRestricted({ + name: 'MultichainTransactionsController', + allowedActions: [ + 'SnapController:handleRequest', + 'AccountsController:listMultichainAccounts', + ], + allowedEvents: [ + 'AccountsController:accountAdded', + 'AccountsController:accountRemoved', + ], + }); + + const mockSnapHandleRequest = jest.fn(); + controllerMessenger.registerActionHandler( + 'SnapController:handleRequest', + mockSnapHandleRequest.mockReturnValue( + mocks?.handleRequestReturnValue ?? mockTransactionResult, + ), + ); + + const mockListMultichainAccounts = jest.fn(); + controllerMessenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + mockListMultichainAccounts.mockReturnValue( + mocks?.listMultichainAccounts ?? [mockBtcAccount, mockEthAccount], + ), + ); + + const controller = new MultichainTransactionsController({ + messenger: multichainTransactionsControllerMessenger, + state, + }); + + return { + controller, + messenger: controllerMessenger, + mockSnapHandleRequest, + mockListMultichainAccounts, + }; +}; + +describe('MultichainTransactionsController', () => { + it('initialize with default state', () => { + const { controller } = setupController({}); + expect(controller.state).toEqual({ nonEvmTransactions: {} }); + }); + + it('starts polling when calling start', async () => { + const { controller } = setupController(); + const startPollingSpy = jest.spyOn(controller, 'startPolling'); + + await controller.start(); + + expect(startPollingSpy).toHaveBeenCalledWith({ + accountId: '', + pagination: { limit: 10 }, + }); + }); + + it('stops polling when calling stop', async () => { + const { controller } = setupController(); + const stopAllPollingSpy = jest.spyOn(controller, 'stopAllPolling'); + + await controller.start(); + controller.stop(); + + expect(stopAllPollingSpy).toHaveBeenCalled(); + }); + + it('executes polling correctly', async () => { + const { controller } = setupController(); + const updateTransactionsSpy = jest.spyOn(controller as any, '#updateTransactions'); + + await controller['_executePoll'](); + + expect(updateTransactionsSpy).toHaveBeenCalled(); + }); + + it('update transactions when calling updateTransactions', async () => { + const { controller } = setupController(); + + await controller.updateTransactions(); + + expect(controller.state).toEqual({ + nonEvmTransactions: { + [mockBtcAccount.id]: { + transactions: mockTransactionResult.data, + next: null, + lastUpdated: expect.any(Number), + }, + }, + }); + }); + + it('update transactions 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.updateTransactions(); + + expect(controller.state).toEqual({ + nonEvmTransactions: { + [mockBtcAccount.id]: { + transactions: mockTransactionResult.data, + next: null, + lastUpdated: expect.any(Number), + }, + }, + }); + }); + + it('update transactions when "AccountsController:accountRemoved" is fired', async () => { + const { controller, messenger, mockListMultichainAccounts } = + setupController(); + + controller.start(); + await controller.updateTransactions(); + expect(controller.state).toEqual({ + nonEvmTransactions: { + [mockBtcAccount.id]: { + transactions: mockTransactionResult.data, + next: null, + lastUpdated: expect.any(Number), + }, + }, + }); + + messenger.publish('AccountsController:accountRemoved', mockBtcAccount.id); + mockListMultichainAccounts.mockReturnValue([]); + await controller.updateTransactions(); + + expect(controller.state).toEqual({ + nonEvmTransactions: {}, + }); + }); + + 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.updateTransactions(); + + expect(controller.state).toStrictEqual({ + nonEvmTransactions: {}, + }); + }); + + it('should update transactions for a specific account', async () => { + const { controller } = setupController(); + await controller.updateTransactionsForAccount(mockBtcAccount.id); + + expect(controller.state.nonEvmTransactions[mockBtcAccount.id]).toEqual({ + transactions: mockTransactionResult.data, + next: null, + lastUpdated: expect.any(Number), + }); + }); + + it('should handle pagination when fetching transactions', async () => { + const firstPage = { + data: [ + { + id: '1', + account: mockBtcAccount.id, + chain: 'bip122:000000000933ea01ad0ee984209779ba', + type: 'send' as const, + status: 'confirmed' as const, + timestamp: Date.now(), + from: [], + to: [], + fees: [], + events: [ + { + status: 'confirmed' as const, + timestamp: Date.now(), + }, + ], + }, + ], + next: 'page2', + }; + + const secondPage = { + data: [ + { + id: '2', + account: mockBtcAccount.id, + chain: 'bip122:000000000933ea01ad0ee984209779ba', + type: 'send' as const, + status: 'confirmed' as const, + timestamp: Date.now(), + from: [], + to: [], + fees: [], + events: [ + { + status: 'confirmed' as const, + timestamp: Date.now(), + }, + ], + }, + ], + next: null, + }; + + const { controller, mockSnapHandleRequest } = setupController(); + mockSnapHandleRequest + .mockReturnValueOnce(firstPage) + .mockReturnValueOnce(secondPage); + + await controller.updateTransactionsForAccount(mockBtcAccount.id); + + expect(mockSnapHandleRequest).toHaveBeenCalledWith( + expect.objectContaining({ + request: expect.objectContaining({ + method: 'keyring_listAccountTransactions', + }), + }), + ); + }); + + it('should handle errors gracefully when updating transactions', async () => { + const { controller, mockSnapHandleRequest } = setupController(); + mockSnapHandleRequest.mockRejectedValue(new Error('Failed to fetch')); + + await expect(controller.updateTransactions()).resolves.not.toThrow(); + expect(controller.state.nonEvmTransactions).toEqual({}); + }); +}); diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts new file mode 100644 index 00000000000..f7b62eac268 --- /dev/null +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts @@ -0,0 +1,499 @@ +import type { Json, JsonRpcRequest } from '@metamask/utils'; + +import { + BaseController, + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; + +import { isEvmAccountType, Transaction } from '@metamask/keyring-api'; +import { KeyringClient } from '@metamask/keyring-snap-client'; +import { type InternalAccount } from '@metamask/keyring-internal-api'; +import type { HandleSnapRequest } from '@metamask/snaps-controllers'; +import type { SnapId } from '@metamask/snaps-sdk'; +import { HandlerType } from '@metamask/snaps-utils'; +import type { Draft } from 'immer'; +import type { + AccountsControllerAccountAddedEvent, + AccountsControllerAccountRemovedEvent, + AccountsControllerListMultichainAccountsAction, +} from '@metamask/accounts-controller'; +import { + MultichainNetworks, + TRANSACTIONS_CHECK_INTERVALS, +} from './constants'; + +const controllerName = 'MultichainTransactionsController'; + +export type PaginationOptions = { + limit: number; + next?: string | null; +}; + +/** + * State used by the {@link MultichainTransactionsController} to cache account transactions. + */ +export type MultichainTransactionsControllerState = { + nonEvmTransactions: { + [accountId: string]: TransactionStateEntry; + }; +}; + +/** + * Default state of the {@link MultichainTransactionsController}. + */ +export const defaultState: MultichainTransactionsControllerState = { + nonEvmTransactions: {}, +}; + +/** + * Returns the state of the {@link MultichainTransactionsController}. + */ +export type MultichainTransactionsControllerGetStateAction = + ControllerGetStateAction< + typeof controllerName, + MultichainTransactionsControllerState + >; + +/** + * Updates the transactions of all supported accounts. + */ +export type MultichainTransactionsControllerListTransactionsAction = { + type: `${typeof controllerName}:updateTransactions`; + handler: MultichainTransactionsController['updateTransactions']; +}; + +/** + * Event emitted when the state of the {@link MultichainTransactionsController} changes. + */ +export type MultichainTransactionsControllerStateChange = + ControllerStateChangeEvent< + typeof controllerName, + MultichainTransactionsControllerState + >; + +/** + * Actions exposed by the {@link MultichainTransactionsController}. + */ +export type MultichainTransactionsControllerActions = + | MultichainTransactionsControllerGetStateAction + | MultichainTransactionsControllerListTransactionsAction; + +/** + * Events emitted by {@link MultichainTransactionsController}. + */ +export type MultichainTransactionsControllerEvents = + MultichainTransactionsControllerStateChange; + +/** + * Messenger type for the MultichainTransactionsController. + */ +export type MultichainTransactionsControllerMessenger = + RestrictedControllerMessenger< + typeof controllerName, + MultichainTransactionsControllerActions | AllowedActions, + MultichainTransactionsControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] + >; + +/** + * 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; + +/** + * {@link MultichainTransactionsController}'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 MultichainTransactionsControllerMetadata = { + nonEvmTransactions: { + persist: true, + anonymous: false, + }, +}; + +/** + * The state of transactions for a specific account. + */ +export type TransactionStateEntry = { + transactions: Transaction[]; + next: string | null; + lastUpdated: number; +}; + +/** The input to start polling for the {@link MultichainTransactionsController} */ +type MultichainTransactionsPollingInput = { + accountId: string; + pagination: PaginationOptions; +}; + +/** + * This type is used to track the state of transaction fetching for each account. + * It's not about the transactions themselves, but rather about managing when and how we fetch them. + */ +type TransactionInfo = { + lastUpdated: number; + blockTime: number; + pagination: PaginationOptions; +}; + +/** + * The MultichainTransactionsController is responsible for fetching and caching account + * transactions for non-EVM accounts. + */ +export class MultichainTransactionsController extends StaticIntervalPollingController()< + typeof controllerName, + MultichainTransactionsControllerState, + MultichainTransactionsControllerMessenger +> { + #transactions: Record = {}; + + #handle?: ReturnType; + + constructor({ + messenger, + state, + interval = 5000, + }: { + messenger: MultichainTransactionsControllerMessenger; + state: MultichainTransactionsControllerState; + interval?: number; + }) { + super({ + messenger, + name: controllerName, + metadata: MultichainTransactionsControllerMetadata, + state: { + ...defaultState, + ...state, + }, + }); + + this.setIntervalLength(interval); + + // Register all non-EVM accounts + for (const account of this.#listAccounts()) { + if (this.#isNonEvmAccount(account)) { + this.track( + account.id, + this.#getBlockTimeForAccount(account), + ); + } + } + + this.messagingSystem.subscribe( + 'AccountsController:accountAdded', + (account: InternalAccount) => this.#handleOnAccountAdded(account), + ); + this.messagingSystem.subscribe( + 'AccountsController:accountRemoved', + (accountId: string) => this.#handleOnAccountRemoved(accountId), + ); + } + + /** + * Checks if an account ID is being tracked. + * + * @param accountId - The account ID. + * @returns True if the account is being tracked, false otherwise. + */ + isTracked(accountId: string) { + return accountId in this.#transactions; + } + + /** + * 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}`); + } + } + + /** + * Implementation of polling execution + */ + async _executePoll(): Promise { + try { + await Promise.allSettled( + Object.keys(this.#transactions).map(async (accountId) => { + this.assertBeingTracked(accountId); + + const info = this.#transactions[accountId]; + const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; + const hasNoTransactionsYet = info.lastUpdated === 0; + + if (hasNoTransactionsYet || isOutdated) { + await this.#updateTransactions(accountId, info.pagination); + this.#transactions[accountId].lastUpdated = Date.now(); + } + }), + ); + } catch (error) { + console.error('Error during transaction polling:', error); + } + } + + /** + * 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 transactions). + * @param pagination - Options for paginating transaction results. Defaults to { limit: 10 }. + */ + track( + accountId: string, + blockTime: number, + pagination: PaginationOptions = { limit: 10 }, + ) { + if (blockTime <= 0) { + throw new Error('Block time must be positive'); + } + + if (!this.isTracked(accountId)) { + this.#transactions[accountId] = { + lastUpdated: 0, + blockTime, + pagination, + }; + } + } + + /** + * Stop tracking an account + */ + untrack(accountId: string) { + this.assertBeingTracked(accountId); + delete this.#transactions[accountId]; + } + + /** + * 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 transactions for. + * + * @returns A list of accounts that we should get transactions for. + */ + #listAccounts(): InternalAccount[] { + const accounts = this.#listMultichainAccounts(); + return accounts.filter((account) => this.#isNonEvmAccount(account)); + } + + /** + * Updates the transactions for one account. + * + * @param accountId - The ID of the account to update transactions for. + * @param pagination - Options for paginating transaction results. + */ + async #updateTransactions(accountId: string, pagination: PaginationOptions) { + const account = this.#listAccounts().find( + (accountItem) => accountItem.id === accountId, + ); + + if (account?.metadata.snap) { + const response = await this.#getTransactions( + account.id, + account.metadata.snap.id, + pagination, + ); + + /** + * Filter only Solana transactions to ensure they're mainnet + * All other chain transactions are included as-is + */ + const transactions = response.data.filter((tx) => { + if (tx.chain.startsWith(MultichainNetworks.Solana)) { + return tx.chain === MultichainNetworks.Solana; + } + return true; + }); + + this.update((state: Draft) => { + const entry: TransactionStateEntry = { + transactions, + next: response.next, + lastUpdated: Date.now(), + }; + + Object.assign(state.nonEvmTransactions, { [account.id]: entry }); + }); + } + } + + /** + * Gets transactions for an account. + * + * @param accountId - The ID of the account to get transactions for. + * @param snapId - The ID of the snap that manages the account. + * @param pagination - Options for paginating transaction results. + * @returns A promise that resolves to the transaction data and pagination info. + */ + async #getTransactions( + accountId: string, + snapId: string, + pagination: PaginationOptions, + ): Promise<{ + data: Transaction[]; + next: string | null; + }> { + return await this.#getClient(snapId).listAccountTransactions( + accountId, + pagination, + ); + } + + /** + * Update the transactions for a tracked account ID. + * + * @param accountId - The account ID. + * @throws If the account ID is not being tracked. + */ + async updateTransactionsForAccount(accountId: string) { + this.assertBeingTracked(accountId); + + const info = this.#transactions[accountId]; + const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; + const hasNoTransactionsYet = info.lastUpdated === 0; + + if (hasNoTransactionsYet || isOutdated) { + await this.#updateTransactions(accountId, info.pagination); + this.#transactions[accountId].lastUpdated = Date.now(); + } + } + + /** + * Update the transactions of all tracked accounts + */ + async updateTransactions() { + await Promise.allSettled( + Object.keys(this.#transactions).map(async (accountId) => { + await this.updateTransactionsForAccount(accountId); + }), + ); + } + + /** + * Starts the polling process. + */ + + async start(): Promise { + await this.startPolling({ + accountId: '', // Empty string as we're not polling individual accounts + pagination: { limit: 10 }, // Default pagination + }); + } + + /** + * Stops the polling process. + */ + stop(): void { + this.stopAllPolling(); + } + + /** + * Gets the block time for a given account. + * + * @param account - The account to get the block time for. + * @returns The block time for the account. + */ + #getBlockTimeForAccount(account: InternalAccount): number { + if (account.type in TRANSACTIONS_CHECK_INTERVALS) { + return TRANSACTIONS_CHECK_INTERVALS[ + account.type as keyof typeof TRANSACTIONS_CHECK_INTERVALS + ]; + } + throw new Error( + `Unsupported account type for transactions tracking: ${account.type}`, + ); + } + + /** + * 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)) { + return; + } + + this.track(account.id, this.#getBlockTimeForAccount(account)); + } + + /** + * Handles changes when a new account has been removed. + * + * @param accountId - The account ID being removed. + */ + async #handleOnAccountRemoved(accountId: string) { + if (this.isTracked(accountId)) { + this.untrack(accountId); + } + + if (accountId in this.state.nonEvmTransactions) { + this.update((state: Draft) => { + delete state.nonEvmTransactions[accountId]; + }); + } + } + + /** + * 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/multichain-transactions-controller/src/constants.ts b/packages/multichain-transactions-controller/src/constants.ts new file mode 100644 index 00000000000..85d64d10cd2 --- /dev/null +++ b/packages/multichain-transactions-controller/src/constants.ts @@ -0,0 +1,45 @@ +import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; + +/** + * The network identifiers for supported networks in CAIP-2 format. + * Note: This is a temporary workaround until we have a more robust + * solution for network identifiers. + */ +export enum MultichainNetworks { + Bitcoin = 'bip122:000000000019d6689c085ae165831e93', + BitcoinTestnet = 'bip122:000000000933ea01ad0ee984209779ba', + Solana = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + SolanaDevnet = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', + SolanaTestnet = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z', +} + +export enum MultichainNativeAssets { + Bitcoin = `${MultichainNetworks.Bitcoin}/slip44:0`, + BitcoinTestnet = `${MultichainNetworks.BitcoinTestnet}/slip44:0`, + Solana = `${MultichainNetworks.Solana}/slip44:501`, + SolanaDevnet = `${MultichainNetworks.SolanaDevnet}/slip44:501`, + SolanaTestnet = `${MultichainNetworks.SolanaTestnet}/slip44:501`, +} + +const BITCOIN_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds +const SOLANA_TRANSACTIONS_UPDATE_TIME = 7000; // 7 seconds +const BTC_TRANSACTIONS_UPDATE_TIME = BITCOIN_AVG_BLOCK_TIME / 2; + +export const TRANSACTIONS_CHECK_INTERVALS = { + // NOTE: We set an interval of half the average block time for bitcoin + // to mitigate when our interval is de-synchronized with the actual block time. + [BtcAccountType.P2wpkh]: BTC_TRANSACTIONS_UPDATE_TIME, + [SolAccountType.DataAccount]: SOLANA_TRANSACTIONS_UPDATE_TIME, +}; + +/** + * Maps network identifiers to their corresponding native asset types. + * Each network is mapped to an array containing its native asset for consistency. + */ +export const NETWORK_ASSETS_MAP: Record = { + [MultichainNetworks.Solana]: [MultichainNativeAssets.Solana], + [MultichainNetworks.SolanaTestnet]: [MultichainNativeAssets.SolanaTestnet], + [MultichainNetworks.SolanaDevnet]: [MultichainNativeAssets.SolanaDevnet], + [MultichainNetworks.Bitcoin]: [MultichainNativeAssets.Bitcoin], + [MultichainNetworks.BitcoinTestnet]: [MultichainNativeAssets.BitcoinTestnet], +}; \ No newline at end of file diff --git a/packages/multichain-transactions-controller/src/index.ts b/packages/multichain-transactions-controller/src/index.ts new file mode 100644 index 00000000000..cb8c0cbd6d0 --- /dev/null +++ b/packages/multichain-transactions-controller/src/index.ts @@ -0,0 +1,12 @@ +export { MultichainTransactionsController } from './MultichainTransactionsController'; +export type { + MultichainTransactionsControllerState, + PaginationOptions, + TransactionStateEntry, +} from './MultichainTransactionsController'; +export { + TRANSACTIONS_CHECK_INTERVALS, + NETWORK_ASSETS_MAP, + MultichainNetworks, + MultichainNativeAssets, +} from './constants'; \ No newline at end of file diff --git a/packages/multichain-transactions-controller/tsconfig.build.json b/packages/multichain-transactions-controller/tsconfig.build.json new file mode 100644 index 00000000000..048cb0e3bef --- /dev/null +++ b/packages/multichain-transactions-controller/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../accounts-controller/tsconfig.build.json" }, + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" }, + { "path": "../polling-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain-transactions-controller/tsconfig.json b/packages/multichain-transactions-controller/tsconfig.json new file mode 100644 index 00000000000..e0331deb7e0 --- /dev/null +++ b/packages/multichain-transactions-controller/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../accounts-controller" }, + { "path": "../base-controller" }, + { "path": "../keyring-controller" }, + { "path": "../polling-controller" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain-transactions-controller/typedoc.json b/packages/multichain-transactions-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/multichain-transactions-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 518e786576e..aecae79bff4 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -18,6 +18,9 @@ { "path": "./packages/keyring-controller/tsconfig.build.json" }, { "path": "./packages/logging-controller/tsconfig.build.json" }, { "path": "./packages/message-manager/tsconfig.build.json" }, + { + "path": "./packages/multichain-transactions-controller/tsconfig.build.json" + }, { "path": "./packages/multichain/tsconfig.build.json" }, { "path": "./packages/name-controller/tsconfig.build.json" }, { "path": "./packages/network-controller/tsconfig.build.json" }, @@ -27,6 +30,7 @@ { "path": "./packages/permission-controller/tsconfig.build.json" }, { "path": "./packages/permission-log-controller/tsconfig.build.json" }, { "path": "./packages/phishing-controller/tsconfig.build.json" }, + { "path": "./packages/polling-controller/tsconfig.build.json" }, { "path": "./packages/preferences-controller/tsconfig.build.json" }, { "path": "./packages/profile-sync-controller/tsconfig.build.json" }, { "path": "./packages/queued-request-controller/tsconfig.build.json" }, @@ -35,8 +39,7 @@ { "path": "./packages/selected-network-controller/tsconfig.build.json" }, { "path": "./packages/signature-controller/tsconfig.build.json" }, { "path": "./packages/transaction-controller/tsconfig.build.json" }, - { "path": "./packages/user-operation-controller/tsconfig.build.json" }, - { "path": "./packages/polling-controller/tsconfig.build.json" } + { "path": "./packages/user-operation-controller/tsconfig.build.json" } ], "files": [], "include": [] diff --git a/tsconfig.json b/tsconfig.json index c1b71cc0afc..15074dfa7a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,12 +24,14 @@ { "path": "./packages/keyring-controller" }, { "path": "./packages/message-manager" }, { "path": "./packages/multichain" }, + { "path": "./packages/multichain-transactions-controller" }, { "path": "./packages/name-controller" }, { "path": "./packages/network-controller" }, { "path": "./packages/notification-services-controller" }, { "path": "./packages/permission-controller" }, { "path": "./packages/permission-log-controller" }, { "path": "./packages/phishing-controller" }, + { "path": "./packages/polling-controller" }, { "path": "./packages/preferences-controller" }, { "path": "./packages/profile-sync-controller" }, { "path": "./packages/queued-request-controller" }, @@ -38,8 +40,7 @@ { "path": "./packages/selected-network-controller" }, { "path": "./packages/signature-controller" }, { "path": "./packages/transaction-controller" }, - { "path": "./packages/user-operation-controller" }, - { "path": "./packages/polling-controller" } + { "path": "./packages/user-operation-controller" } ], "files": [], "include": ["./types"] diff --git a/yarn.lock b/yarn.lock index 7b7447656d5..8deb1bf6e7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3265,6 +3265,29 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller": + version: 0.0.0-use.local + resolution: "@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller" + dependencies: + "@metamask/accounts-controller": "npm:^20.0.2" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^7.1.0" + "@metamask/keyring-api": "npm:^12.0.0" + "@metamask/keyring-controller": "npm:^19.0.2" + "@metamask/polling-controller": "npm:^12.0.2" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + jest: "npm:^27.5.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + peerDependencies: + "@metamask/accounts-controller": ^20.0.0 + "@metamask/keyring-controller": ^19.0.0 + languageName: unknown + linkType: soft + "@metamask/multichain@workspace:packages/multichain": version: 0.0.0-use.local resolution: "@metamask/multichain@workspace:packages/multichain" From a0a03ce8d599771ff40d63d2ffb8385c3aca040c Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 13 Jan 2025 10:18:41 +0000 Subject: [PATCH 02/16] chore: adding dependencis --- .../package.json | 9 +- .../MultichainTransactionsController.test.ts | 37 ++-- .../src/MultichainTransactionsController.ts | 166 +++--------------- .../src/MultichainTransactionsTracker.test.ts | 135 ++++++++++++++ .../src/MultichainTransactionsTracker.ts | 134 ++++++++++++++ .../src/Poller.ts | 28 +++ yarn.lock | 31 ++++ 7 files changed, 374 insertions(+), 166 deletions(-) create mode 100644 packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts create mode 100644 packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts create mode 100644 packages/multichain-transactions-controller/src/Poller.ts diff --git a/packages/multichain-transactions-controller/package.json b/packages/multichain-transactions-controller/package.json index 65e2faa9441..831200d4e43 100644 --- a/packages/multichain-transactions-controller/package.json +++ b/packages/multichain-transactions-controller/package.json @@ -50,12 +50,19 @@ "dependencies": { "@metamask/base-controller": "^7.1.0", "@metamask/keyring-api": "^12.0.0", - "@metamask/polling-controller": "^12.0.2" + "@metamask/polling-controller": "^12.0.2", + "@metamask/snaps-utils": "^8.3.0", + "@metamask/utils": "^11.0.1", + "immer": "^9.0.6" }, "devDependencies": { "@metamask/accounts-controller": "^20.0.2", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.0.2", + "@metamask/keyring-internal-api": "^1.1.0", + "@metamask/keyring-snap-client": "^1.0.0", + "@metamask/snaps-controllers": "^9.10.0", + "@metamask/snaps-sdk": "^6.7.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts index ff1e7e0319b..8ae4e73a054 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts @@ -17,6 +17,7 @@ import { defaultState, MultichainTransactionsControllerMessenger, } from './MultichainTransactionsController'; +import { MultichainTransactionsTracker } from './MultichainTransactionsTracker'; const mockBtcAccount = { address: 'bc1qssdcp5kvwh6nghzg9tuk99xsflwkdv4hgvq58q', @@ -146,35 +147,25 @@ describe('MultichainTransactionsController', () => { expect(controller.state).toEqual({ nonEvmTransactions: {} }); }); - it('starts polling when calling start', async () => { + it('starts tracking when calling start', async () => { + const spyTracker = jest.spyOn( + MultichainTransactionsTracker.prototype, + 'start', + ); const { controller } = setupController(); - const startPollingSpy = jest.spyOn(controller, 'startPolling'); - await controller.start(); - - expect(startPollingSpy).toHaveBeenCalledWith({ - accountId: '', - pagination: { limit: 10 }, - }); + expect(spyTracker).toHaveBeenCalledTimes(1); }); - it('stops polling when calling stop', async () => { + it('stops tracking when calling stop', async () => { + const spyTracker = jest.spyOn( + MultichainTransactionsTracker.prototype, + 'stop', + ); const { controller } = setupController(); - const stopAllPollingSpy = jest.spyOn(controller, 'stopAllPolling'); - await controller.start(); - controller.stop(); - - expect(stopAllPollingSpy).toHaveBeenCalled(); - }); - - it('executes polling correctly', async () => { - const { controller } = setupController(); - const updateTransactionsSpy = jest.spyOn(controller as any, '#updateTransactions'); - - await controller['_executePoll'](); - - expect(updateTransactionsSpy).toHaveBeenCalled(); + await controller.stop(); + expect(spyTracker).toHaveBeenCalledTimes(1); }); it('update transactions when calling updateTransactions', async () => { diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts index f7b62eac268..c529fb2cd41 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts @@ -6,11 +6,9 @@ import { type ControllerStateChangeEvent, type RestrictedControllerMessenger, } from '@metamask/base-controller'; -import { StaticIntervalPollingController } from '@metamask/polling-controller'; - import { isEvmAccountType, Transaction } from '@metamask/keyring-api'; import { KeyringClient } from '@metamask/keyring-snap-client'; -import { type InternalAccount } from '@metamask/keyring-internal-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; @@ -24,6 +22,7 @@ import { MultichainNetworks, TRANSACTIONS_CHECK_INTERVALS, } from './constants'; +import { MultichainTransactionsTracker } from './MultichainTransactionsTracker'; const controllerName = 'MultichainTransactionsController'; @@ -136,43 +135,23 @@ export type TransactionStateEntry = { lastUpdated: number; }; -/** The input to start polling for the {@link MultichainTransactionsController} */ -type MultichainTransactionsPollingInput = { - accountId: string; - pagination: PaginationOptions; -}; - -/** - * This type is used to track the state of transaction fetching for each account. - * It's not about the transactions themselves, but rather about managing when and how we fetch them. - */ -type TransactionInfo = { - lastUpdated: number; - blockTime: number; - pagination: PaginationOptions; -}; - /** * The MultichainTransactionsController is responsible for fetching and caching account * transactions for non-EVM accounts. */ -export class MultichainTransactionsController extends StaticIntervalPollingController()< +export class MultichainTransactionsController extends BaseController< typeof controllerName, MultichainTransactionsControllerState, MultichainTransactionsControllerMessenger > { - #transactions: Record = {}; - - #handle?: ReturnType; + #tracker: MultichainTransactionsTracker; constructor({ messenger, state, - interval = 5000, }: { messenger: MultichainTransactionsControllerMessenger; state: MultichainTransactionsControllerState; - interval?: number; }) { super({ messenger, @@ -184,21 +163,21 @@ export class MultichainTransactionsController extends StaticIntervalPollingContr }, }); - this.setIntervalLength(interval); + this.#tracker = new MultichainTransactionsTracker( + async (accountId: string, pagination: PaginationOptions) => + await this.#updateTransactions(accountId, pagination), + ); - // Register all non-EVM accounts + // Register all non-EVM accounts into the tracker for (const account of this.#listAccounts()) { if (this.#isNonEvmAccount(account)) { - this.track( - account.id, - this.#getBlockTimeForAccount(account), - ); + this.#tracker.track(account.id, this.#getBlockTimeFor(account)); } } this.messagingSystem.subscribe( 'AccountsController:accountAdded', - (account: InternalAccount) => this.#handleOnAccountAdded(account), + (account) => this.#handleOnAccountAdded(account), ); this.messagingSystem.subscribe( 'AccountsController:accountRemoved', @@ -206,86 +185,6 @@ export class MultichainTransactionsController extends StaticIntervalPollingContr ); } - /** - * Checks if an account ID is being tracked. - * - * @param accountId - The account ID. - * @returns True if the account is being tracked, false otherwise. - */ - isTracked(accountId: string) { - return accountId in this.#transactions; - } - - /** - * 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}`); - } - } - - /** - * Implementation of polling execution - */ - async _executePoll(): Promise { - try { - await Promise.allSettled( - Object.keys(this.#transactions).map(async (accountId) => { - this.assertBeingTracked(accountId); - - const info = this.#transactions[accountId]; - const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; - const hasNoTransactionsYet = info.lastUpdated === 0; - - if (hasNoTransactionsYet || isOutdated) { - await this.#updateTransactions(accountId, info.pagination); - this.#transactions[accountId].lastUpdated = Date.now(); - } - }), - ); - } catch (error) { - console.error('Error during transaction polling:', error); - } - } - - /** - * 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 transactions). - * @param pagination - Options for paginating transaction results. Defaults to { limit: 10 }. - */ - track( - accountId: string, - blockTime: number, - pagination: PaginationOptions = { limit: 10 }, - ) { - if (blockTime <= 0) { - throw new Error('Block time must be positive'); - } - - if (!this.isTracked(accountId)) { - this.#transactions[accountId] = { - lastUpdated: 0, - blockTime, - pagination, - }; - } - } - - /** - * Stop tracking an account - */ - untrack(accountId: string) { - this.assertBeingTracked(accountId); - delete this.#transactions[accountId]; - } - /** * Lists the multichain accounts coming from the `AccountsController`. * @@ -371,51 +270,34 @@ export class MultichainTransactionsController extends StaticIntervalPollingContr } /** - * Update the transactions for a tracked account ID. + * Updates transactions for a specific account * - * @param accountId - The account ID. - * @throws If the account ID is not being tracked. + * @param accountId - The ID of the account to get transactions for. */ async updateTransactionsForAccount(accountId: string) { - this.assertBeingTracked(accountId); - - const info = this.#transactions[accountId]; - const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; - const hasNoTransactionsYet = info.lastUpdated === 0; - - if (hasNoTransactionsYet || isOutdated) { - await this.#updateTransactions(accountId, info.pagination); - this.#transactions[accountId].lastUpdated = Date.now(); - } + await this.#tracker.updateTransactionsForAccount(accountId); } /** - * Update the transactions of all tracked accounts + * Updates the transactions of all supported accounts. This method doesn't return + * anything, but it updates the state of the controller. */ async updateTransactions() { - await Promise.allSettled( - Object.keys(this.#transactions).map(async (accountId) => { - await this.updateTransactionsForAccount(accountId); - }), - ); + await this.#tracker.updateTransactions(); } /** * Starts the polling process. */ - async start(): Promise { - await this.startPolling({ - accountId: '', // Empty string as we're not polling individual accounts - pagination: { limit: 10 }, // Default pagination - }); + this.#tracker.start(); } /** * Stops the polling process. */ - stop(): void { - this.stopAllPolling(); + async stop(): Promise { + this.#tracker.stop(); } /** @@ -424,7 +306,7 @@ export class MultichainTransactionsController extends StaticIntervalPollingContr * @param account - The account to get the block time for. * @returns The block time for the account. */ - #getBlockTimeForAccount(account: InternalAccount): number { + #getBlockTimeFor(account: InternalAccount): number { if (account.type in TRANSACTIONS_CHECK_INTERVALS) { return TRANSACTIONS_CHECK_INTERVALS[ account.type as keyof typeof TRANSACTIONS_CHECK_INTERVALS @@ -459,7 +341,7 @@ export class MultichainTransactionsController extends StaticIntervalPollingContr return; } - this.track(account.id, this.#getBlockTimeForAccount(account)); + this.#tracker.track(account.id, this.#getBlockTimeFor(account)); } /** @@ -468,8 +350,8 @@ export class MultichainTransactionsController extends StaticIntervalPollingContr * @param accountId - The account ID being removed. */ async #handleOnAccountRemoved(accountId: string) { - if (this.isTracked(accountId)) { - this.untrack(accountId); + if (this.#tracker.isTracked(accountId)) { + this.#tracker.untrack(accountId); } if (accountId in this.state.nonEvmTransactions) { diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts new file mode 100644 index 00000000000..688e91d7759 --- /dev/null +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts @@ -0,0 +1,135 @@ +import { SolAccountType, SolMethod } from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { v4 as uuidv4 } from 'uuid'; + +import { Poller } from './Poller'; +import { MultichainTransactionsTracker } from './MultichainTransactionsTracker'; + +const MOCK_TIMESTAMP = 1733788800; + +const mockSolanaAccount = { + address: '', + id: uuidv4(), + metadata: { + name: 'Solana Account', + importTime: Date.now(), + keyring: { + type: KeyringTypes.snap, + }, + snap: { + id: 'mock-solana-snap', + name: 'mock-solana-snap', + enabled: true, + }, + lastSelected: 0, + }, + options: {}, + methods: [SolMethod.SendAndConfirmTransaction], + type: SolAccountType.DataAccount, +}; + + + +function setupTracker() { + const mockUpdateTransactions = jest.fn(); + const tracker = new MultichainTransactionsTracker(mockUpdateTransactions); + + return { + tracker, + mockUpdateTransactions, + }; +} + +describe('MultichainTransactionsTracker', () => { + it('starts polling when calling start', async () => { + const { tracker } = setupTracker(); + const spyPoller = jest.spyOn(Poller.prototype, 'start'); + + await tracker.start(); + expect(spyPoller).toHaveBeenCalledTimes(1); + }); + + it('stops polling when calling stop', async () => { + const { tracker } = setupTracker(); + const spyPoller = jest.spyOn(Poller.prototype, 'stop'); + + await tracker.start(); + await tracker.stop(); + expect(spyPoller).toHaveBeenCalledTimes(1); + }); + + it('is not tracking if none accounts have been registered', async () => { + const { tracker, mockUpdateTransactions } = setupTracker(); + + await tracker.start(); + await tracker.updateTransactions(); + + expect(mockUpdateTransactions).not.toHaveBeenCalled(); + }); + + it('tracks account transactions', async () => { + const { tracker, mockUpdateTransactions } = setupTracker(); + + await tracker.start(); + tracker.track(mockSolanaAccount.id, 0); + await tracker.updateTransactions(); + + expect(mockUpdateTransactions).toHaveBeenCalledWith(mockSolanaAccount.id, { + limit: 10, + }); + }); + + it('untracks account transactions', async () => { + const { tracker, mockUpdateTransactions } = setupTracker(); + + await tracker.start(); + tracker.track(mockSolanaAccount.id, 0); + await tracker.updateTransactions(); + expect(mockUpdateTransactions).toHaveBeenCalledWith(mockSolanaAccount.id, { + limit: 10, + }); + + tracker.untrack(mockSolanaAccount.id); + await tracker.updateTransactions(); + expect(mockUpdateTransactions).toHaveBeenCalledTimes(1); + }); + + it('tracks account after being registered', async () => { + const { tracker } = setupTracker(); + + await tracker.start(); + tracker.track(mockSolanaAccount.id, 0); + expect(tracker.isTracked(mockSolanaAccount.id)).toBe(true); + }); + + it('does not track account if not registered', async () => { + const { tracker } = setupTracker(); + + await tracker.start(); + expect(tracker.isTracked(mockSolanaAccount.id)).toBe(false); + }); + + it('does not refresh transactions if they are considered up-to-date', async () => { + const { tracker, mockUpdateTransactions } = setupTracker(); + + const blockTime = 400; + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date(MOCK_TIMESTAMP).getTime()); + + await tracker.start(); + tracker.track(mockSolanaAccount.id, blockTime); + await tracker.updateTransactions(); + expect(mockUpdateTransactions).toHaveBeenCalledTimes(1); + + await tracker.updateTransactions(); + expect(mockUpdateTransactions).toHaveBeenCalledTimes(1); + + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date(MOCK_TIMESTAMP + blockTime).getTime()); + + await tracker.updateTransactions(); + expect(mockUpdateTransactions).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts new file mode 100644 index 00000000000..fb3d2b07f76 --- /dev/null +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts @@ -0,0 +1,134 @@ +import { Poller } from './Poller'; +import { PaginationOptions } from './MultichainTransactionsController'; + +type TransactionInfo = { + lastUpdated: number; + blockTime: number; + pagination: PaginationOptions; +}; + +// Every 5s in milliseconds. +const TRANSACTIONS_TRACKING_INTERVAL = 5 * 1000; + +export class MultichainTransactionsTracker { + #poller: Poller; + + #updateTransactions: ( + accountId: string, + pagination: PaginationOptions, + ) => Promise; + + #transactions: Record = {}; + + constructor( + updateTransactionsCallback: ( + accountId: string, + pagination: PaginationOptions, + ) => Promise, + ) { + this.#updateTransactions = updateTransactionsCallback; + + this.#poller = new Poller(() => { + this.updateTransactions(); + }, TRANSACTIONS_TRACKING_INTERVAL); + } + + /** + * Starts the tracking process. + */ + async start(): Promise { + this.#poller.start(); + } + + /** + * Stops the tracking process. + */ + async stop(): Promise { + this.#poller.stop(); + } + + /** + * Checks if an account ID is being tracked. + * + * @param accountId - The account ID. + * @returns True if the account is being tracked, false otherwise. + */ + isTracked(accountId: string) { + return accountId in this.#transactions; + } + + /** + * 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 transactions). + * @param pagination - Options for paginating transaction results. Defaults to { limit: 10 }. + */ + track( + accountId: string, + blockTime: number, + pagination: PaginationOptions = { limit: 10 }, + ) { + if (!this.isTracked(accountId)) { + this.#transactions[accountId] = { + lastUpdated: 0, + blockTime, + pagination, + }; + } + } + + /** + * 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.#transactions[accountId]; + } + + /** + * Update the transactions for a tracked account ID. + * + * @param accountId - The account ID. + * @throws If the account ID is not being tracked. + */ + async updateTransactionsForAccount(accountId: string) { + this.assertBeingTracked(accountId); + + const info = this.#transactions[accountId]; + const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; + const hasNoTransactionsYet = info.lastUpdated === 0; + + if (hasNoTransactionsYet || isOutdated) { + await this.#updateTransactions(accountId, info.pagination); + this.#transactions[accountId].lastUpdated = Date.now(); + } + } + + /** + * Update the transactions of all tracked accounts + */ + async updateTransactions() { + await Promise.allSettled( + Object.keys(this.#transactions).map(async (accountId) => { + await this.updateTransactionsForAccount(accountId); + }), + ); + } +} diff --git a/packages/multichain-transactions-controller/src/Poller.ts b/packages/multichain-transactions-controller/src/Poller.ts new file mode 100644 index 00000000000..600e2ea615d --- /dev/null +++ b/packages/multichain-transactions-controller/src/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/yarn.lock b/yarn.lock index 8deb1bf6e7c..fb65e9d95ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3125,6 +3125,18 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^13.0.0": + version: 13.0.0 + resolution: "@metamask/keyring-api@npm:13.0.0" + dependencies: + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.0.1" + bech32: "npm:^2.0.0" + checksum: 10/f7e8982112a2813790354267af8f79cbf241d7ca9d733fe5e8de1a13993203b154ac6d358a92bd4340cbd4d25f4bac48681d17a25e3dc6f2336c95c00c371686 + languageName: node + linkType: hard + "@metamask/keyring-controller@npm:^19.0.2, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" @@ -3175,6 +3187,18 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-internal-api@npm:^1.1.0": + version: 1.1.0 + resolution: "@metamask/keyring-internal-api@npm:1.1.0" + dependencies: + "@metamask/keyring-api": "npm:^13.0.0" + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.0.1" + checksum: 10/5db127cfe319c289b95d55d08b6737820f00761564e219d756a55e030b83043fb43d18bac3b63fcc69cd2e03129f831410ce960ba954d8183d976d87a6781b8c + languageName: node + linkType: hard + "@metamask/keyring-internal-snap-client@npm:^1.0.0": version: 1.0.0 resolution: "@metamask/keyring-internal-snap-client@npm:1.0.0" @@ -3274,9 +3298,16 @@ __metadata: "@metamask/base-controller": "npm:^7.1.0" "@metamask/keyring-api": "npm:^12.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" "@metamask/polling-controller": "npm:^12.0.2" + "@metamask/snaps-controllers": "npm:^9.10.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" + "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" + immer: "npm:^9.0.6" jest: "npm:^27.5.1" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" From 4d8d10926353224edf7abdde6ee2e0faf2ca29f6 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 13 Jan 2025 10:38:59 +0000 Subject: [PATCH 03/16] chore: updates --- .../src/MultichainTransactionsController.ts | 5 +---- .../src/MultichainTransactionsTracker.test.ts | 2 -- packages/multichain-transactions-controller/src/constants.ts | 2 +- packages/multichain-transactions-controller/src/index.ts | 2 +- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts index c529fb2cd41..800a426178d 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts @@ -18,10 +18,7 @@ import type { AccountsControllerAccountRemovedEvent, AccountsControllerListMultichainAccountsAction, } from '@metamask/accounts-controller'; -import { - MultichainNetworks, - TRANSACTIONS_CHECK_INTERVALS, -} from './constants'; +import { MultichainNetworks, TRANSACTIONS_CHECK_INTERVALS } from './constants'; import { MultichainTransactionsTracker } from './MultichainTransactionsTracker'; const controllerName = 'MultichainTransactionsController'; diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts index 688e91d7759..430cc8e7e30 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts @@ -28,8 +28,6 @@ const mockSolanaAccount = { type: SolAccountType.DataAccount, }; - - function setupTracker() { const mockUpdateTransactions = jest.fn(); const tracker = new MultichainTransactionsTracker(mockUpdateTransactions); diff --git a/packages/multichain-transactions-controller/src/constants.ts b/packages/multichain-transactions-controller/src/constants.ts index 85d64d10cd2..8b5c477cfe4 100644 --- a/packages/multichain-transactions-controller/src/constants.ts +++ b/packages/multichain-transactions-controller/src/constants.ts @@ -42,4 +42,4 @@ export const NETWORK_ASSETS_MAP: Record = { [MultichainNetworks.SolanaDevnet]: [MultichainNativeAssets.SolanaDevnet], [MultichainNetworks.Bitcoin]: [MultichainNativeAssets.Bitcoin], [MultichainNetworks.BitcoinTestnet]: [MultichainNativeAssets.BitcoinTestnet], -}; \ No newline at end of file +}; diff --git a/packages/multichain-transactions-controller/src/index.ts b/packages/multichain-transactions-controller/src/index.ts index cb8c0cbd6d0..4605ca2cbae 100644 --- a/packages/multichain-transactions-controller/src/index.ts +++ b/packages/multichain-transactions-controller/src/index.ts @@ -9,4 +9,4 @@ export { NETWORK_ASSETS_MAP, MultichainNetworks, MultichainNativeAssets, -} from './constants'; \ No newline at end of file +} from './constants'; From ff896e3680db790afc7be111cd12cc64111d5c15 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 13 Jan 2025 10:52:57 +0000 Subject: [PATCH 04/16] chore: reset file --- .depcheckrc.yml | 1 - .../multichain-transactions-controller/jest.config.js | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.depcheckrc.yml b/.depcheckrc.yml index 81cfa798d2e..00a8b8eb691 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -28,4 +28,3 @@ ignores: - 'eslint-import-resolver-typescript' # Ignore dependencies which plug into the NPM lifecycle - '@lavamoat/preinstall-always-fail' - \ No newline at end of file diff --git a/packages/multichain-transactions-controller/jest.config.js b/packages/multichain-transactions-controller/jest.config.js index ca084133399..1d1aca52dbd 100644 --- a/packages/multichain-transactions-controller/jest.config.js +++ b/packages/multichain-transactions-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 100, - functions: 100, - lines: 100, - statements: 100, + branches: 80, + functions: 95, + lines: 90, + statements: 90, }, }, }); From cab992439c2bb56193ba3ff979484fa8da16f330 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 13 Jan 2025 11:02:55 +0000 Subject: [PATCH 05/16] chore: lint fix --- .../multichain-transactions-controller/package.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/multichain-transactions-controller/package.json b/packages/multichain-transactions-controller/package.json index 831200d4e43..4635e63a45a 100644 --- a/packages/multichain-transactions-controller/package.json +++ b/packages/multichain-transactions-controller/package.json @@ -1,11 +1,10 @@ { "name": "@metamask/multichain-transactions-controller", "version": "0.0.0", - "description": "This package is responsible for getting transactions from our Bitcoin and Solana snaps.", + "description": "This package is responsible for getting transactions from our Bitcoin and Solana snaps", "keywords": [ "MetaMask", - "Bitcoin", - "Solana" + "Ethereum" ], "homepage": "https://github.com/MetaMask/core/tree/main/packages/multichain-transactions-controller#readme", "bugs": { @@ -30,8 +29,8 @@ }, "./package.json": "./package.json" }, - "main": "./dist/index.js", - "types": "./dist/types/index.d.ts", + "main": "./dist/index.cjs", + "types": "./dist/types/index.d.cts", "files": [ "dist/" ], @@ -49,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.1.0", - "@metamask/keyring-api": "^12.0.0", + "@metamask/keyring-api": "^13.0.0", "@metamask/polling-controller": "^12.0.2", "@metamask/snaps-utils": "^8.3.0", "@metamask/utils": "^11.0.1", From 0fd57bdbf74770b1f86ad9d3ea5fc757a9659759 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 13 Jan 2025 11:11:01 +0000 Subject: [PATCH 06/16] chore: ran yarn --- yarn.lock | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index cef3302117e..84a13f0cdc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -3364,7 +3352,7 @@ __metadata: "@metamask/accounts-controller": "npm:^20.0.2" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.1.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" From c6d653a8e52076448f0aba94772f5d55158c5ab8 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 13 Jan 2025 11:18:09 +0000 Subject: [PATCH 07/16] chore: fix package json entry, types --- packages/multichain-transactions-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-transactions-controller/package.json b/packages/multichain-transactions-controller/package.json index 4635e63a45a..ea6f2c21d40 100644 --- a/packages/multichain-transactions-controller/package.json +++ b/packages/multichain-transactions-controller/package.json @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "main": "./dist/index.cjs", - "types": "./dist/types/index.d.cts", + "types": "./dist/index.d.cts", "files": [ "dist/" ], From 7f5e254b1e1d9923ba42b46a9c6118fe31b66998 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 13 Jan 2025 11:26:42 +0000 Subject: [PATCH 08/16] chore: update teams json --- teams.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/teams.json b/teams.json index f59b3edb175..07dc1b203d4 100644 --- a/teams.json +++ b/teams.json @@ -34,5 +34,6 @@ "metamask/selected-network-controller": "team-wallet-api-platform,team-wallet-framework,team-assets", "metamask/signature-controller": "team-confirmations", "metamask/transaction-controller": "team-confirmations", - "metamask/user-operation-controller": "team-confirmations" + "metamask/user-operation-controller": "team-confirmations", + "metamask/multichain-transactions-controller": "team-sol,team-accounts" } From a56bb7d4882fe3965bb7a728c867a56669c4a06c Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Jan 2025 09:35:29 +0000 Subject: [PATCH 09/16] chore: update codeowners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c2025590372..ebfd7397874 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,7 +59,7 @@ /packages/permission-log-controller @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers /packages/selected-network-controller @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers @MetaMask/metamask-assets /packages/profile-sync-controller @MetaMask/notifications @MetaMask/identity -/packages/multichain-transactions-controller @MetaMask/solana-wallet-snap-team @MetaMask/cet-btc-team +/packages/multichain-transactions-controller @MetaMask/accounts-engineers ## Package Release related /packages/accounts-controller/package.json @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers From 0f81e0404e7f36902ccbb1a6653f649ac99be04c Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Jan 2025 09:51:44 +0000 Subject: [PATCH 10/16] chore: runs yarn --- yarn.lock | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index fb81b72ae7e..22cc99cdc70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2255,6 +2255,31 @@ __metadata: languageName: node linkType: hard +"@metamask/accounts-controller@npm:^20.0.2": + version: 20.0.2 + resolution: "@metamask/accounts-controller@npm:20.0.2" + dependencies: + "@ethereumjs/util": "npm:^8.1.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/eth-snap-keyring": "npm:^7.0.0" + "@metamask/keyring-api": "npm:^12.0.0" + "@metamask/keyring-internal-api": "npm:^1.0.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" + "@metamask/utils": "npm:^10.0.0" + deepmerge: "npm:^4.2.2" + ethereum-cryptography: "npm:^2.1.2" + immer: "npm:^9.0.6" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/keyring-controller": ^19.0.0 + "@metamask/providers": ^18.1.0 + "@metamask/snaps-controllers": ^9.7.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/4376418913fcdebda251c00a2f373ada5c5b26e45617ece3a5624bb0d7440bbfb1fb8332cae0f5f4fa04751fe2ffd8cea40fab342d54591fe21d8cd34c14744c + languageName: node + linkType: hard + "@metamask/accounts-controller@npm:^21.0.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" @@ -2463,7 +2488,7 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^7.0.2, @metamask/base-controller@npm:^7.1.1, @metamask/base-controller@workspace:packages/base-controller": +"@metamask/base-controller@npm:^7.0.2, @metamask/base-controller@npm:^7.1.0, @metamask/base-controller@npm:^7.1.1, @metamask/base-controller@workspace:packages/base-controller": version: 0.0.0-use.local resolution: "@metamask/base-controller@workspace:packages/base-controller" dependencies: @@ -2878,6 +2903,31 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-snap-keyring@npm:^7.0.0": + version: 7.1.0 + resolution: "@metamask/eth-snap-keyring@npm:7.1.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/eth-sig-util": "npm:^8.1.2" + "@metamask/keyring-api": "npm:^13.0.0" + "@metamask/keyring-internal-api": "npm:^1.1.0" + "@metamask/keyring-internal-snap-client": "npm:^1.1.0" + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/snaps-controllers": "npm:^9.10.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.0.1" + "@types/uuid": "npm:^9.0.8" + uuid: "npm:^9.0.1" + webextension-polyfill: "npm:^0.12.0" + peerDependencies: + "@metamask/keyring-api": ^13.0.0 + "@metamask/providers": ^18.3.1 + checksum: 10/0cfa24d5ad2f0ec83d096de906c48994994b2af999cdd7ab6a472aa9c96c1ceab1f800588f4cdc6f9a2c6821417772321c1a031fead53a5c61c792a6adea839f + languageName: node + linkType: hard + "@metamask/eth-snap-keyring@npm:^8.0.0": version: 8.0.0 resolution: "@metamask/eth-snap-keyring@npm:8.0.0" @@ -3156,6 +3206,18 @@ __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" @@ -3168,7 +3230,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.0.3, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^19.0.2, @metamask/keyring-controller@npm:^19.0.3, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -3206,6 +3268,18 @@ __metadata: languageName: unknown linkType: soft +"@metamask/keyring-internal-api@npm:^1.0.0, @metamask/keyring-internal-api@npm:^1.1.0": + version: 1.1.0 + resolution: "@metamask/keyring-internal-api@npm:1.1.0" + dependencies: + "@metamask/keyring-api": "npm:^13.0.0" + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.0.1" + checksum: 10/5db127cfe319c289b95d55d08b6737820f00761564e219d756a55e030b83043fb43d18bac3b63fcc69cd2e03129f831410ce960ba954d8183d976d87a6781b8c + languageName: node + linkType: hard + "@metamask/keyring-internal-api@npm:^2.0.0": version: 2.0.0 resolution: "@metamask/keyring-internal-api@npm:2.0.0" @@ -3218,6 +3292,23 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-internal-snap-client@npm:^1.1.0": + version: 1.1.0 + resolution: "@metamask/keyring-internal-snap-client@npm:1.1.0" + dependencies: + "@metamask/keyring-api": "npm:^13.0.0" + "@metamask/keyring-snap-client": "npm:^1.1.0" + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/snaps-controllers": "npm:^9.10.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" + webextension-polyfill: "npm:^0.12.0" + peerDependencies: + "@metamask/providers": ^18.3.1 + checksum: 10/58e0f00cc3798b156b7e2b3766216a8b355970391a33c9fb4b31cffc7c06d77cf63be275e5e217e0e05c517ed01dd8745b202882d6b7dd14891115d1dd022b7a + languageName: node + linkType: hard + "@metamask/keyring-internal-snap-client@npm:^2.0.0": version: 2.0.0 resolution: "@metamask/keyring-internal-snap-client@npm:2.0.0" @@ -3235,6 +3326,22 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-snap-client@npm:^1.0.0, @metamask/keyring-snap-client@npm:^1.1.0": + version: 1.1.0 + resolution: "@metamask/keyring-snap-client@npm:1.1.0" + dependencies: + "@metamask/keyring-api": "npm:^13.0.0" + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@types/uuid": "npm:^9.0.8" + uuid: "npm:^9.0.1" + webextension-polyfill: "npm:^0.12.0" + peerDependencies: + "@metamask/providers": ^18.3.1 + checksum: 10/f6e72d94d2fefd24619eca8c6fe838e448adbc42553656a4ffac44c32266836eb8223b30881c70434ce88e433c6fce12dbdaf9920dca7542b54334802cff72b7 + languageName: node + linkType: hard + "@metamask/keyring-snap-client@npm:^2.0.0": version: 2.0.0 resolution: "@metamask/keyring-snap-client@npm:2.0.0" From 70a01da3db97d23f056ec1e22f3a2c32bbdf549a Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Jan 2025 10:32:35 +0000 Subject: [PATCH 11/16] chore: fix lint issues --- .../package.json | 4 ++- .../MultichainTransactionsController.test.ts | 16 ++++++------ .../src/MultichainTransactionsController.ts | 6 ++--- .../src/MultichainTransactionsTracker.test.ts | 26 ++++++++++++------- .../src/MultichainTransactionsTracker.ts | 10 +++---- yarn.lock | 2 ++ 6 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/multichain-transactions-controller/package.json b/packages/multichain-transactions-controller/package.json index ea6f2c21d40..18d6bf039fb 100644 --- a/packages/multichain-transactions-controller/package.json +++ b/packages/multichain-transactions-controller/package.json @@ -52,7 +52,9 @@ "@metamask/polling-controller": "^12.0.2", "@metamask/snaps-utils": "^8.3.0", "@metamask/utils": "^11.0.1", - "immer": "^9.0.6" + "@types/uuid": "^8.3.0", + "immer": "^9.0.6", + "uuid": "^8.3.2" }, "devDependencies": { "@metamask/accounts-controller": "^20.0.2", diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts index e678ade9d7e..2726f2b180c 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts @@ -11,11 +11,11 @@ import { KeyringTypes } from '@metamask/keyring-controller'; import { v4 as uuidv4 } from 'uuid'; import { MultichainTransactionsController, - AllowedActions, - AllowedEvents, - MultichainTransactionsControllerState, defaultState, - MultichainTransactionsControllerMessenger, + type AllowedActions, + type AllowedEvents, + type MultichainTransactionsControllerState, + type MultichainTransactionsControllerMessenger, } from './MultichainTransactionsController'; import { MultichainTransactionsTracker } from './MultichainTransactionsTracker'; @@ -155,7 +155,7 @@ describe('MultichainTransactionsController', () => { 'start', ); const { controller } = setupController(); - await controller.start(); + controller.start(); expect(spyTracker).toHaveBeenCalledTimes(1); }); @@ -165,8 +165,8 @@ describe('MultichainTransactionsController', () => { 'stop', ); const { controller } = setupController(); - await controller.start(); - await controller.stop(); + controller.start(); + controller.stop(); expect(spyTracker).toHaveBeenCalledTimes(1); }); @@ -331,7 +331,7 @@ describe('MultichainTransactionsController', () => { const { controller, mockSnapHandleRequest } = setupController(); mockSnapHandleRequest.mockRejectedValue(new Error('Failed to fetch')); - await expect(controller.updateTransactions()).resolves.not.toThrow(); + await controller.updateTransactions(); expect(controller.state.nonEvmTransactions).toEqual({}); }); }); diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts index 800a426178d..1351f36b01b 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts @@ -6,7 +6,7 @@ import { type ControllerStateChangeEvent, type RestrictedControllerMessenger, } from '@metamask/base-controller'; -import { isEvmAccountType, Transaction } from '@metamask/keyring-api'; +import { isEvmAccountType, type Transaction } from '@metamask/keyring-api'; import { KeyringClient } from '@metamask/keyring-snap-client'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; @@ -286,14 +286,14 @@ export class MultichainTransactionsController 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/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts index 430cc8e7e30..b16fc320c26 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts @@ -28,7 +28,13 @@ const mockSolanaAccount = { type: SolAccountType.DataAccount, }; -function setupTracker() { +/** + * Creates and returns a new MultichainTransactionsTracker instance with a mock update function. + */ +function setupTracker(): { + tracker: MultichainTransactionsTracker; + mockUpdateTransactions: jest.Mock; +} { const mockUpdateTransactions = jest.fn(); const tracker = new MultichainTransactionsTracker(mockUpdateTransactions); @@ -43,7 +49,7 @@ describe('MultichainTransactionsTracker', () => { const { tracker } = setupTracker(); const spyPoller = jest.spyOn(Poller.prototype, 'start'); - await tracker.start(); + tracker.start(); expect(spyPoller).toHaveBeenCalledTimes(1); }); @@ -51,15 +57,15 @@ describe('MultichainTransactionsTracker', () => { const { tracker } = setupTracker(); const spyPoller = jest.spyOn(Poller.prototype, 'stop'); - await tracker.start(); - await tracker.stop(); + tracker.start(); + tracker.stop(); expect(spyPoller).toHaveBeenCalledTimes(1); }); it('is not tracking if none accounts have been registered', async () => { const { tracker, mockUpdateTransactions } = setupTracker(); - await tracker.start(); + tracker.start(); await tracker.updateTransactions(); expect(mockUpdateTransactions).not.toHaveBeenCalled(); @@ -68,7 +74,7 @@ describe('MultichainTransactionsTracker', () => { it('tracks account transactions', async () => { const { tracker, mockUpdateTransactions } = setupTracker(); - await tracker.start(); + tracker.start(); tracker.track(mockSolanaAccount.id, 0); await tracker.updateTransactions(); @@ -80,7 +86,7 @@ describe('MultichainTransactionsTracker', () => { it('untracks account transactions', async () => { const { tracker, mockUpdateTransactions } = setupTracker(); - await tracker.start(); + tracker.start(); tracker.track(mockSolanaAccount.id, 0); await tracker.updateTransactions(); expect(mockUpdateTransactions).toHaveBeenCalledWith(mockSolanaAccount.id, { @@ -95,7 +101,7 @@ describe('MultichainTransactionsTracker', () => { it('tracks account after being registered', async () => { const { tracker } = setupTracker(); - await tracker.start(); + tracker.start(); tracker.track(mockSolanaAccount.id, 0); expect(tracker.isTracked(mockSolanaAccount.id)).toBe(true); }); @@ -103,7 +109,7 @@ describe('MultichainTransactionsTracker', () => { it('does not track account if not registered', async () => { const { tracker } = setupTracker(); - await tracker.start(); + tracker.start(); expect(tracker.isTracked(mockSolanaAccount.id)).toBe(false); }); @@ -115,7 +121,7 @@ describe('MultichainTransactionsTracker', () => { .spyOn(global.Date, 'now') .mockImplementation(() => new Date(MOCK_TIMESTAMP).getTime()); - await tracker.start(); + tracker.start(); tracker.track(mockSolanaAccount.id, blockTime); await tracker.updateTransactions(); expect(mockUpdateTransactions).toHaveBeenCalledTimes(1); diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts index fb3d2b07f76..c3fd9b63ab0 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts @@ -1,5 +1,5 @@ import { Poller } from './Poller'; -import { PaginationOptions } from './MultichainTransactionsController'; +import type { PaginationOptions } from './MultichainTransactionsController'; type TransactionInfo = { lastUpdated: number; @@ -28,22 +28,22 @@ export class MultichainTransactionsTracker { ) { this.#updateTransactions = updateTransactionsCallback; - this.#poller = new Poller(() => { - this.updateTransactions(); + this.#poller = new Poller(async () => { + await this.updateTransactions(); }, TRANSACTIONS_TRACKING_INTERVAL); } /** * Starts the tracking process. */ - async start(): Promise { + start(): void { this.#poller.start(); } /** * Stops the tracking process. */ - async stop(): Promise { + stop(): void { this.#poller.stop(); } diff --git a/yarn.lock b/yarn.lock index 22cc99cdc70..8239c311f8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3432,6 +3432,7 @@ __metadata: "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" + "@types/uuid": "npm:^8.3.0" deepmerge: "npm:^4.2.2" immer: "npm:^9.0.6" jest: "npm:^27.5.1" @@ -3439,6 +3440,7 @@ __metadata: typedoc: "npm:^0.24.8" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" + uuid: "npm:^8.3.2" peerDependencies: "@metamask/accounts-controller": ^20.0.0 "@metamask/keyring-controller": ^19.0.0 From f366a9a7881a86599b22fdb2a8162c865696644a Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Jan 2025 11:26:35 +0000 Subject: [PATCH 12/16] chore: fix lint warnings and update thresholds --- eslint-warning-thresholds.json | 8 +++---- .../MultichainTransactionsController.test.ts | 21 +++++++++++-------- .../src/MultichainTransactionsController.ts | 8 +++---- .../src/MultichainTransactionsTracker.test.ts | 4 +++- .../src/MultichainTransactionsTracker.ts | 4 ++-- .../src/Poller.ts | 4 ++-- 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index fc7d2173b69..718808fabae 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -2,16 +2,16 @@ "@typescript-eslint/consistent-type-exports": 19, "@typescript-eslint/no-base-to-string": 3, "@typescript-eslint/no-duplicate-enum-values": 2, - "@typescript-eslint/no-misused-promises": 3, - "@typescript-eslint/no-unsafe-enum-comparison": 59, + "@typescript-eslint/no-misused-promises": 1, + "@typescript-eslint/no-unsafe-enum-comparison": 32, "@typescript-eslint/no-unused-vars": 36, "@typescript-eslint/prefer-promise-reject-errors": 13, "@typescript-eslint/prefer-readonly": 145, - "@typescript-eslint/switch-exhaustiveness-check": 10, + "@typescript-eslint/switch-exhaustiveness-check": 9, "import-x/namespace": 189, "import-x/no-named-as-default": 1, "import-x/no-named-as-default-member": 8, - "import-x/order": 209, + "import-x/order": 208, "jest/no-conditional-in-test": 129, "jest/prefer-lowercase-title": 2, "jest/prefer-strict-equal": 2, diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts index 2726f2b180c..27ecb2596c2 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts @@ -1,6 +1,5 @@ import { ControllerMessenger } from '@metamask/base-controller'; -import type { Transaction, CaipAssetType } from '@metamask/keyring-api'; -import { type InternalAccount } from '@metamask/keyring-internal-api'; +import type { CaipAssetType, Transaction } from '@metamask/keyring-api'; import { BtcAccountType, BtcMethod, @@ -8,7 +7,9 @@ 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 { MultichainTransactionsController, defaultState, @@ -146,7 +147,7 @@ const setupController = ({ describe('MultichainTransactionsController', () => { it('initialize with default state', () => { const { controller } = setupController({}); - expect(controller.state).toEqual({ nonEvmTransactions: {} }); + expect(controller.state).toStrictEqual({ nonEvmTransactions: {} }); }); it('starts tracking when calling start', async () => { @@ -175,7 +176,7 @@ describe('MultichainTransactionsController', () => { await controller.updateTransactions(); - expect(controller.state).toEqual({ + expect(controller.state).toStrictEqual({ nonEvmTransactions: { [mockBtcAccount.id]: { transactions: mockTransactionResult.data, @@ -199,7 +200,7 @@ describe('MultichainTransactionsController', () => { messenger.publish('AccountsController:accountAdded', mockBtcAccount); await controller.updateTransactions(); - expect(controller.state).toEqual({ + expect(controller.state).toStrictEqual({ nonEvmTransactions: { [mockBtcAccount.id]: { transactions: mockTransactionResult.data, @@ -216,7 +217,7 @@ describe('MultichainTransactionsController', () => { controller.start(); await controller.updateTransactions(); - expect(controller.state).toEqual({ + expect(controller.state).toStrictEqual({ nonEvmTransactions: { [mockBtcAccount.id]: { transactions: mockTransactionResult.data, @@ -230,7 +231,7 @@ describe('MultichainTransactionsController', () => { mockListMultichainAccounts.mockReturnValue([]); await controller.updateTransactions(); - expect(controller.state).toEqual({ + expect(controller.state).toStrictEqual({ nonEvmTransactions: {}, }); }); @@ -257,7 +258,9 @@ describe('MultichainTransactionsController', () => { const { controller } = setupController(); await controller.updateTransactionsForAccount(mockBtcAccount.id); - expect(controller.state.nonEvmTransactions[mockBtcAccount.id]).toEqual({ + expect( + controller.state.nonEvmTransactions[mockBtcAccount.id], + ).toStrictEqual({ transactions: mockTransactionResult.data, next: null, lastUpdated: expect.any(Number), @@ -332,6 +335,6 @@ describe('MultichainTransactionsController', () => { mockSnapHandleRequest.mockRejectedValue(new Error('Failed to fetch')); await controller.updateTransactions(); - expect(controller.state.nonEvmTransactions).toEqual({}); + expect(controller.state.nonEvmTransactions).toStrictEqual({}); }); }); diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts index 1351f36b01b..ecd6b46b95d 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts @@ -1,5 +1,3 @@ -import type { Json, JsonRpcRequest } from '@metamask/utils'; - import { BaseController, type ControllerGetStateAction, @@ -12,12 +10,14 @@ import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; -import type { Draft } from 'immer'; import type { AccountsControllerAccountAddedEvent, AccountsControllerAccountRemovedEvent, AccountsControllerListMultichainAccountsAction, } from '@metamask/accounts-controller'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; +import type { Draft } from 'immer'; + import { MultichainNetworks, TRANSACTIONS_CHECK_INTERVALS } from './constants'; import { MultichainTransactionsTracker } from './MultichainTransactionsTracker'; @@ -141,7 +141,7 @@ export class MultichainTransactionsController extends BaseController< MultichainTransactionsControllerState, MultichainTransactionsControllerMessenger > { - #tracker: MultichainTransactionsTracker; + readonly #tracker: MultichainTransactionsTracker; constructor({ messenger, diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts index b16fc320c26..386d07616f6 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.test.ts @@ -2,8 +2,8 @@ import { SolAccountType, SolMethod } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import { v4 as uuidv4 } from 'uuid'; -import { Poller } from './Poller'; import { MultichainTransactionsTracker } from './MultichainTransactionsTracker'; +import { Poller } from './Poller'; const MOCK_TIMESTAMP = 1733788800; @@ -30,6 +30,8 @@ const mockSolanaAccount = { /** * Creates and returns a new MultichainTransactionsTracker instance with a mock update function. + * + * @returns The tracker instance and mock update function. */ function setupTracker(): { tracker: MultichainTransactionsTracker; diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts index c3fd9b63ab0..bf6a3dc6a50 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsTracker.ts @@ -11,9 +11,9 @@ type TransactionInfo = { const TRANSACTIONS_TRACKING_INTERVAL = 5 * 1000; export class MultichainTransactionsTracker { - #poller: Poller; + readonly #poller: Poller; - #updateTransactions: ( + readonly #updateTransactions: ( accountId: string, pagination: PaginationOptions, ) => Promise; diff --git a/packages/multichain-transactions-controller/src/Poller.ts b/packages/multichain-transactions-controller/src/Poller.ts index 600e2ea615d..166014a5f3f 100644 --- a/packages/multichain-transactions-controller/src/Poller.ts +++ b/packages/multichain-transactions-controller/src/Poller.ts @@ -1,7 +1,7 @@ export class Poller { - #interval: number; + readonly #interval: number; - #callback: () => void; + readonly #callback: () => void; #handle: NodeJS.Timeout | undefined = undefined; From 40826b1122999d500d47514d01352a1fdfa6bf9d Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Jan 2025 11:35:05 +0000 Subject: [PATCH 13/16] chore: reset --- eslint-warning-thresholds.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 718808fabae..fc7d2173b69 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -2,16 +2,16 @@ "@typescript-eslint/consistent-type-exports": 19, "@typescript-eslint/no-base-to-string": 3, "@typescript-eslint/no-duplicate-enum-values": 2, - "@typescript-eslint/no-misused-promises": 1, - "@typescript-eslint/no-unsafe-enum-comparison": 32, + "@typescript-eslint/no-misused-promises": 3, + "@typescript-eslint/no-unsafe-enum-comparison": 59, "@typescript-eslint/no-unused-vars": 36, "@typescript-eslint/prefer-promise-reject-errors": 13, "@typescript-eslint/prefer-readonly": 145, - "@typescript-eslint/switch-exhaustiveness-check": 9, + "@typescript-eslint/switch-exhaustiveness-check": 10, "import-x/namespace": 189, "import-x/no-named-as-default": 1, "import-x/no-named-as-default-member": 8, - "import-x/order": 208, + "import-x/order": 209, "jest/no-conditional-in-test": 129, "jest/prefer-lowercase-title": 2, "jest/prefer-strict-equal": 2, From 1573d85cab88f056a8cd033d1472a91892921bf5 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Jan 2025 11:49:58 +0000 Subject: [PATCH 14/16] chore: update dependencies --- eslint-warning-thresholds.json | 8 +- .../package.json | 12 +- yarn.lock | 123 ++---------------- 3 files changed, 18 insertions(+), 125 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index fc7d2173b69..718808fabae 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -2,16 +2,16 @@ "@typescript-eslint/consistent-type-exports": 19, "@typescript-eslint/no-base-to-string": 3, "@typescript-eslint/no-duplicate-enum-values": 2, - "@typescript-eslint/no-misused-promises": 3, - "@typescript-eslint/no-unsafe-enum-comparison": 59, + "@typescript-eslint/no-misused-promises": 1, + "@typescript-eslint/no-unsafe-enum-comparison": 32, "@typescript-eslint/no-unused-vars": 36, "@typescript-eslint/prefer-promise-reject-errors": 13, "@typescript-eslint/prefer-readonly": 145, - "@typescript-eslint/switch-exhaustiveness-check": 10, + "@typescript-eslint/switch-exhaustiveness-check": 9, "import-x/namespace": 189, "import-x/no-named-as-default": 1, "import-x/no-named-as-default-member": 8, - "import-x/order": 209, + "import-x/order": 208, "jest/no-conditional-in-test": 129, "jest/prefer-lowercase-title": 2, "jest/prefer-strict-equal": 2, diff --git a/packages/multichain-transactions-controller/package.json b/packages/multichain-transactions-controller/package.json index 18d6bf039fb..5c5d0610437 100644 --- a/packages/multichain-transactions-controller/package.json +++ b/packages/multichain-transactions-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.0", + "@metamask/base-controller": "^7.1.1", "@metamask/keyring-api": "^13.0.0", "@metamask/polling-controller": "^12.0.2", "@metamask/snaps-utils": "^8.3.0", @@ -57,11 +57,11 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@metamask/accounts-controller": "^20.0.2", + "@metamask/accounts-controller": "^21.0.0", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.2", - "@metamask/keyring-internal-api": "^1.1.0", - "@metamask/keyring-snap-client": "^1.0.0", + "@metamask/keyring-controller": "^19.0.3", + "@metamask/keyring-internal-api": "^2.0.0", + "@metamask/keyring-snap-client": "^2.0.0", "@metamask/snaps-controllers": "^9.10.0", "@metamask/snaps-sdk": "^6.7.0", "@types/jest": "^27.4.1", @@ -73,7 +73,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^20.0.0", + "@metamask/accounts-controller": "^21.0.0", "@metamask/keyring-controller": "^19.0.0" }, "engines": { diff --git a/yarn.lock b/yarn.lock index 8239c311f8b..df90b4304c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2255,31 +2255,6 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^20.0.2": - version: 20.0.2 - resolution: "@metamask/accounts-controller@npm:20.0.2" - dependencies: - "@ethereumjs/util": "npm:^8.1.0" - "@metamask/base-controller": "npm:^7.0.2" - "@metamask/eth-snap-keyring": "npm:^7.0.0" - "@metamask/keyring-api": "npm:^12.0.0" - "@metamask/keyring-internal-api": "npm:^1.0.0" - "@metamask/snaps-sdk": "npm:^6.7.0" - "@metamask/snaps-utils": "npm:^8.3.0" - "@metamask/utils": "npm:^10.0.0" - deepmerge: "npm:^4.2.2" - ethereum-cryptography: "npm:^2.1.2" - immer: "npm:^9.0.6" - uuid: "npm:^8.3.2" - peerDependencies: - "@metamask/keyring-controller": ^19.0.0 - "@metamask/providers": ^18.1.0 - "@metamask/snaps-controllers": ^9.7.0 - webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/4376418913fcdebda251c00a2f373ada5c5b26e45617ece3a5624bb0d7440bbfb1fb8332cae0f5f4fa04751fe2ffd8cea40fab342d54591fe21d8cd34c14744c - languageName: node - linkType: hard - "@metamask/accounts-controller@npm:^21.0.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" @@ -2488,7 +2463,7 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^7.0.2, @metamask/base-controller@npm:^7.1.0, @metamask/base-controller@npm:^7.1.1, @metamask/base-controller@workspace:packages/base-controller": +"@metamask/base-controller@npm:^7.0.2, @metamask/base-controller@npm:^7.1.1, @metamask/base-controller@workspace:packages/base-controller": version: 0.0.0-use.local resolution: "@metamask/base-controller@workspace:packages/base-controller" dependencies: @@ -2903,31 +2878,6 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^7.0.0": - version: 7.1.0 - resolution: "@metamask/eth-snap-keyring@npm:7.1.0" - dependencies: - "@ethereumjs/tx": "npm:^4.2.0" - "@metamask/eth-sig-util": "npm:^8.1.2" - "@metamask/keyring-api": "npm:^13.0.0" - "@metamask/keyring-internal-api": "npm:^1.1.0" - "@metamask/keyring-internal-snap-client": "npm:^1.1.0" - "@metamask/keyring-utils": "npm:^1.0.0" - "@metamask/snaps-controllers": "npm:^9.10.0" - "@metamask/snaps-sdk": "npm:^6.7.0" - "@metamask/snaps-utils": "npm:^8.3.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.0.1" - "@types/uuid": "npm:^9.0.8" - uuid: "npm:^9.0.1" - webextension-polyfill: "npm:^0.12.0" - peerDependencies: - "@metamask/keyring-api": ^13.0.0 - "@metamask/providers": ^18.3.1 - checksum: 10/0cfa24d5ad2f0ec83d096de906c48994994b2af999cdd7ab6a472aa9c96c1ceab1f800588f4cdc6f9a2c6821417772321c1a031fead53a5c61c792a6adea839f - languageName: node - linkType: hard - "@metamask/eth-snap-keyring@npm:^8.0.0": version: 8.0.0 resolution: "@metamask/eth-snap-keyring@npm:8.0.0" @@ -3206,18 +3156,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" @@ -3230,7 +3168,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.0.2, @metamask/keyring-controller@npm:^19.0.3, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^19.0.3, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -3268,18 +3206,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/keyring-internal-api@npm:^1.0.0, @metamask/keyring-internal-api@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask/keyring-internal-api@npm:1.1.0" - dependencies: - "@metamask/keyring-api": "npm:^13.0.0" - "@metamask/keyring-utils": "npm:^1.0.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.0.1" - checksum: 10/5db127cfe319c289b95d55d08b6737820f00761564e219d756a55e030b83043fb43d18bac3b63fcc69cd2e03129f831410ce960ba954d8183d976d87a6781b8c - languageName: node - linkType: hard - "@metamask/keyring-internal-api@npm:^2.0.0": version: 2.0.0 resolution: "@metamask/keyring-internal-api@npm:2.0.0" @@ -3292,23 +3218,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-internal-snap-client@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask/keyring-internal-snap-client@npm:1.1.0" - dependencies: - "@metamask/keyring-api": "npm:^13.0.0" - "@metamask/keyring-snap-client": "npm:^1.1.0" - "@metamask/keyring-utils": "npm:^1.0.0" - "@metamask/snaps-controllers": "npm:^9.10.0" - "@metamask/snaps-sdk": "npm:^6.7.0" - "@metamask/snaps-utils": "npm:^8.3.0" - webextension-polyfill: "npm:^0.12.0" - peerDependencies: - "@metamask/providers": ^18.3.1 - checksum: 10/58e0f00cc3798b156b7e2b3766216a8b355970391a33c9fb4b31cffc7c06d77cf63be275e5e217e0e05c517ed01dd8745b202882d6b7dd14891115d1dd022b7a - languageName: node - linkType: hard - "@metamask/keyring-internal-snap-client@npm:^2.0.0": version: 2.0.0 resolution: "@metamask/keyring-internal-snap-client@npm:2.0.0" @@ -3326,22 +3235,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-snap-client@npm:^1.0.0, @metamask/keyring-snap-client@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask/keyring-snap-client@npm:1.1.0" - dependencies: - "@metamask/keyring-api": "npm:^13.0.0" - "@metamask/keyring-utils": "npm:^1.0.0" - "@metamask/superstruct": "npm:^3.1.0" - "@types/uuid": "npm:^9.0.8" - uuid: "npm:^9.0.1" - webextension-polyfill: "npm:^0.12.0" - peerDependencies: - "@metamask/providers": ^18.3.1 - checksum: 10/f6e72d94d2fefd24619eca8c6fe838e448adbc42553656a4ffac44c32266836eb8223b30881c70434ce88e433c6fce12dbdaf9920dca7542b54334802cff72b7 - languageName: node - linkType: hard - "@metamask/keyring-snap-client@npm:^2.0.0": version: 2.0.0 resolution: "@metamask/keyring-snap-client@npm:2.0.0" @@ -3419,13 +3312,13 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller" dependencies: - "@metamask/accounts-controller": "npm:^20.0.2" + "@metamask/accounts-controller": "npm:^21.0.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.0" + "@metamask/base-controller": "npm:^7.1.1" "@metamask/keyring-api": "npm:^13.0.0" - "@metamask/keyring-controller": "npm:^19.0.2" - "@metamask/keyring-internal-api": "npm:^1.1.0" - "@metamask/keyring-snap-client": "npm:^1.0.0" + "@metamask/keyring-controller": "npm:^19.0.3" + "@metamask/keyring-internal-api": "npm:^2.0.0" + "@metamask/keyring-snap-client": "npm:^2.0.0" "@metamask/polling-controller": "npm:^12.0.2" "@metamask/snaps-controllers": "npm:^9.10.0" "@metamask/snaps-sdk": "npm:^6.7.0" @@ -3442,7 +3335,7 @@ __metadata: typescript: "npm:~5.2.2" uuid: "npm:^8.3.2" peerDependencies: - "@metamask/accounts-controller": ^20.0.0 + "@metamask/accounts-controller": ^21.0.0 "@metamask/keyring-controller": ^19.0.0 languageName: unknown linkType: soft From c21c7491d8f4477f1eae4a710fb7febb604c0616 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Jan 2025 11:54:17 +0000 Subject: [PATCH 15/16] chore: update to correct value --- eslint-warning-thresholds.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 718808fabae..9383a03e926 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -3,7 +3,7 @@ "@typescript-eslint/no-base-to-string": 3, "@typescript-eslint/no-duplicate-enum-values": 2, "@typescript-eslint/no-misused-promises": 1, - "@typescript-eslint/no-unsafe-enum-comparison": 32, + "@typescript-eslint/no-unsafe-enum-comparison": 35, "@typescript-eslint/no-unused-vars": 36, "@typescript-eslint/prefer-promise-reject-errors": 13, "@typescript-eslint/prefer-readonly": 145, From f2ed82a6b667c25543144138cefacf04ade94f05 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Jan 2025 11:59:50 +0000 Subject: [PATCH 16/16] chore: updates comparison --- .../src/MultichainTransactionsController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts index ecd6b46b95d..65e0a693573 100644 --- a/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts +++ b/packages/multichain-transactions-controller/src/MultichainTransactionsController.ts @@ -226,8 +226,9 @@ export class MultichainTransactionsController extends BaseController< * All other chain transactions are included as-is */ const transactions = response.data.filter((tx) => { - if (tx.chain.startsWith(MultichainNetworks.Solana)) { - return tx.chain === MultichainNetworks.Solana; + const chain = tx.chain as MultichainNetworks; + if (chain.startsWith(MultichainNetworks.Solana)) { + return chain === MultichainNetworks.Solana; } return true; });