diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 91b96b4055b..edba796d74e 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add per-provider throttling for non-EVM account creation to improve performance on low-end devices ([#7000](https://github.com/MetaMask/core/pull/7000)) + - Solana provider is now limited to 3 concurrent account creations by default when creating multichain account groups. + - Other providers remain unthrottled by default. + ## [2.0.1] ### Fixed diff --git a/packages/multichain-account-service/src/MultichainAccountService.test.ts b/packages/multichain-account-service/src/MultichainAccountService.test.ts index 424b5f49805..144d709033e 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.test.ts @@ -200,6 +200,7 @@ describe('MultichainAccountService', () => { }, }, [SOL_ACCOUNT_PROVIDER_NAME]: { + maxConcurrency: 3, discovery: { timeoutMs: 5000, maxAttempts: 4, @@ -218,11 +219,11 @@ describe('MultichainAccountService', () => { expect(mocks.EvmAccountProvider.constructor).toHaveBeenCalledWith( messenger, - providerConfigs[EvmAccountProvider.NAME], + providerConfigs?.[EvmAccountProvider.NAME], ); expect(mocks.SolAccountProvider.constructor).toHaveBeenCalledWith( messenger, - providerConfigs[SolAccountProvider.NAME], + providerConfigs?.[SolAccountProvider.NAME], ); }); @@ -232,6 +233,7 @@ describe('MultichainAccountService', () => { // NOTE: We use constants here, since `*AccountProvider` are mocked, thus, their `.NAME` will // be `undefined`. [SOL_ACCOUNT_PROVIDER_NAME]: { + maxConcurrency: 3, discovery: { timeoutMs: 5000, maxAttempts: 4, @@ -255,7 +257,7 @@ describe('MultichainAccountService', () => { ); expect(mocks.SolAccountProvider.constructor).toHaveBeenCalledWith( messenger, - providerConfigs[SolAccountProvider.NAME], + providerConfigs?.[SolAccountProvider.NAME], ); }); }); diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index 776e057b139..b628ab0558c 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -18,14 +18,16 @@ import { MultichainAccountWallet } from './MultichainAccountWallet'; import type { EvmAccountProviderConfig, NamedAccountProvider, - SolAccountProviderConfig, } from './providers'; import { AccountProviderWrapper, isAccountProviderWrapper, } from './providers/AccountProviderWrapper'; import { EvmAccountProvider } from './providers/EvmAccountProvider'; -import { SolAccountProvider } from './providers/SolAccountProvider'; +import { + SolAccountProvider, + type SolAccountProviderConfig, +} from './providers/SolAccountProvider'; import type { MultichainAccountServiceMessenger } from './types'; export const serviceName = 'MultichainAccountService'; diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts index d87a2361881..40dde775ad7 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts @@ -364,6 +364,53 @@ describe('MultichainAccountWallet', () => { 'Unable to create multichain account group for index: 1', ); }); + + it('aggregates non-EVM failures when waiting for all providers', async () => { + const startingIndex = 0; + + const mockEvmAccount = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(startingIndex) + .get(); + + const { wallet, providers } = setup({ + providers: [ + setupNamedAccountProvider({ accounts: [mockEvmAccount], index: 0 }), + setupNamedAccountProvider({ + name: 'Non-EVM Provider', + accounts: [], + index: 1, + }), + ], + }); + + const nextIndex = 1; + const nextEvmAccount = MockAccountBuilder.from(mockEvmAccount) + .withGroupIndex(nextIndex) + .get(); + + const [evmProvider, solProvider] = providers; + evmProvider.createAccounts.mockResolvedValueOnce([nextEvmAccount]); + evmProvider.getAccounts.mockReturnValueOnce([nextEvmAccount]); + evmProvider.getAccount.mockReturnValueOnce(nextEvmAccount); + + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const SOL_PROVIDER_ERROR = 'SOL create failed'; + solProvider.createAccounts.mockRejectedValueOnce( + new Error(SOL_PROVIDER_ERROR), + ); + + await expect( + wallet.createMultichainAccountGroup(nextIndex, { + waitForAllProvidersToFinishCreatingAccounts: true, + }), + ).rejects.toThrow( + `Unable to create multichain account group for index: ${nextIndex}:\n- Error: ${SOL_PROVIDER_ERROR}`, + ); + + expect(warnSpy).toHaveBeenCalled(); + }); }); describe('createNextMultichainAccountGroup', () => { diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index f90c0a73694..b1d6b3df96a 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -26,6 +26,7 @@ import { import { MultichainAccountGroup } from './MultichainAccountGroup'; import { EvmAccountProvider, type NamedAccountProvider } from './providers'; import type { MultichainAccountServiceMessenger } from './types'; +import { toRejectedErrorMessage } from './utils'; /** * The context for a provider discovery. @@ -220,6 +221,65 @@ export class MultichainAccountWallet< } } + /** + * Create accounts with non‑EVM providers. Optional throttling is managed by each provider internally. + * When awaitAll is true, waits for all providers and throws if any failed. + * When false, starts work in background and logs errors without throwing. + * + * @param options - Method options. + * @param options.groupIndex - The group index to create accounts for. + * @param options.providers - The non‑EVM account providers. + * @param options.awaitAll - Whether to wait for all providers to finish. + * @throws If awaitAll is true and any provider fails to create accounts. + * @returns A promise that resolves when done (if awaitAll is true) or immediately (if false). + */ + async #createNonEvmAccounts({ + groupIndex, + providers, + awaitAll, + }: { + groupIndex: number; + providers: NamedAccountProvider[]; + awaitAll: boolean; + }): Promise { + if (awaitAll) { + const tasks = providers.map((provider) => + provider.createAccounts({ + entropySource: this.#entropySource, + groupIndex, + }), + ); + + const results = await Promise.allSettled(tasks); + if (results.some((r) => r.status === 'rejected')) { + const errorMessage = toRejectedErrorMessage( + `Unable to create multichain account group for index: ${groupIndex}`, + results, + ); + + this.#log(`${WARNING_PREFIX} ${errorMessage}`); + console.warn(errorMessage); + throw new Error(errorMessage); + } + return; + } + + // Background mode: start tasks and log errors. + // Optional throttling is handled internally by each provider based on its config. + providers.forEach((provider) => { + // eslint-disable-next-line no-void + void provider + .createAccounts({ + entropySource: this.#entropySource, + groupIndex, + }) + .catch((error) => { + const errorMessage = `Unable to create multichain account group for index: ${groupIndex} (background mode with provider "${provider.getName()}")`; + this.#log(`${WARNING_PREFIX} ${errorMessage}:`, error); + }); + }); + } + /** * Gets multichain account for a given ID. * The default group ID will default to the multichain account with index 0. @@ -335,70 +395,39 @@ export class MultichainAccountWallet< this.#log(`Creating new group for index ${groupIndex}...`); - if (options?.waitForAllProvidersToFinishCreatingAccounts) { - // Create account with all providers and await them. - const results = await Promise.allSettled( - this.#providers.map((provider) => - provider.createAccounts({ - entropySource: this.#entropySource, - groupIndex, - }), - ), - ); + // Extract the EVM provider from the list of providers. + // We always await EVM account creation first. + const [evmProvider, ...otherProviders] = this.#providers; + assert( + evmProvider instanceof EvmAccountProvider, + 'EVM account provider must be first', + ); - // If any of the provider failed to create their accounts, then we consider the - // multichain account group to have failed too. - if (results.some((result) => result.status === 'rejected')) { - // NOTE: Some accounts might still have been created on other account providers. We - // don't rollback them. - const error = `Unable to create multichain account group for index: ${groupIndex}`; - - let message = `${error}:`; - for (const result of results) { - if (result.status === 'rejected') { - message += `\n- ${result.reason}`; - } - } - this.#log(`${WARNING_PREFIX} ${message}`); - console.warn(message); + try { + await evmProvider.createAccounts({ + entropySource: this.#entropySource, + groupIndex, + }); + } catch (error) { + const errorMessage = `Unable to create multichain account group for index: ${groupIndex} with provider "${evmProvider.getName()}". Error: ${(error as Error).message}`; + this.#log(`${ERROR_PREFIX} ${errorMessage}:`, error); + throw new Error(errorMessage); + } - throw new Error(error); - } + // We then create accounts with other providers (some being throttled if configured). + // Depending on the options, we either await all providers or run them in background. + if (options?.waitForAllProvidersToFinishCreatingAccounts) { + await this.#createNonEvmAccounts({ + groupIndex, + providers: otherProviders, + awaitAll: true, + }); } else { - // Extract the EVM provider from the list of providers. - // We will only await the EVM provider to create its accounts, while - // all other providers will be started in the background. - const [evmProvider, ...otherProviders] = this.#providers; - assert( - evmProvider instanceof EvmAccountProvider, - 'EVM account provider must be first', - ); - - // Create account with the EVM provider first and await it. - // If it fails, we don't start creating accounts with other providers. - try { - await evmProvider.createAccounts({ - entropySource: this.#entropySource, - groupIndex, - }); - } catch (error) { - const errorMessage = `Unable to create multichain account group for index: ${groupIndex} with provider "${evmProvider.getName()}". Error: ${(error as Error).message}`; - this.#log(`${ERROR_PREFIX} ${errorMessage}:`, error); - throw new Error(errorMessage); - } - - // Create account with other providers in the background - otherProviders.forEach((provider) => { - provider - .createAccounts({ - entropySource: this.#entropySource, - groupIndex, - }) - .catch((error) => { - // Log errors from background providers but don't fail the operation - const errorMessage = `Could not to create account with provider "${provider.getName()}" for multichain account group index: ${groupIndex}`; - this.#log(`${WARNING_PREFIX} ${errorMessage}:`, error); - }); + // eslint-disable-next-line no-void + void this.#createNonEvmAccounts({ + groupIndex, + providers: otherProviders, + awaitAll: false, }); } diff --git a/packages/multichain-account-service/src/providers/BtcAccountProvider.ts b/packages/multichain-account-service/src/providers/BtcAccountProvider.ts index 8e004f82bf9..a938a02f7ab 100644 --- a/packages/multichain-account-service/src/providers/BtcAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/BtcAccountProvider.ts @@ -7,20 +7,14 @@ import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; import type { Json, JsonRpcRequest } from '@metamask/utils'; -import { SnapAccountProvider } from './SnapAccountProvider'; +import { + SnapAccountProvider, + type SnapAccountProviderConfig, +} from './SnapAccountProvider'; import { withRetry, withTimeout } from './utils'; import type { MultichainAccountServiceMessenger } from '../types'; -export type BtcAccountProviderConfig = { - discovery: { - maxAttempts: number; - timeoutMs: number; - backOffMs: number; - }; - createAccounts: { - timeoutMs: number; - }; -}; +export type BtcAccountProviderConfig = SnapAccountProviderConfig; export const BTC_ACCOUNT_PROVIDER_NAME = 'Bitcoin' as const; @@ -31,8 +25,6 @@ export class BtcAccountProvider extends SnapAccountProvider { readonly #client: KeyringClient; - readonly #config: BtcAccountProviderConfig; - constructor( messenger: MultichainAccountServiceMessenger, config: BtcAccountProviderConfig = { @@ -46,11 +38,10 @@ export class BtcAccountProvider extends SnapAccountProvider { }, }, ) { - super(BtcAccountProvider.BTC_SNAP_ID, messenger); + super(BtcAccountProvider.BTC_SNAP_ID, messenger, config); this.#client = this.#getKeyringClientFromSnapId( BtcAccountProvider.BTC_SNAP_ID, ); - this.#config = config; } getName(): string { @@ -88,20 +79,22 @@ export class BtcAccountProvider extends SnapAccountProvider { entropySource: EntropySourceId; groupIndex: number; }): Promise[]> { - const createAccount = await this.getRestrictedSnapAccountCreator(); - - const account = await withTimeout( - createAccount({ - entropySource, - index, - addressType: BtcAccountType.P2wpkh, - scope: BtcScope.Mainnet, - }), - this.#config.createAccounts.timeoutMs, - ); - - assertIsBip44Account(account); - return [account]; + return this.withMaxConcurrency(async () => { + const createAccount = await this.getRestrictedSnapAccountCreator(); + + const account = await withTimeout( + createAccount({ + entropySource, + index, + addressType: BtcAccountType.P2wpkh, + scope: BtcScope.Mainnet, + }), + this.config.createAccounts.timeoutMs, + ); + + assertIsBip44Account(account); + return [account]; + }); } async discoverAccounts({ @@ -119,11 +112,11 @@ export class BtcAccountProvider extends SnapAccountProvider { entropySource, groupIndex, ), - this.#config.discovery.timeoutMs, + this.config.discovery.timeoutMs, ), { - maxAttempts: this.#config.discovery.maxAttempts, - backOffMs: this.#config.discovery.backOffMs, + maxAttempts: this.config.discovery.maxAttempts, + backOffMs: this.config.discovery.backOffMs, }, ); diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts index 50c5e256833..530d4e59093 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts @@ -10,7 +10,6 @@ import type { } from '@metamask/keyring-internal-api'; import type { Provider } from '@metamask/network-controller'; import { add0x, assert, bytesToHex, type Hex } from '@metamask/utils'; -import type { MultichainAccountServiceMessenger } from 'src/types'; import { assertAreBip44Accounts, @@ -18,6 +17,7 @@ import { BaseBip44AccountProvider, } from './BaseBip44AccountProvider'; import { withRetry, withTimeout } from './utils'; +import type { MultichainAccountServiceMessenger } from '../types'; const ETH_MAINNET_CHAIN_ID = '0x1'; diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts index 647421fac7e..2ac3c1d1734 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts @@ -1,7 +1,87 @@ -import { isSnapAccountProvider } from './SnapAccountProvider'; +import type { Bip44Account } from '@metamask/account-api'; +import type { EntropySourceId, KeyringAccount } from '@metamask/keyring-api'; +import type { SnapId } from '@metamask/snaps-sdk'; + +import { + isSnapAccountProvider, + SnapAccountProvider, +} from './SnapAccountProvider'; import { SolAccountProvider } from './SolAccountProvider'; +import { + getMultichainAccountServiceMessenger, + getRootMessenger, +} from '../tests'; import type { MultichainAccountServiceMessenger } from '../types'; +const THROTTLED_OPERATION_DELAY_MS = 10; +const TEST_SNAP_ID = 'npm:@metamask/test-snap' as SnapId; +const TEST_ENTROPY_SOURCE = 'test-entropy-source' as EntropySourceId; + +// Helper to create a tracked provider that monitors concurrent execution +const createTrackedProvider = (maxConcurrency: number) => { + const tracker: { + startLog: number[]; + endLog: number[]; + activeCount: number; + maxActiveCount: number; + } = { + startLog: [], + endLog: [], + activeCount: 0, + maxActiveCount: 0, + }; + + class TrackedProvider extends SnapAccountProvider { + getName(): string { + return 'Test Provider'; + } + + isAccountCompatible(): boolean { + return true; + } + + async discoverAccounts(): Promise[]> { + return []; + } + + async createAccounts(options: { + entropySource: EntropySourceId; + groupIndex: number; + }): Promise[]> { + return this.withMaxConcurrency(async () => { + tracker.startLog.push(options.groupIndex); + tracker.activeCount += 1; + tracker.maxActiveCount = Math.max( + tracker.maxActiveCount, + tracker.activeCount, + ); + await new Promise((resolve) => + setTimeout(resolve, THROTTLED_OPERATION_DELAY_MS), + ); + tracker.activeCount -= 1; + tracker.endLog.push(options.groupIndex); + return []; + }); + } + } + + const messenger = getMultichainAccountServiceMessenger(getRootMessenger()); + const config = { + maxConcurrency, + createAccounts: { + timeoutMs: 5000, + }, + discovery: { + timeoutMs: 2000, + maxAttempts: 3, + backOffMs: 1000, + }, + }; + const provider = new TrackedProvider(TEST_SNAP_ID, messenger, config); + + return { provider, tracker }; +}; + describe('SnapAccountProvider', () => { describe('isSnapAccountProvider', () => { it('returns false for plain object with snapId property', () => { @@ -47,4 +127,75 @@ describe('SnapAccountProvider', () => { expect(isSnapAccountProvider(solProvider)).toBe(true); }); }); + + describe('withMaxConcurrency', () => { + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + it('throttles createAccounts when maxConcurrency is finite', async () => { + const { provider, tracker } = createTrackedProvider(2); // Allow only 2 concurrent operations + + // Start 4 concurrent calls + const promises = [0, 1, 2, 3].map((index) => + provider.createAccounts({ + entropySource: TEST_ENTROPY_SOURCE, + groupIndex: index, + }), + ); + + await Promise.all(promises); + + // All operations should complete + expect(tracker.startLog).toHaveLength(4); + expect(tracker.endLog).toHaveLength(4); + + // With maxConcurrency=2, never more than 2 should run concurrently + expect(tracker.maxActiveCount).toBe(2); + + // First 2 should start immediately, next 2 should wait + expect(tracker.startLog.slice(0, 2).sort()).toStrictEqual([0, 1]); + }); + + it('does not throttle when maxConcurrency is Infinity', async () => { + const { provider, tracker } = createTrackedProvider(Infinity); // No throttling + + // Start 4 concurrent calls + const promises = [0, 1, 2, 3].map((index) => + provider.createAccounts({ + entropySource: TEST_ENTROPY_SOURCE, + groupIndex: index, + }), + ); + + await Promise.all(promises); + + // All 4 operations should complete + expect(tracker.startLog).toHaveLength(4); + + // With no throttling, all 4 should have been able to run concurrently + expect(tracker.maxActiveCount).toBe(4); + }); + + it('respects concurrency limit across multiple calls', async () => { + const { provider, tracker } = createTrackedProvider(1); // Only 1 concurrent operation + + // Start 3 concurrent calls + const promises = [0, 1, 2].map((index) => + provider.createAccounts({ + entropySource: TEST_ENTROPY_SOURCE, + groupIndex: index, + }), + ); + + await Promise.all(promises); + + // Verify all completed + expect(tracker.endLog).toHaveLength(3); + + // With maxConcurrency=1, never more than 1 should run at a time + expect(tracker.maxActiveCount).toBe(1); + }); + }); }); diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts index 6b1e814f9ca..a549600ee30 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts @@ -4,21 +4,70 @@ import type { EntropySourceId, KeyringAccount } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { Json, SnapId } from '@metamask/snaps-sdk'; -import type { MultichainAccountServiceMessenger } from 'src/types'; +import { Semaphore } from 'async-mutex'; import { BaseBip44AccountProvider } from './BaseBip44AccountProvider'; +import type { MultichainAccountServiceMessenger } from '../types'; export type RestrictedSnapKeyringCreateAccount = ( options: Record, ) => Promise; +export type SnapAccountProviderConfig = { + maxConcurrency?: number; + discovery: { + maxAttempts: number; + timeoutMs: number; + backOffMs: number; + }; + createAccounts: { + timeoutMs: number; + }; +}; + export abstract class SnapAccountProvider extends BaseBip44AccountProvider { readonly snapId: SnapId; - constructor(snapId: SnapId, messenger: MultichainAccountServiceMessenger) { + protected readonly config: SnapAccountProviderConfig; + + readonly #queue?: Semaphore; + + constructor( + snapId: SnapId, + messenger: MultichainAccountServiceMessenger, + config: SnapAccountProviderConfig, + ) { super(messenger); this.snapId = snapId; + + const maxConcurrency = config.maxConcurrency ?? Infinity; + this.config = { + ...config, + maxConcurrency, + }; + + // Create semaphore only if concurrency is limited + if (isFinite(maxConcurrency)) { + this.#queue = new Semaphore(maxConcurrency); + } + } + + /** + * Wraps an async operation with concurrency limiting based on maxConcurrency config. + * If maxConcurrency is Infinity (the default), the operation runs immediately without throttling. + * Otherwise, it's queued through the semaphore to respect the concurrency limit. + * + * @param operation - The async operation to execute. + * @returns The result of the operation. + */ + protected async withMaxConcurrency( + operation: () => Promise, + ): Promise { + if (this.#queue) { + return this.#queue.runExclusive(operation); + } + return operation(); } protected async getRestrictedSnapAccountCreator(): Promise { diff --git a/packages/multichain-account-service/src/providers/SolAccountProvider.ts b/packages/multichain-account-service/src/providers/SolAccountProvider.ts index 05a447757be..635104f5f4a 100644 --- a/packages/multichain-account-service/src/providers/SolAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SolAccountProvider.ts @@ -11,21 +11,15 @@ import { KeyringClient } from '@metamask/keyring-snap-client'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; import type { Json, JsonRpcRequest } from '@metamask/utils'; -import type { MultichainAccountServiceMessenger } from 'src/types'; -import { SnapAccountProvider } from './SnapAccountProvider'; +import { + SnapAccountProvider, + type SnapAccountProviderConfig, +} from './SnapAccountProvider'; import { withRetry, withTimeout } from './utils'; +import type { MultichainAccountServiceMessenger } from '../types'; -export type SolAccountProviderConfig = { - discovery: { - maxAttempts: number; - timeoutMs: number; - backOffMs: number; - }; - createAccounts: { - timeoutMs: number; - }; -}; +export type SolAccountProviderConfig = SnapAccountProviderConfig; export const SOL_ACCOUNT_PROVIDER_NAME = 'Solana' as const; @@ -36,11 +30,10 @@ export class SolAccountProvider extends SnapAccountProvider { readonly #client: KeyringClient; - readonly #config: SolAccountProviderConfig; - constructor( messenger: MultichainAccountServiceMessenger, config: SolAccountProviderConfig = { + maxConcurrency: 3, discovery: { timeoutMs: 2000, maxAttempts: 3, @@ -51,11 +44,10 @@ export class SolAccountProvider extends SnapAccountProvider { }, }, ) { - super(SolAccountProvider.SOLANA_SNAP_ID, messenger); + super(SolAccountProvider.SOLANA_SNAP_ID, messenger, config); this.#client = this.#getKeyringClientFromSnapId( SolAccountProvider.SOLANA_SNAP_ID, ); - this.#config = config; } getName(): string { @@ -98,7 +90,7 @@ export class SolAccountProvider extends SnapAccountProvider { const createAccount = await this.getRestrictedSnapAccountCreator(); const account = await withTimeout( createAccount({ entropySource, derivationPath }), - this.#config.createAccounts.timeoutMs, + this.config.createAccounts.timeoutMs, ); // Ensure entropy is present before type assertion validation @@ -120,14 +112,16 @@ export class SolAccountProvider extends SnapAccountProvider { entropySource: EntropySourceId; groupIndex: number; }): Promise[]> { - const derivationPath = `m/44'/501'/${groupIndex}'/0'`; - const account = await this.#createAccount({ - entropySource, - groupIndex, - derivationPath, + return this.withMaxConcurrency(async () => { + const derivationPath = `m/44'/501'/${groupIndex}'/0'`; + const account = await this.#createAccount({ + entropySource, + groupIndex, + derivationPath, + }); + + return [account]; }); - - return [account]; } async discoverAccounts({ @@ -145,11 +139,11 @@ export class SolAccountProvider extends SnapAccountProvider { entropySource, groupIndex, ), - this.#config.discovery.timeoutMs, + this.config.discovery.timeoutMs, ), { - maxAttempts: this.#config.discovery.maxAttempts, - backOffMs: this.#config.discovery.backOffMs, + maxAttempts: this.config.discovery.maxAttempts, + backOffMs: this.config.discovery.backOffMs, }, ); diff --git a/packages/multichain-account-service/src/providers/TrxAccountProvider.ts b/packages/multichain-account-service/src/providers/TrxAccountProvider.ts index d62e2c45096..c2c1f660be1 100644 --- a/packages/multichain-account-service/src/providers/TrxAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/TrxAccountProvider.ts @@ -7,21 +7,15 @@ import { KeyringClient } from '@metamask/keyring-snap-client'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; import type { Json, JsonRpcRequest } from '@metamask/utils'; -import type { MultichainAccountServiceMessenger } from 'src/types'; -import { SnapAccountProvider } from './SnapAccountProvider'; +import { + SnapAccountProvider, + type SnapAccountProviderConfig, +} from './SnapAccountProvider'; import { withRetry, withTimeout } from './utils'; +import type { MultichainAccountServiceMessenger } from '../types'; -export type TrxAccountProviderConfig = { - discovery: { - maxAttempts: number; - timeoutMs: number; - backOffMs: number; - }; - createAccounts: { - timeoutMs: number; - }; -}; +export type TrxAccountProviderConfig = SnapAccountProviderConfig; export const TRX_ACCOUNT_PROVIDER_NAME = 'Tron' as const; @@ -32,8 +26,6 @@ export class TrxAccountProvider extends SnapAccountProvider { readonly #client: KeyringClient; - readonly #config: TrxAccountProviderConfig; - constructor( messenger: MultichainAccountServiceMessenger, config: TrxAccountProviderConfig = { @@ -47,11 +39,10 @@ export class TrxAccountProvider extends SnapAccountProvider { }, }, ) { - super(TrxAccountProvider.TRX_SNAP_ID, messenger); + super(TrxAccountProvider.TRX_SNAP_ID, messenger, config); this.#client = this.#getKeyringClientFromSnapId( TrxAccountProvider.TRX_SNAP_ID, ); - this.#config = config; } getName(): string { @@ -89,20 +80,22 @@ export class TrxAccountProvider extends SnapAccountProvider { entropySource: EntropySourceId; groupIndex: number; }): Promise[]> { - const createAccount = await this.getRestrictedSnapAccountCreator(); - - const account = await withTimeout( - createAccount({ - entropySource, - index, - addressType: TrxAccountType.Eoa, - scope: TrxScope.Mainnet, - }), - this.#config.createAccounts.timeoutMs, - ); - - assertIsBip44Account(account); - return [account]; + return this.withMaxConcurrency(async () => { + const createAccount = await this.getRestrictedSnapAccountCreator(); + + const account = await withTimeout( + createAccount({ + entropySource, + index, + addressType: TrxAccountType.Eoa, + scope: TrxScope.Mainnet, + }), + this.config.createAccounts.timeoutMs, + ); + + assertIsBip44Account(account); + return [account]; + }); } async discoverAccounts({ @@ -120,11 +113,11 @@ export class TrxAccountProvider extends SnapAccountProvider { entropySource, groupIndex, ), - this.#config.discovery.timeoutMs, + this.config.discovery.timeoutMs, ), { - maxAttempts: this.#config.discovery.maxAttempts, - backOffMs: this.#config.discovery.backOffMs, + maxAttempts: this.config.discovery.maxAttempts, + backOffMs: this.config.discovery.backOffMs, }, ); diff --git a/packages/multichain-account-service/src/utils.ts b/packages/multichain-account-service/src/utils.ts new file mode 100644 index 00000000000..b7fa12338cd --- /dev/null +++ b/packages/multichain-account-service/src/utils.ts @@ -0,0 +1,12 @@ +export const toRejectedErrorMessage = ( + prefix: string, + results: PromiseSettledResult[], +) => { + let errorMessage = `${prefix}:`; + for (const r of results) { + if (r.status === 'rejected') { + errorMessage += `\n- ${r.reason}`; + } + } + return errorMessage; +};