UNPKG

@metamask/multichain-account-service

Version:
303 lines 15.3 kB
"use strict"; 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 _EvmAccountProvider_instances, _EvmAccountProvider_config, _EvmAccountProvider_trace, _EvmAccountProvider_createAccount, _EvmAccountProvider_getAddressFromGroupIndex, _EvmAccountProvider_getTransactionCount; Object.defineProperty(exports, "__esModule", { value: true }); exports.EvmAccountProvider = exports.EVM_ACCOUNT_PROVIDER_DEFAULT_CONFIG = exports.EVM_ACCOUNT_PROVIDER_NAME = void 0; const util_1 = require("@ethereumjs/util"); const keyring_api_1 = require("@metamask/keyring-api"); const keyring_controller_1 = require("@metamask/keyring-controller"); const utils_1 = require("@metamask/utils"); const analytics_1 = require("../analytics/index.cjs"); const traces_1 = require("../analytics/traces.cjs"); const logger_1 = require("../logger.cjs"); const BaseBip44AccountProvider_1 = require("./BaseBip44AccountProvider.cjs"); const utils_2 = require("./utils.cjs"); const ETH_MAINNET_CHAIN_ID = '0x1'; /** * Asserts an internal account exists. * * @param account - The internal account to check. * @throws An error if the internal account does not exist. */ function assertInternalAccountExists(account) { if (!account) { throw new Error('Internal account does not exist'); } } exports.EVM_ACCOUNT_PROVIDER_NAME = 'EVM'; exports.EVM_ACCOUNT_PROVIDER_DEFAULT_CONFIG = { discovery: { maxAttempts: 3, timeoutMs: 500, backOffMs: 500, }, }; class EvmAccountProvider extends BaseBip44AccountProvider_1.BaseBip44AccountProvider { constructor(messenger, config = exports.EVM_ACCOUNT_PROVIDER_DEFAULT_CONFIG, trace) { super(messenger); _EvmAccountProvider_instances.add(this); _EvmAccountProvider_config.set(this, void 0); _EvmAccountProvider_trace.set(this, void 0); this.capabilities = { scopes: [keyring_api_1.EthScope.Eoa], bip44: { deriveIndex: true, deriveIndexRange: true, }, }; __classPrivateFieldSet(this, _EvmAccountProvider_config, { ...config, discovery: { ...config.discovery, enabled: config.discovery.enabled ?? true, }, }, "f"); __classPrivateFieldSet(this, _EvmAccountProvider_trace, trace ?? analytics_1.traceFallback, "f"); } isAccountCompatible(account) { return (account.type === keyring_api_1.EthAccountType.Eoa && account.metadata.keyring.type === keyring_controller_1.KeyringTypes.hd); } getName() { return EvmAccountProvider.NAME; } /** * Get the EVM provider. * * @returns The EVM provider. */ getEvmProvider() { const networkClientId = this.messenger.call('NetworkController:findNetworkClientIdByChainId', ETH_MAINNET_CHAIN_ID); const { provider } = this.messenger.call('NetworkController:getNetworkClientById', networkClientId); return provider; } /** * Create accounts for the EVM provider. * * @param options - The options for the creation of the accounts. * @returns The accounts for the EVM provider. */ async createAccounts(options) { (0, keyring_api_1.assertCreateAccountOptionIsSupported)(options, [ `${keyring_api_1.AccountCreationType.Bip44DeriveIndex}`, `${keyring_api_1.AccountCreationType.Bip44DeriveIndexRange}`, ]); const { entropySource } = options; if (options.type === keyring_api_1.AccountCreationType.Bip44DeriveIndexRange) { const { range } = options; // Use a single withKeyring call for the entire range. const accountIds = await this.withKeyringV2({ id: entropySource }, async ({ keyring }) => { const existing = await keyring.getAccounts(); // Validate no gaps: we can only create accounts starting from existing.length. if (range.from > existing.length) { throw new Error(`Bad account creation request, group index range would create gaps (${range.from} (from) > ${existing.length} (next available index))`); } const result = []; // Collect existing accounts within the range. for (let groupIndex = range.from; groupIndex <= range.to; groupIndex++) { if (groupIndex < existing.length) { // Account already exists. result.push(existing[groupIndex].id); } } // Create new accounts one-by-one since HdKeyringV2 only supports // bip44:derive-index (not bip44:derive-index-range). for (let groupIndex = Math.max(range.from, existing.length); groupIndex <= range.to; groupIndex++) { const [created] = await keyring.createAccounts({ type: keyring_api_1.AccountCreationType.Bip44DeriveIndex, entropySource, groupIndex, }); (0, utils_1.assert)(created, 'Account creation failed'); result.push(created.id); } return result; }); const accounts = []; for (const account of this.messenger.call('AccountsController:getAccounts', accountIds)) { assertInternalAccountExists(account); this.accounts.add(account.id); accounts.push(account); } (0, BaseBip44AccountProvider_1.assertAreBip44Accounts)(accounts); return accounts; } // Handle Bip44DeriveIndex (single account creation). const { groupIndex } = options; const [created] = await __classPrivateFieldGet(this, _EvmAccountProvider_instances, "m", _EvmAccountProvider_createAccount).call(this, { entropySource, groupIndex, throwOnGap: true, }); (0, utils_1.assert)(created, 'Account creation failed'); const account = this.messenger.call('AccountsController:getAccount', created.id); // We MUST have the associated internal account. assertInternalAccountExists(account); const accountsArray = [account]; (0, BaseBip44AccountProvider_1.assertAreBip44Accounts)(accountsArray); this.accounts.add(account.id); return accountsArray; } /** * Discover and create accounts for the EVM provider. * * @param opts - The options for the discovery and creation of accounts. * @param opts.entropySource - The entropy source to use for the discovery and creation of accounts. * @param opts.groupIndex - The index of the group to create the accounts for. * @returns The accounts for the EVM provider. */ async discoverAccounts(opts) { return __classPrivateFieldGet(this, _EvmAccountProvider_trace, "f").call(this, { name: traces_1.TraceName.EvmDiscoverAccounts, data: { provider: this.getName(), }, }, async () => { if (!__classPrivateFieldGet(this, _EvmAccountProvider_config, "f").discovery.enabled) { return []; } const provider = this.getEvmProvider(); const { entropySource, groupIndex } = opts; const addressFromGroupIndex = await __classPrivateFieldGet(this, _EvmAccountProvider_instances, "m", _EvmAccountProvider_getAddressFromGroupIndex).call(this, { entropySource, groupIndex, }); const count = await __classPrivateFieldGet(this, _EvmAccountProvider_instances, "m", _EvmAccountProvider_getTransactionCount).call(this, provider, addressFromGroupIndex); if (count === 0) { return []; } // We have some activity on this address, we try to create the account. const [created] = await __classPrivateFieldGet(this, _EvmAccountProvider_instances, "m", _EvmAccountProvider_createAccount).call(this, { entropySource, groupIndex, throwOnGap: false, }); (0, utils_1.assert)(created, 'Account creation failed'); (0, utils_1.assert)(addressFromGroupIndex === created.address, 'Created account does not match address from group index.'); const account = this.messenger.call('AccountsController:getAccount', created.id); assertInternalAccountExists(account); (0, BaseBip44AccountProvider_1.assertIsBip44Account)(account); this.accounts.add(account.id); return [account]; }); } async resyncAccounts() { // No-op for the EVM account provider, since keyring accounts are already on // the MetaMask side. } /** * Delete an EVM account by id. * * Resolves the account's entropy source from the tracked account, then * forwards to the v2 HD keyring's `deleteAccount(id)`. When this is the * last account on a non-primary HD keyring, the keyring controller will * automatically prune the empty keyring (see * `KeyringController.#cleanUpEmptiedKeyringsAfter`). * * @param id - The id of the account to delete. */ async deleteAccount(id) { const account = this.getAccount(id); const entropySource = account.options.entropy.id; await this.withKeyringV2({ id: entropySource }, async ({ keyring }) => { await keyring.deleteAccount(id); }); this.accounts.delete(id); } } exports.EvmAccountProvider = EvmAccountProvider; _EvmAccountProvider_config = new WeakMap(), _EvmAccountProvider_trace = new WeakMap(), _EvmAccountProvider_instances = new WeakSet(), _EvmAccountProvider_createAccount = /** * Create an EVM account. * * @param opts - The options for the creation of the account. * @param opts.entropySource - The entropy source to use for the creation of the account. * @param opts.groupIndex - The index of the group to create the account for. * @param opts.throwOnGap - Whether to throw an error if the account index is not contiguous. * @returns The created or existing keyring account(s) at the requested group * index. Returns an empty array if the keyring did not create an account. */ async function _EvmAccountProvider_createAccount({ entropySource, groupIndex, throwOnGap, }) { return await this.withKeyringV2({ id: entropySource }, async ({ keyring }) => { const existing = await keyring.getAccounts(); if (groupIndex < existing.length) { return [existing[groupIndex]]; } // If the throwOnGap flag is set, we throw an error to prevent index gaps. if (throwOnGap && groupIndex !== existing.length) { throw new Error('Trying to create too many accounts'); } return await keyring.createAccounts({ type: keyring_api_1.AccountCreationType.Bip44DeriveIndex, entropySource, groupIndex, }); }); }, _EvmAccountProvider_getAddressFromGroupIndex = /** * Get the address that would be derived for a given group index without * persisting an account in the keyring. * * Peeks at the next address via the HD `root` so discovery can short-circuit * (and skip the vault write + `stateChange` event) when there is no on-chain * activity at this index. * * @param opts - The options for the address derivation. * @param opts.entropySource - The entropy source to derive from. * @param opts.groupIndex - The group index to derive at. * @returns The derived address for the given group index. */ async function _EvmAccountProvider_getAddressFromGroupIndex({ entropySource, groupIndex, }) { // NOTE: To avoid exposing this function at keyring level, we just re-use its internal state // and compute the derivation here. return await this.withKeyringV2({ id: entropySource }, async ({ keyring }) => { // If the account already exist, do not re-derive and just re-use that account. const existing = await keyring.getAccounts(); if (groupIndex < existing.length) { return existing[groupIndex].address; } // If not, then we just "peek" the next address to avoid creating the account. (0, utils_1.assert)(keyring.root, 'Expected HD keyring.root to be set'); const hdKey = keyring.root.deriveChild(groupIndex); (0, utils_1.assert)(hdKey.publicKey, 'Expected public key to be set'); return (0, utils_1.add0x)((0, utils_1.bytesToHex)((0, util_1.publicToAddress)(hdKey.publicKey, true)).toLowerCase()); }); }, _EvmAccountProvider_getTransactionCount = /** * Get the transaction count for an EVM account. * This method uses a retry and timeout mechanism to handle transient failures. * * @param provider - The provider to use for the transaction count. * @param address - The address of the account. * @returns The transaction count. */ async function _EvmAccountProvider_getTransactionCount(provider, address) { const method = 'eth_getTransactionCount'; const response = await (0, utils_2.withRetry)(() => (0, utils_2.withTimeout)(() => provider.request({ method, params: [address, 'latest'], }), __classPrivateFieldGet(this, _EvmAccountProvider_config, "f").discovery.timeoutMs), { maxAttempts: __classPrivateFieldGet(this, _EvmAccountProvider_config, "f").discovery.maxAttempts, backOffMs: __classPrivateFieldGet(this, _EvmAccountProvider_config, "f").discovery.backOffMs, }); // Make sure we got the right response format, if not, we fallback to "0x0", to avoid having to deal with `NaN`. if (!(0, utils_1.isStrictHexString)(response)) { const message = `Received invalid hex response from "${method}" request: ${JSON.stringify(response)}`; (0, logger_1.projectLogger)(`${logger_1.WARNING_PREFIX} ${message}`); console.warn(message); return 0; } return parseInt(response, 16); }; EvmAccountProvider.NAME = exports.EVM_ACCOUNT_PROVIDER_NAME; //# sourceMappingURL=EvmAccountProvider.cjs.map