@metamask/multichain-account-service
Version:
Service to manage multichain accounts
260 lines • 13 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 _MultichainAccountWallet_id, _MultichainAccountWallet_providers, _MultichainAccountWallet_entropySource, _MultichainAccountWallet_accountGroups;
import { getGroupIndexFromMultichainAccountGroupId, isMultichainAccountGroupId, toMultichainAccountWalletId } from "@metamask/account-api";
import { toDefaultAccountGroupId } from "@metamask/account-api";
import { AccountWalletType } from "@metamask/account-api";
import { MultichainAccountGroup } from "./MultichainAccountGroup.mjs";
/**
* A multichain account wallet that holds multiple multichain accounts (one multichain account per
* group index).
*/
export class MultichainAccountWallet {
constructor({ providers, entropySource, }) {
_MultichainAccountWallet_id.set(this, void 0);
_MultichainAccountWallet_providers.set(this, void 0);
_MultichainAccountWallet_entropySource.set(this, void 0);
_MultichainAccountWallet_accountGroups.set(this, void 0);
__classPrivateFieldSet(this, _MultichainAccountWallet_id, toMultichainAccountWalletId(entropySource), "f");
__classPrivateFieldSet(this, _MultichainAccountWallet_providers, providers, "f");
__classPrivateFieldSet(this, _MultichainAccountWallet_entropySource, entropySource, "f");
__classPrivateFieldSet(this, _MultichainAccountWallet_accountGroups, new Map(), "f");
// Initial synchronization.
this.sync();
}
/**
* Force wallet synchronization.
*
* This can be used if account providers got new accounts that the wallet
* doesn't know about.
*/
sync() {
for (const provider of __classPrivateFieldGet(this, _MultichainAccountWallet_providers, "f")) {
for (const account of provider.getAccounts()) {
const { entropy } = account.options;
// Filter for this wallet only.
if (entropy.id !== this.entropySource) {
continue;
}
// This multichain account might exists already.
let multichainAccount = __classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").get(entropy.groupIndex);
if (!multichainAccount) {
multichainAccount = new MultichainAccountGroup({
groupIndex: entropy.groupIndex,
wallet: this,
providers: __classPrivateFieldGet(this, _MultichainAccountWallet_providers, "f"),
});
// This existing multichain account group might differ from the
// `createMultichainAccountGroup` behavior. When creating a new
// group, we expect the providers to all succeed. But here, we're
// just fetching the account lists from them, so this group might
// not be "aligned" yet (e.g having a missing Solana account).
//
// Since "aligning" is an async operation, it would have to be run
// after the first-sync.
// TODO: Implement align mechanism to create "missing" accounts.
__classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").set(entropy.groupIndex, multichainAccount);
}
}
}
// Now force-sync all remaining multichain accounts.
for (const [groupIndex, multichainAccount,] of __classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").entries()) {
multichainAccount.sync();
// Clean up old multichain accounts.
if (!multichainAccount.hasAccounts()) {
__classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").delete(groupIndex);
}
}
}
/**
* Gets the multichain account wallet ID.
*
* @returns The multichain account wallet ID.
*/
get id() {
return __classPrivateFieldGet(this, _MultichainAccountWallet_id, "f");
}
/**
* Gets the multichain account wallet type, which is always {@link AccountWalletType.Entropy}.
*
* @returns The multichain account wallet type.
*/
get type() {
return AccountWalletType.Entropy;
}
/**
* Gets the multichain account wallet entropy source.
*
* @returns The multichain account wallet entropy source.
*/
get entropySource() {
return __classPrivateFieldGet(this, _MultichainAccountWallet_entropySource, "f");
}
/**
* Gets multichain account for a given ID.
* The default group ID will default to the multichain account with index 0.
*
* @param id - Account group ID.
* @returns Account group.
*/
getAccountGroup(id) {
// We consider the "default case" to be mapped to index 0.
if (id === toDefaultAccountGroupId(this.id)) {
return __classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").get(0);
}
// If it is not a valid ID, we cannot extract the group index
// from it, so we fail fast.
if (!isMultichainAccountGroupId(id)) {
return undefined;
}
const groupIndex = getGroupIndexFromMultichainAccountGroupId(id);
return __classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").get(groupIndex);
}
/**
* Gets all multichain accounts. Similar to {@link MultichainAccountWallet.getMultichainAccountGroups}.
*
* @returns The multichain accounts.
*/
getAccountGroups() {
return this.getMultichainAccountGroups();
}
/**
* Gets multichain account group for a given index.
*
* @param groupIndex - Multichain account index.
* @returns The multichain account associated with the given index.
*/
getMultichainAccountGroup(groupIndex) {
return __classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").get(groupIndex);
}
/**
* Gets all multichain account groups.
*
* @returns The multichain accounts.
*/
getMultichainAccountGroups() {
return Array.from(__classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").values()); // TODO: Prevent copy here.
}
/**
* Gets next group index for this wallet.
*
* @returns The next group index of this wallet.
*/
getNextGroupIndex() {
// We do not check for gaps.
return (Math.max(-1, // So it will default to 0 if no groups.
...__classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").keys()) + 1);
}
/**
* Creates a multichain account group for a given group index.
*
* @param groupIndex - The group index to use.
* @throws If any of the account providers fails to create their accounts.
* @returns The multichain account group for this group index.
*/
async createMultichainAccountGroup(groupIndex) {
const nextGroupIndex = this.getNextGroupIndex();
if (groupIndex > nextGroupIndex) {
throw new Error(`You cannot use a group index that is higher than the next available one: expected <=${nextGroupIndex}, got ${groupIndex}`);
}
let group = this.getMultichainAccountGroup(groupIndex);
if (group) {
// If the group already exists, we just `sync` it and returns the same
// reference.
group.sync();
return group;
}
const results = await Promise.allSettled(__classPrivateFieldGet(this, _MultichainAccountWallet_providers, "f").map((provider) => provider.createAccounts({
entropySource: __classPrivateFieldGet(this, _MultichainAccountWallet_entropySource, "f"),
groupIndex,
})));
// --------------------------------------------------------------------------------
// READ THIS CAREFULLY:
//
// Since we're not "fully supporting multichain" for now, we still rely on single
// :accountCreated events to sync multichain account groups and wallets. Which means
// that even if of the provider fails, some accounts will still be created on some
// other providers and will become "available" on the `AccountsController`, like:
//
// 1. Creating a multichain account group for index 1
// 2. EvmAccountProvider.createAccounts returns the EVM account for index 1
// * AccountsController WILL fire :accountCreated for this account
// * This account WILL BE "available" on the AccountsController state
// 3. SolAccountProvider.createAccounts fails to create a Solana account for index 1
// * AccountsController WON't fire :accountCreated for this account
// * This account WON'T be "available" on the Account
// 4. MultichainAccountService will receive a :accountCreated for the EVM account from
// step 2 and will create a new multichain account group for index 1, but it won't
// receive any event for the Solana account of this group. Thus, this group won't be
// "aligned" (missing "blockchain account" on this group).
//
// --------------------------------------------------------------------------------
// If any of the provider failed to create their accounts, then we consider the
// multichain account group to have failed too.
if (results.some((result) => result.status === 'rejected')) {
// NOTE: Some accounts might still have been created on other account providers. We
// don't rollback them.
const error = `Unable to create multichain account group for index: ${groupIndex}`;
let warn = `${error}:`;
for (const result of results) {
if (result.status === 'rejected') {
warn += `\n- ${result.reason}`;
}
}
console.warn(warn);
throw new Error(error);
}
// Because of the :accountAdded automatic sync, we might already have created the
// group, so we first try to get it.
group = this.getMultichainAccountGroup(groupIndex);
if (!group) {
// If for some reason it's still not created, we're creating it explicitly now:
group = new MultichainAccountGroup({
wallet: this,
providers: __classPrivateFieldGet(this, _MultichainAccountWallet_providers, "f"),
groupIndex,
});
}
// Register the account to our internal map.
__classPrivateFieldGet(this, _MultichainAccountWallet_accountGroups, "f").set(groupIndex, group); // `group` cannot be undefined here.
return group;
}
/**
* Creates the next multichain account group.
*
* @throws If any of the account providers fails to create their accounts.
* @returns The multichain account group for the next group index available.
*/
async createNextMultichainAccountGroup() {
return this.createMultichainAccountGroup(this.getNextGroupIndex());
}
/**
* Align all multichain account groups.
*/
async alignGroups() {
const groups = this.getMultichainAccountGroups();
await Promise.all(groups.map((g) => g.align()));
}
/**
* Align a specific multichain account group.
*
* @param groupIndex - The group index to align.
*/
async alignGroup(groupIndex) {
const group = this.getMultichainAccountGroup(groupIndex);
if (group) {
await group.align();
}
}
}
_MultichainAccountWallet_id = new WeakMap(), _MultichainAccountWallet_providers = new WeakMap(), _MultichainAccountWallet_entropySource = new WeakMap(), _MultichainAccountWallet_accountGroups = new WeakMap();
//# sourceMappingURL=MultichainAccountWallet.mjs.map