@metamask/multichain-account-service
Version:
Service to manage multichain accounts
299 lines • 14.8 kB
JavaScript
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;
import { publicToAddress } from "@ethereumjs/util";
import { AccountCreationType, assertCreateAccountOptionIsSupported, EthAccountType, EthScope } from "@metamask/keyring-api";
import { KeyringTypes } from "@metamask/keyring-controller";
import { add0x, assert, bytesToHex, isStrictHexString } from "@metamask/utils";
import { traceFallback } from "../analytics/index.mjs";
import { TraceName } from "../analytics/traces.mjs";
import { projectLogger as log, WARNING_PREFIX } from "../logger.mjs";
import { assertAreBip44Accounts, assertIsBip44Account, BaseBip44AccountProvider } from "./BaseBip44AccountProvider.mjs";
import { withRetry, withTimeout } from "./utils.mjs";
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');
}
}
export const EVM_ACCOUNT_PROVIDER_NAME = 'EVM';
export const EVM_ACCOUNT_PROVIDER_DEFAULT_CONFIG = {
discovery: {
maxAttempts: 3,
timeoutMs: 500,
backOffMs: 500,
},
};
export class EvmAccountProvider extends BaseBip44AccountProvider {
constructor(messenger, config = 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: [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 ?? traceFallback, "f");
}
isAccountCompatible(account) {
return (account.type === EthAccountType.Eoa &&
account.metadata.keyring.type === 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) {
assertCreateAccountOptionIsSupported(options, [
`${AccountCreationType.Bip44DeriveIndex}`,
`${AccountCreationType.Bip44DeriveIndexRange}`,
]);
const { entropySource } = options;
if (options.type === 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: AccountCreationType.Bip44DeriveIndex,
entropySource,
groupIndex,
});
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);
}
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,
});
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];
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: 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,
});
assert(created, 'Account creation failed');
assert(addressFromGroupIndex === created.address, 'Created account does not match address from group index.');
const account = this.messenger.call('AccountsController:getAccount', created.id);
assertInternalAccountExists(account);
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);
}
}
_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: 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.
assert(keyring.root, 'Expected HD keyring.root to be set');
const hdKey = keyring.root.deriveChild(groupIndex);
assert(hdKey.publicKey, 'Expected public key to be set');
return add0x(bytesToHex(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 withRetry(() => 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 (!isStrictHexString(response)) {
const message = `Received invalid hex response from "${method}" request: ${JSON.stringify(response)}`;
log(`${WARNING_PREFIX} ${message}`);
console.warn(message);
return 0;
}
return parseInt(response, 16);
};
EvmAccountProvider.NAME = EVM_ACCOUNT_PROVIDER_NAME;
//# sourceMappingURL=EvmAccountProvider.mjs.map