UNPKG

@metamask/multichain-account-service

Version:
489 lines 28.3 kB
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _MultichainAccountService_instances, _MultichainAccountService_messenger, _MultichainAccountService_providers, _MultichainAccountService_trace, _MultichainAccountService_wallets, _MultichainAccountService_getStateKeys, _MultichainAccountService_constructServiceState, _MultichainAccountService_getWallet, _MultichainAccountService_getPrimaryEntropySourceId, _MultichainAccountService_createWalletByImport, _MultichainAccountService_createWalletByNewVault, _MultichainAccountService_createWalletByRestore; import { isBip44Account, toMultichainAccountWalletId } from "@metamask/account-api"; import { KeyringTypes } from "@metamask/keyring-controller"; import { areUint8ArraysEqual, assert } from "@metamask/utils"; import { traceFallback } from "./analytics/index.mjs"; import { isPerfEnabled, withLocalPerfTrace } from "./analytics/perf.mjs"; import { reportError } from "./errors.mjs"; import { projectLogger as log } from "./logger.mjs"; import { MultichainAccountWallet } from "./MultichainAccountWallet.mjs"; import { EVM_ACCOUNT_PROVIDER_NAME, BTC_ACCOUNT_PROVIDER_NAME, TRX_ACCOUNT_PROVIDER_NAME, BtcAccountProvider, TrxAccountProvider } from "./providers/index.mjs"; import { AccountProviderWrapper, isAccountProviderWrapper } from "./providers/AccountProviderWrapper.mjs"; import { EvmAccountProvider } from "./providers/EvmAccountProvider.mjs"; import { SolAccountProvider } from "./providers/SolAccountProvider.mjs"; import { SOL_ACCOUNT_PROVIDER_NAME } from "./providers/SolAccountProvider.mjs"; import { toErrorMessage } from "./utils.mjs"; export const serviceName = 'MultichainAccountService'; const MESSENGER_EXPOSED_METHODS = [ 'getMultichainAccountGroup', 'getMultichainAccountGroups', 'getMultichainAccountWallet', 'getMultichainAccountWallets', 'createNextMultichainAccountGroup', 'createMultichainAccountGroup', 'createMultichainAccountGroups', 'setBasicFunctionality', 'alignWallets', 'alignWallet', 'createMultichainAccountWallet', 'resyncAccounts', 'removeMultichainAccountWallet', 'init', ]; /** * Service to expose multichain accounts capabilities. */ export class MultichainAccountService { /** * Constructs a new MultichainAccountService. * * @param options - The options. * @param options.messenger - The messenger suited to this * MultichainAccountService. * @param options.providers - Optional list of account * @param options.providerConfigs - Optional provider configs * @param options.config - Optional config. */ constructor({ messenger, providers = [], providerConfigs, config, }) { _MultichainAccountService_instances.add(this); _MultichainAccountService_messenger.set(this, void 0); _MultichainAccountService_providers.set(this, void 0); _MultichainAccountService_trace.set(this, void 0); _MultichainAccountService_wallets.set(this, void 0); /** * The name of the service. */ this.name = serviceName; __classPrivateFieldSet(this, _MultichainAccountService_messenger, messenger, "f"); __classPrivateFieldSet(this, _MultichainAccountService_wallets, new Map(), "f"); // Pass trace callback directly to preserve original 'this' context. // This avoids binding the callback to the MultichainAccountService instance. let trace = config?.trace ?? traceFallback; // Wrap the trace callback with local performance tracing if performance logging is enabled. if (isPerfEnabled()) { trace = withLocalPerfTrace(trace); } // This trace is passed down to wallets and providers to be used for tracing operations within them. __classPrivateFieldSet(this, _MultichainAccountService_trace, trace, "f"); // TODO: Rely on keyring capabilities once the keyring API is used by all keyrings. __classPrivateFieldSet(this, _MultichainAccountService_providers, [ new EvmAccountProvider(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), providerConfigs?.[EVM_ACCOUNT_PROVIDER_NAME], trace), new AccountProviderWrapper(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), new SolAccountProvider(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), providerConfigs?.[SOL_ACCOUNT_PROVIDER_NAME], trace)), new AccountProviderWrapper(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), new BtcAccountProvider(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), providerConfigs?.[BTC_ACCOUNT_PROVIDER_NAME], trace)), new AccountProviderWrapper(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), new TrxAccountProvider(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), providerConfigs?.[TRX_ACCOUNT_PROVIDER_NAME], trace)), // Custom account providers that can be provided by the MetaMask client. ...providers, ], "f"); __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f").registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS); } /** * Initialize the service and constructs the internal reprensentation of * multichain accounts and wallets. */ async init() { log('Initializing...'); __classPrivateFieldGet(this, _MultichainAccountService_wallets, "f").clear(); const { serviceState, providerState } = __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_constructServiceState).call(this); for (const provider of __classPrivateFieldGet(this, _MultichainAccountService_providers, "f")) { const providerName = provider.getName(); // Initialize providers even if there are no accounts yet. // Passing an empty array ensures providers start in a valid state. const state = providerState[providerName] ?? []; provider.init(state); } for (const entropySource of Object.keys(serviceState)) { const wallet = new MultichainAccountWallet({ entropySource, providers: __classPrivateFieldGet(this, _MultichainAccountService_providers, "f"), messenger: __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), trace: __classPrivateFieldGet(this, _MultichainAccountService_trace, "f"), }); wallet.init(serviceState[entropySource]); __classPrivateFieldGet(this, _MultichainAccountService_wallets, "f").set(wallet.id, wallet); } log('Initialized'); } /** * Re-synchronize MetaMask accounts and the providers accounts if needed. * * NOTE: This is mostly required if one of the providers (keyrings or Snaps) * have different sets of accounts. This method would ensure that both are * in-sync and use the same accounts (and same IDs). * * READ THIS CAREFULLY (State inconsistency bugs/de-sync) * We've seen some problems were keyring accounts on some Snaps were not synchronized * with the accounts on MM side. This causes problems where we cannot interact with * those accounts because the Snap does know about them. * To "workaround" this de-sync problem for now, we make sure that both parties are * in-sync when the service boots up. * ---------------------------------------------------------------------------------- */ async resyncAccounts() { log('Re-sync provider accounts if needed...'); const accounts = __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f") .call('AccountsController:listMultichainAccounts') .filter(isBip44Account); // We use `Promise.all` + `try-catch` combo, since we don't wanna block the wallet // from being used even if some accounts are not sync (best-effort). await Promise.all(__classPrivateFieldGet(this, _MultichainAccountService_providers, "f").map(async (provider) => { try { await provider.resyncAccounts(accounts); } catch (error) { reportError(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), `Unable to re-sync provider "${provider.getName()}"`, error, { provider: provider.getName(), }); } })); log('Providers got re-synced!'); } /** * Gets a reference to the multichain account wallet matching this entropy source. * * @param options - Options. * @param options.entropySource - The entropy source of the multichain account. * @throws If none multichain account match this entropy. * @returns A reference to the multichain account wallet. */ getMultichainAccountWallet({ entropySource, }) { return __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getWallet).call(this, entropySource); } /** * Gets an array of all multichain account wallets. * * @returns An array of all multichain account wallets. */ getMultichainAccountWallets() { return Array.from(__classPrivateFieldGet(this, _MultichainAccountService_wallets, "f").values()); } /** * Creates a new multichain account wallet by either importing an existing mnemonic, * creating a new vault and keychain, or restoring a vault and keyring. * * NOTE: This method should only be called in client code where a mutex lock is acquired. * `discoverAccounts` should be called after this method to discover and create accounts. * * @param params - The parameters to use to create the new wallet. * @param params.mnemonic - The mnemonic to use to create the new wallet. * @param params.password - The password to encrypt the vault with. * @param params.type - The flow type to use to create the new wallet. * @throws If the mnemonic has already been imported. * @returns The new multichain account wallet. */ async createMultichainAccountWallet(params) { let wallet; if (params.type === 'import') { wallet = await __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_createWalletByImport).call(this, params.mnemonic); } else if (params.type === 'create') { wallet = await __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_createWalletByNewVault).call(this, params.password); } else if (params.type === 'restore') { wallet = await __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_createWalletByRestore).call(this, params.password, params.mnemonic); } assert(wallet, 'Failed to create wallet.'); wallet.init({}); // READ THIS CAREFULLY: // We do not await for non-EVM account creations as they // are depending on the Snap platform to be ready (which is, waiting for onboarding to be completed). // Awaiting for this might cause a deadlock otherwise (during onboarding at least). await wallet.createMultichainAccountGroup(0, { waitForAllProvidersToFinishCreatingAccounts: false, }); __classPrivateFieldGet(this, _MultichainAccountService_wallets, "f").set(wallet.id, wallet); log(`Wallet created: [${wallet.id}]`); return wallet; } /** * Removes a multichain account wallet, deleting all of its accounts across * every registered provider (EVM and snap-based). * * The deletion iterates providers (the source of truth for their own * account lists) and filters each provider's accounts to those matching * the wallet's entropy source. Cleanup is best-effort end-to-end: neither * a single account deletion failure nor a failure to enumerate a given * provider's accounts aborts cleanup of the remaining providers. If one or * more operations fail, a single aggregated error is reported via * `reportError` with all per-failure details in its context. The wallet is * always removed from the service's internal map at the end. * * @param entropySource - The entropy source of the multichain account wallet. */ async removeMultichainAccountWallet(entropySource) { const wallet = __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getWallet).call(this, entropySource); const failures = []; for (const provider of __classPrivateFieldGet(this, _MultichainAccountService_providers, "f")) { // Enumerating a provider's owned accounts can itself throw (e.g. // `unwrap()`, `getAccounts()`, or reading account options). Catch it as // a provider-level failure and move on so one bad provider does not // abort cleanup of the others or skip the always-remove step below. let owned; try { // For wrapped providers, enumerate via the underlying provider so we // also see accounts when the wrapper has been disabled (i.e. basic // functionality is off). The wrapper's `deleteAccount` itself forwards // unconditionally, but its `getAccounts()` returns `[]` when disabled, // which would otherwise leave snap-backed accounts orphaned in their // underlying keyrings. const source = isAccountProviderWrapper(provider) ? provider.unwrap() : provider; owned = source .getAccounts() .filter((account) => account.options.entropy.id === entropySource); } catch (error) { failures.push({ provider: provider.getName(), error, }); continue; } for (const account of owned) { try { await provider.deleteAccount(account.id); } catch (error) { failures.push({ provider: provider.getName(), accountId: account.id, error, }); } } } if (failures.length > 0) { // One aggregated report per wallet-removal action: keeps the Sentry // message stable for grouping while still surfacing every per-account // failure in `context`. The shape is pinned by // `RemoveMultichainAccountWalletFailureContext`. const context = { failures: failures.map(({ provider, accountId, error }) => ({ provider, accountId, error: toErrorMessage(error), })), }; reportError(__classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), `Failed to delete one or more accounts during wallet removal`, new Error('Wallet removal partially failed'), context); } __classPrivateFieldGet(this, _MultichainAccountService_wallets, "f").delete(wallet.id); } /** * Gets a reference to the multichain account group matching this entropy source * and a group index. * * @param options - Options. * @param options.entropySource - The entropy source of the multichain account. * @param options.groupIndex - The group index of the multichain account. * @throws If none multichain account match this entropy source and group index. * @returns A reference to the multichain account. */ getMultichainAccountGroup({ entropySource, groupIndex, }) { const multichainAccount = __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getWallet).call(this, entropySource).getMultichainAccountGroup(groupIndex); if (!multichainAccount) { throw new Error(`No multichain account for index: ${groupIndex}`); } return multichainAccount; } /** * Gets all multichain account groups for a given entropy source. * * @param options - Options. * @param options.entropySource - The entropy source to query. * @throws If no multichain accounts match this entropy source. * @returns A list of all multichain accounts. */ getMultichainAccountGroups({ entropySource, }) { return __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getWallet).call(this, entropySource).getMultichainAccountGroups(); } /** * Creates the next multichain account group. * * @param options - Options. * @param options.entropySource - The wallet's entropy source. * @returns The next multichain account group. */ async createNextMultichainAccountGroup({ entropySource, }) { return await __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getWallet).call(this, entropySource).createNextMultichainAccountGroup(); } /** * Creates a multichain account group. * * @param options - Options. * @param options.groupIndex - The group index to use. * @param options.entropySource - The wallet's entropy source. * @returns The multichain account group for this group index. */ async createMultichainAccountGroup({ groupIndex, entropySource, }) { return await __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getWallet).call(this, entropySource).createMultichainAccountGroup(groupIndex); } /** * Creates multiple multichain account groups up to maxGroupIndex. * * @param params - Parameters for creating account groups. * @param params.fromGroupIndex - Starting group index to create (inclusive) (defaults to 0). * @param params.toGroupIndex - Maximum group index to create (inclusive). * @param params.entropySource - The entropy source ID. * @returns Array of created multichain account groups. */ async createMultichainAccountGroups({ fromGroupIndex = 0, toGroupIndex, entropySource, }) { return await __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getWallet).call(this, entropySource).createMultichainAccountGroups({ from: fromGroupIndex, to: toGroupIndex }, { waitForAllProvidersToFinishCreatingAccounts: false }); } /** * Set basic functionality state and trigger alignment if enabled. * When basic functionality is disabled, snap-based providers are disabled. * When enabled, all snap providers are enabled and wallet alignment is triggered. * EVM providers are never disabled as they're required for basic wallet functionality. * * @param enabled - Whether basic functionality is enabled. */ async setBasicFunctionality(enabled) { log(`Turning basic functionality: ${enabled ? 'ON' : 'OFF'}`); // Loop through providers and enable/disable only wrapped ones when basic functionality changes for (const provider of __classPrivateFieldGet(this, _MultichainAccountService_providers, "f")) { if (isAccountProviderWrapper(provider)) { log(`${enabled ? 'Enabling' : 'Disabling'} account provider: "${provider.getName()}"`); provider.setEnabled(enabled); } // Regular providers (like EVM) are never disabled for basic functionality } // Trigger alignment only when basic functionality is enabled if (enabled) { await this.alignWallets(); } } /** * Align all multichain account wallets. */ async alignWallets() { log(`Triggering alignment on all wallets...`); const wallets = this.getMultichainAccountWallets(); await Promise.all(wallets.map((w) => w.alignAccounts())); log(`Wallets aligned`); } /** * Align a specific multichain account wallet. * * @param entropySource - The entropy source of the multichain account wallet. */ async alignWallet(entropySource) { const wallet = this.getMultichainAccountWallet({ entropySource }); log(`Triggering alignment for wallet: [${wallet.id}]`); await wallet.alignAccounts(); log(`Wallet [${wallet.id}] aligned`); } } _MultichainAccountService_messenger = new WeakMap(), _MultichainAccountService_providers = new WeakMap(), _MultichainAccountService_trace = new WeakMap(), _MultichainAccountService_wallets = new WeakMap(), _MultichainAccountService_instances = new WeakSet(), _MultichainAccountService_getStateKeys = function _MultichainAccountService_getStateKeys(account) { for (const provider of __classPrivateFieldGet(this, _MultichainAccountService_providers, "f")) { if (isBip44Account(account) && provider.isAccountCompatible(account)) { return { entropySource: account.options.entropy.id, groupIndex: account.options.entropy.groupIndex, providerName: provider.getName(), }; } } return null; }, _MultichainAccountService_constructServiceState = function _MultichainAccountService_constructServiceState() { var _a, _b; const accounts = __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f").call('AccountsController:listMultichainAccounts'); const serviceState = {}; const providerState = {}; for (const account of accounts) { const keys = __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getStateKeys).call(this, account); if (keys) { const { entropySource, groupIndex, providerName } = keys; serviceState[entropySource] ?? (serviceState[entropySource] = {}); (_a = serviceState[entropySource])[groupIndex] ?? (_a[groupIndex] = {}); (_b = serviceState[entropySource][groupIndex])[providerName] ?? (_b[providerName] = []); serviceState[entropySource][groupIndex][providerName].push(account.id); providerState[providerName] ?? (providerState[providerName] = []); providerState[providerName].push(account.id); } } return { serviceState, providerState }; }, _MultichainAccountService_getWallet = function _MultichainAccountService_getWallet(entropySource) { const wallet = __classPrivateFieldGet(this, _MultichainAccountService_wallets, "f").get(toMultichainAccountWalletId(entropySource)); if (!wallet) { throw new Error('Unknown wallet, no wallet matching this entropy source'); } return wallet; }, _MultichainAccountService_getPrimaryEntropySourceId = function _MultichainAccountService_getPrimaryEntropySourceId() { const { keyrings } = __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f").call('KeyringController:getState'); const primaryKeyring = keyrings.find((keyring) => keyring.type === KeyringTypes.hd); assert(primaryKeyring, 'Primary keyring not found'); return primaryKeyring.metadata.id; }, _MultichainAccountService_createWalletByImport = /** * Creates a new multichain account wallet by importing an existing mnemonic. * * @param mnemonic - The mnemonic to use to create the new wallet. * @returns The new multichain account wallet. */ async function _MultichainAccountService_createWalletByImport(mnemonic) { log(`Creating new wallet by importing an existing mnemonic...`); const existingKeyrings = __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f").call('KeyringController:getKeyringsByType', KeyringTypes.hd); const alreadyHasImportedSrp = existingKeyrings.some((keyring) => { if (!keyring.mnemonic) { return false; } return areUint8ArraysEqual(keyring.mnemonic, mnemonic); }); if (alreadyHasImportedSrp) { throw new Error('This Secret Recovery Phrase has already been imported.'); } const result = await __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f").call('KeyringController:addNewKeyring', KeyringTypes.hd, { mnemonic, numberOfAccounts: 1 }); return new MultichainAccountWallet({ providers: __classPrivateFieldGet(this, _MultichainAccountService_providers, "f"), entropySource: result.id, messenger: __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), trace: __classPrivateFieldGet(this, _MultichainAccountService_trace, "f"), }); }, _MultichainAccountService_createWalletByNewVault = /** * Creates a new multichain account wallet by creating a new vault and keychain. * * @param password - The password to encrypt the vault with. * @returns The new multichain account wallet. */ async function _MultichainAccountService_createWalletByNewVault(password) { log(`Creating new wallet by creating a new vault and keychain...`); await __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f").call('KeyringController:createNewVaultAndKeychain', password); const entropySourceId = __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getPrimaryEntropySourceId).call(this); return new MultichainAccountWallet({ providers: __classPrivateFieldGet(this, _MultichainAccountService_providers, "f"), entropySource: entropySourceId, messenger: __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), trace: __classPrivateFieldGet(this, _MultichainAccountService_trace, "f"), }); }, _MultichainAccountService_createWalletByRestore = /** * Creates a new multichain account wallet by restoring a vault and keyring. * * @param password - The password to encrypt the vault with. * @param mnemonic - The mnemonic to use to restore the new wallet. * @returns The new multichain account wallet. */ async function _MultichainAccountService_createWalletByRestore(password, mnemonic) { log(`Creating new wallet by restoring vault and keyring...`); await __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f").call('KeyringController:createNewVaultAndRestore', password, mnemonic); const entropySourceId = __classPrivateFieldGet(this, _MultichainAccountService_instances, "m", _MultichainAccountService_getPrimaryEntropySourceId).call(this); return new MultichainAccountWallet({ providers: __classPrivateFieldGet(this, _MultichainAccountService_providers, "f"), entropySource: entropySourceId, messenger: __classPrivateFieldGet(this, _MultichainAccountService_messenger, "f"), trace: __classPrivateFieldGet(this, _MultichainAccountService_trace, "f"), }); }; //# sourceMappingURL=MultichainAccountService.mjs.map