UNPKG

@metamask/multichain-account-service

Version:
331 lines 17.9 kB
"use strict"; 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 __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 _SnapAccountProvider_instances, _SnapAccountProvider_client, _SnapAccountProvider_sender, _SnapAccountProvider_queue, _SnapAccountProvider_trace, _SnapAccountProvider_createSender, _SnapAccountProvider_resolveClient, _SnapAccountProvider_withSnapKeyring; Object.defineProperty(exports, "__esModule", { value: true }); exports.isSnapAccountProvider = exports.SnapAccountProvider = void 0; const account_api_1 = require("@metamask/account-api"); const v2_1 = require("@metamask/eth-snap-keyring/v2"); const keyring_api_1 = require("@metamask/keyring-api"); const snaps_utils_1 = require("@metamask/snaps-utils"); const async_mutex_1 = require("async-mutex"); const analytics_1 = require("../analytics/index.cjs"); const errors_1 = require("../errors.cjs"); const logger_1 = require("../logger.cjs"); const BaseBip44AccountProvider_1 = require("./BaseBip44AccountProvider.cjs"); const SnapKeyringClient_1 = require("./SnapKeyringClient.cjs"); const utils_1 = require("./utils.cjs"); class SnapAccountProvider extends BaseBip44AccountProvider_1.BaseBip44AccountProvider { constructor(snapId, messenger, config, /* istanbul ignore next */ trace = analytics_1.traceFallback) { super(messenger); _SnapAccountProvider_instances.add(this); /** * The Snap's keyring capabilities, sourced from `SnapAccountService` (which * reads them from the Snap's manifest). Populated the first time the client * is resolved; defaults to an empty capability set until then. */ this.capabilities = v2_1.EMPTY_CAPABILITIES; /** * Version-agnostic keyring client, resolved lazily once the Snap is ready and * its capabilities are known — see {@link SnapAccountProvider.withSnap}. */ _SnapAccountProvider_client.set(this, void 0); _SnapAccountProvider_sender.set(this, void 0); _SnapAccountProvider_queue.set(this, void 0); _SnapAccountProvider_trace.set(this, void 0); this.snapId = snapId; __classPrivateFieldSet(this, _SnapAccountProvider_sender, __classPrivateFieldGet(this, _SnapAccountProvider_instances, "m", _SnapAccountProvider_createSender).call(this, snapId), "f"); const maxConcurrency = config.maxConcurrency ?? Infinity; this.config = { ...config, discovery: { ...config.discovery, enabled: config.discovery.enabled ?? true, }, maxConcurrency, }; // Create semaphore only if concurrency is limited if (isFinite(maxConcurrency)) { __classPrivateFieldSet(this, _SnapAccountProvider_queue, new async_mutex_1.Semaphore(maxConcurrency), "f"); } __classPrivateFieldSet(this, _SnapAccountProvider_trace, trace, "f"); } /** * Ensures that the Snap is ready to be used. * * Once this resolves, a Snap keyring for {@link snapId} is guaranteed to * exist in the `KeyringController`, so subsequent {@link #withSnapKeyring} * calls will not fail with "No keyring matches the selector". * * @returns A promise that resolves when the Snap is ready. * @throws An error if the Snap could not become ready. */ async ensureReady() { return this.messenger.call('SnapAccountService:ensureReady', this.snapId); } /** * 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. */ async withMaxConcurrency(operation) { if (__classPrivateFieldGet(this, _SnapAccountProvider_queue, "f")) { return __classPrivateFieldGet(this, _SnapAccountProvider_queue, "f").runExclusive(operation); } return operation(); } async trace(request, fn) { return __classPrivateFieldGet(this, _SnapAccountProvider_trace, "f").call(this, request, fn); } /** * Whether the Snap supports the v2 keyring protocol, inferred from its * declared capabilities (a v2-capable Snap declares BIP-44 capabilities). * * @returns `true` if the Snap is v2-capable. */ isV2() { return Boolean(this.capabilities.bip44); } async resyncAccounts(accounts) { await this.withSnap(async ({ client, keyring }) => { const localSnapAccounts = accounts.filter((account) => account.metadata.snap?.id === this.snapId); const snapAccounts = new Set((await client.getAccounts()).map((account) => account.id)); // NOTE: This should never happen, but if it does, we recover by deleting the // extra accounts from the Snap to bring it back in sync with MetaMask. if (localSnapAccounts.length < snapAccounts.size) { const autoRemoveExtraSnapAccounts = this.config.resyncAccounts?.autoRemoveExtraSnapAccounts ?? true; if (autoRemoveExtraSnapAccounts) { // Build a set of local account IDs for quick lookup const localAccountIds = new Set(localSnapAccounts.map((account) => account.id)); // Find and delete accounts that exist in Snap but not in MetaMask await Promise.all([...snapAccounts].map(async (snapAccountId) => { try { if (!localAccountIds.has(snapAccountId)) { // This account exists in the Snap but not in MetaMask, delete it from // the Snap. await client.deleteAccount(snapAccountId); // Update the local Set so subsequent checks use the correct size snapAccounts.delete(snapAccountId); } } catch (error) { (0, errors_1.reportError)(this.messenger, `Unable to delete de-synced Snap account: ${this.snapId}`, error, { provider: this.getName(), snapAccountId, }); } })); } else { const message = `Snap "${this.snapId}" has de-synced accounts, Snap has more accounts than MetaMask! (${localSnapAccounts.length} < ${snapAccounts.size})`; (0, logger_1.projectLogger)(`${logger_1.WARNING_PREFIX} ${message}`); console.warn(message); return; } } // We want this part to be fast, so we only check for sizes, but we might need // to make a real "diff" between the 2 states to not miss any de-sync. if (localSnapAccounts.length > snapAccounts.size) { // We always use the MetaMask list as the main reference here. await Promise.all(localSnapAccounts.map(async (account) => { const { id: entropySource, groupIndex } = account.options.entropy; try { if (!snapAccounts.has(account.id)) { // We still need to remove the accounts from the Snap keyring since we're // about to create the same account again, which will use a new ID, but will // keep using the same address, and the Snap keyring does not allow this. await keyring.deleteAccount(account.id); // The Snap has no account in its state for this one, we re-create it. await this.createAccounts({ type: keyring_api_1.AccountCreationType.Bip44DeriveIndex, entropySource, groupIndex, }); } } catch (error) { (0, errors_1.reportError)(this.messenger, 'Unable to re-sync accounts', error, { provider: this.getName(), groupIndex, }); } })); } }); } async withSnap(operation) { await this.ensureReady(); const client = await __classPrivateFieldGet(this, _SnapAccountProvider_instances, "m", _SnapAccountProvider_resolveClient).call(this); const keyring = { createAccounts: (options) => __classPrivateFieldGet(this, _SnapAccountProvider_instances, "m", _SnapAccountProvider_withSnapKeyring).call(this, ({ keyring: snapKeyring }) => snapKeyring.createAccounts(options)), deleteAccount: (id) => __classPrivateFieldGet(this, _SnapAccountProvider_instances, "m", _SnapAccountProvider_withSnapKeyring).call(this, ({ keyring: snapKeyring }) => snapKeyring.deleteAccount(id)), }; return await operation({ client, keyring }); } toBip44Account(account, _options) { (0, account_api_1.assertIsBip44Account)(account); return account; } async createBip44Accounts(keyring, options) { return this.withMaxConcurrency(async () => { const { entropySource } = options; const snapAccounts = await (0, utils_1.withTimeout)(() => this.trace({ name: analytics_1.TraceName.ProviderCreateAccounts, data: { provider: this.getName(), ...(0, analytics_1.toCreateAccountsV2DataTraces)(options), }, }, () => keyring.createAccounts(options)), this.config.createAccounts.timeoutMs); const groupIndexOffset = options.type === `${keyring_api_1.AccountCreationType.Bip44DeriveIndexRange}` ? options.range.from : options.groupIndex; return snapAccounts.map((snapAccount, index) => { const groupIndex = groupIndexOffset + index; const account = this.toBip44Account(snapAccount, { entropySource, groupIndex, }); this.accounts.add(snapAccount.id); return account; }); }); } async createAccounts(options) { (0, keyring_api_1.assertCreateAccountOptionIsSupported)(options, [ `${keyring_api_1.AccountCreationType.Bip44DeriveIndex}`, `${keyring_api_1.AccountCreationType.Bip44DeriveIndexRange}`, ]); return this.withSnap(async ({ keyring }) => this.createBip44Accounts(keyring, options)); } /** * Delete a snap account by id. * * Resolves the account's address from the tracked account, then forwards to * the legacy `SnapKeyring.removeAccount(address)`. The Snap keyring takes * care of notifying the snap to clean up its own state through the normal * account-removal flow (same path used by `resyncAccounts`). * * @param id - The id of the account to delete. */ async deleteAccount(id) { const account = this.getAccount(id); await __classPrivateFieldGet(this, _SnapAccountProvider_instances, "m", _SnapAccountProvider_withSnapKeyring).call(this, async ({ keyring }) => { await keyring.deleteAccount(account.id); }); this.accounts.delete(id); } /** * Discovers accounts for the given entropy source and group index. * * v2 Snaps drive discovery through `createAccounts({ bip44:discover })`: the * Snap checks for on-chain activity (using its own supported scopes) and * returns the created account(s), or nothing once discovery is exhausted. * * v1 Snaps use the client's `discoverAccounts` to detect activity on * {@link v1DiscoveryScopes}, then create the account for the group index. * * @param options - The discovery options. * @param options.entropySource - The entropy source to discover accounts for. * @param options.groupIndex - The group index to discover accounts for. * @returns The discovered (and created) accounts, or an empty array when * there is nothing to discover at this group index. */ async discoverAccounts({ entropySource, groupIndex, }) { return this.withSnap(async ({ client, keyring }) => this.trace({ name: analytics_1.TraceName.SnapDiscoverAccounts, data: { provider: this.getName(), }, }, async () => { if (!this.config.discovery.enabled) { return []; } if (this.isV2()) { // The v2 client has no `discoverAccounts`, so discovery is only // possible when the Snap supports `bip44:discover`. Otherwise there // is no way to discover and we report nothing. if (!this.capabilities.bip44?.discover) { return []; } // v2: the Snap detects on-chain activity and creates the account in // a single `createAccounts({ bip44:discover })` call. An empty // result means discovery is exhausted at this group index. return this.createBip44Accounts(keyring, { type: keyring_api_1.AccountCreationType.Bip44Discover, entropySource, groupIndex, }); } // v1: detect activity via the client, then create the account for // this group index. const discoveredAccounts = await (0, utils_1.withRetry)(() => (0, utils_1.withTimeout)(() => client.discoverAccounts(this.v1DiscoveryScopes, entropySource, groupIndex), this.config.discovery.timeoutMs), { maxAttempts: this.config.discovery.maxAttempts, backOffMs: this.config.discovery.backOffMs, }); if (!discoveredAccounts.length) { return []; } return this.createBip44Accounts(keyring, { type: keyring_api_1.AccountCreationType.Bip44DeriveIndex, entropySource, groupIndex, }); })); } } exports.SnapAccountProvider = SnapAccountProvider; _SnapAccountProvider_client = new WeakMap(), _SnapAccountProvider_sender = new WeakMap(), _SnapAccountProvider_queue = new WeakMap(), _SnapAccountProvider_trace = new WeakMap(), _SnapAccountProvider_instances = new WeakSet(), _SnapAccountProvider_createSender = function _SnapAccountProvider_createSender(snapId) { return { send: async (request) => { const response = await this.messenger.call('SnapController:handleRequest', { snapId: snapId, origin: 'metamask', handler: snaps_utils_1.HandlerType.OnKeyringRequest, request, }); return response; }, }; }, _SnapAccountProvider_resolveClient = /** * Resolves the version-agnostic keyring client, fetching the Snap's * capabilities from `SnapAccountService` on first use and caching both the * capabilities and the resulting client. * * Callers must ensure the Snap is ready (via * {@link SnapAccountProvider.ensureReady}) beforehand so that the * capabilities are reliably populated — {@link SnapAccountProvider.withSnap} * guarantees this ordering. * * @returns The resolved {@link SnapKeyringClient}. */ async function _SnapAccountProvider_resolveClient() { if (!__classPrivateFieldGet(this, _SnapAccountProvider_client, "f")) { this.capabilities = await this.messenger.call('SnapAccountService:getCapabilities', this.snapId); __classPrivateFieldSet(this, _SnapAccountProvider_client, (0, SnapKeyringClient_1.createSnapKeyringClient)(__classPrivateFieldGet(this, _SnapAccountProvider_sender, "f"), this.isV2()), "f"); } return __classPrivateFieldGet(this, _SnapAccountProvider_client, "f"); }, _SnapAccountProvider_withSnapKeyring = async function _SnapAccountProvider_withSnapKeyring(operation) { return this.withKeyringV2({ filter: (keyring) => (0, v2_1.isSnapKeyring)(keyring) && keyring.snapId === this.snapId, }, (args) => operation(args)); }; const isSnapAccountProvider = (provider) => { return provider instanceof SnapAccountProvider; }; exports.isSnapAccountProvider = isSnapAccountProvider; //# sourceMappingURL=SnapAccountProvider.cjs.map