@metamask/multichain-account-service
Version:
Service to manage multichain accounts
303 lines • 15.3 kB
JavaScript
"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