Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
3eaf3ba
feat: add discoverAndCreateAccounts method to MultichainAccountWallet
hmalik88 Aug 27, 2025
0eae422
feat: add provider type and remove groupIndex from discoverAndCreateA…
hmalik88 Aug 27, 2025
6c792c7
feat: add method to get the EVM provider and fill in discoverAndCreat…
hmalik88 Aug 27, 2025
74790a5
feat: update snap provider class to add providerType
hmalik88 Aug 27, 2025
b6f8de3
feat: update sol provider to add providerType and remove groupIndex f…
hmalik88 Aug 27, 2025
6a6a184
feat: add network controller actions to messenger
hmalik88 Aug 27, 2025
2960c7e
chore: remove comment
hmalik88 Aug 27, 2025
ba635c9
chore: add JSdocs
hmalik88 Aug 27, 2025
f2c16a1
refactor: move orchestraction into wallet class
hmalik88 Aug 29, 2025
e4fcc7f
refactor: re-add groupIndex to discoverAndCreateAccounts provider met…
hmalik88 Aug 29, 2025
5994b14
refactor: add groupIndex for sol provider
hmalik88 Aug 29, 2025
bc19d05
refactor: apply code review
hmalik88 Aug 29, 2025
a8034f1
Merge branch 'main' into hm/mul-345
hmalik88 Aug 29, 2025
861acd2
fix: use type guard to narrow provider type
hmalik88 Aug 29, 2025
f7fbe26
fix: lint fix
hmalik88 Aug 29, 2025
9ed9bc3
feat: add discovery for solana
hmalik88 Aug 30, 2025
3666ed1
test: add evm provider tests
hmalik88 Aug 30, 2025
ec712e6
refactor: make accounts readonly again
hmalik88 Aug 30, 2025
0784bc3
refactor: simplify discoverAndCreateAccounts for solana
hmalik88 Aug 31, 2025
1540fcf
test: add solana discovery tests
hmalik88 Aug 31, 2025
923a733
test: add discoverAndCreateAccounts tests for multichain account wall…
hmalik88 Sep 1, 2025
54acf85
refactor: update mock providers to both start with no accounts since …
hmalik88 Sep 1, 2025
d9a22c6
Merge branch 'main' into hm/mul-345
hmalik88 Sep 1, 2025
61d2212
chore: update JSdoc comment
hmalik88 Sep 1, 2025
4faeea1
refactor: add spacing
hmalik88 Sep 1, 2025
8b0afe7
chore: add changelog entries
hmalik88 Sep 1, 2025
071b83f
chore: lint fixes
hmalik88 Sep 1, 2025
e5d17b3
fix: prettier fix
hmalik88 Sep 1, 2025
0d3a917
feat: sync wallet before calling align groups
hmalik88 Sep 1, 2025
f7d62ca
fix: update return type for keyring client\'s send action
hmalik88 Sep 1, 2025
1de1276
feat: relax withKeyring type to accept options
hmalik88 Sep 1, 2025
6467328
feat: refactor discovery logic to not +1 groupIndex
hmalik88 Sep 1, 2025
00a649d
test: update evm provider tests to not be expecting +1 on the groupIndex
hmalik88 Sep 1, 2025
1907177
feat: add createMultichainAccountWallet method
hmalik88 Sep 1, 2025
f63b37c
test: add tests for createMultichainAccountWallet
hmalik88 Sep 1, 2025
eca202a
chore: update changelog
hmalik88 Sep 1, 2025
dc935d8
Merge branch 'main' into hm/mul-345
hmalik88 Sep 1, 2025
b35ead8
refactor: use if statement to avoid importing utils package
hmalik88 Sep 1, 2025
e52859e
feat: update messenger actions with new create multichain account wal…
hmalik88 Sep 1, 2025
6aab24b
feat: register action handler for createMultichainAccountWallet
hmalik88 Sep 1, 2025
ce178b2
test: add test for createMultichainAccountWallet action
hmalik88 Sep 1, 2025
146041e
chore: update changelog again
hmalik88 Sep 1, 2025
b3c5bbd
fix: update changelog entries to be under unreleased
hmalik88 Sep 1, 2025
fe04162
feat: apply code review
hmalik88 Sep 2, 2025
d9e6afd
chore: remove unneeded istanbul ignore
hmalik88 Sep 2, 2025
80d1004
fix: fix import order
hmalik88 Sep 2, 2025
974a6a0
Merge branch 'main' into hm/mul-345
hmalik88 Sep 2, 2025
f820127
feat: add getKeyringsByType action to messenger
hmalik88 Sep 3, 2025
661722a
Merge branch 'main' into hm/mul-345
hmalik88 Sep 3, 2025
0d8fd74
refactor: use addKeyring action and add logic to check for existing k…
hmalik88 Sep 3, 2025
4becc36
chore: update JSDoc
hmalik88 Sep 3, 2025
3232d5b
Merge branch 'main' into hm/mul-345
hmalik88 Sep 3, 2025
1baee79
chore: fix JSdoc
hmalik88 Sep 3, 2025
b9e99ea
Merge branch 'main' into hm/mul-345
hmalik88 Sep 3, 2025
0631293
fix: remove double Buffer.from
hmalik88 Sep 3, 2025
ffe7ad6
fix: return provider's running promise in schedule function to preven…
hmalik88 Sep 4, 2025
f40465d
Merge branch 'main' into hm/mul-345
hmalik88 Sep 4, 2025
e501f17
refactor: move provider discovery context type into types
hmalik88 Sep 4, 2025
1a51802
fix: lint fix
hmalik88 Sep 4, 2025
b4ca8d3
Merge branch 'main' into hm/mul-345
hmalik88 Sep 4, 2025
0ed6351
chore(multichain-account-service): refactor and improvements around d…
ccharly Sep 4, 2025
3734aa9
Merge branch 'main' into hm/mul-345
hmalik88 Sep 4, 2025
7f3c257
chore: remove provider context type from types
hmalik88 Sep 4, 2025
b4e21f2
chore: add breaking entry for multichain account service messenger
hmalik88 Sep 4, 2025
9f870c4
fix: update Evm to EVM in JSDocs
hmalik88 Sep 4, 2025
8880972
chore: remove condition that is no longer true from JSDoc
hmalik88 Sep 4, 2025
55a39fb
refactor: only return newly created wallet instead of tuple in create…
hmalik88 Sep 4, 2025
2e60f1d
chore: rename utils to mnemonic
hmalik88 Sep 4, 2025
1bde1e7
fix: update service tests
hmalik88 Sep 4, 2025
f13020b
feat: add chainId const
hmalik88 Sep 4, 2025
d0f403d
feat: use logger from utils in discoverAndCreateAccounts
hmalik88 Sep 4, 2025
aab0051
fix: typos
hmalik88 Sep 4, 2025
fa84029
Merge branch 'main' into hm/mul-345
hmalik88 Sep 4, 2025
7849934
fix: utils version
hmalik88 Sep 4, 2025
f6832cf
refactor: apply code review
hmalik88 Sep 4, 2025
46fc0d3
fix: update test description
hmalik88 Sep 4, 2025
c63a2bd
feat: add throwOnGap param to createAccount
hmalik88 Sep 4, 2025
6187f4e
refactor: apply code review
hmalik88 Sep 4, 2025
46aad3c
Merge branch 'main' into hm/mul-345
hmalik88 Sep 4, 2025
66aa686
feat: add type alias for discoverAndCreateAccounts return type
hmalik88 Sep 4, 2025
7230071
fix: remove unused vars
hmalik88 Sep 4, 2025
badf9f9
fix: change eth-hd-keyring version to be consistent
hmalik88 Sep 4, 2025
1176ee7
chore: add return types to EVM provider
hmalik88 Sep 5, 2025
428cc30
chore: remove createMultichainAccountWallet code
hmalik88 Sep 5, 2025
7b04612
fix: update logging with the target group index
hmalik88 Sep 5, 2025
edcf69b
Merge branch 'main' into hm/mul-345
hmalik88 Sep 5, 2025
a3a730d
refactor: apply code review
hmalik88 Sep 5, 2025
9c5ea62
fix: EVM provider test
hmalik88 Sep 5, 2025
35fe26d
fix: updated solana discovery to account for different derivation sch…
hmalik88 Sep 5, 2025
022a00b
refactor: change implementation of solana discovery to preserve publi…
hmalik88 Sep 5, 2025
a0830e0
Merge branch 'main' into hm/mul-345
hmalik88 Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,101 @@ export class MultichainAccountWallet<
await group.align();
}
}

/**
* Discover and create accounts for all providers.
*
* NOTE: This method should only be called on a newly created wallet.
*
* @param opts - The options for the discovery and creation of accounts.
* @param opts.skippedProviders - The providers to skip.
* @returns The accounts for each provider.
*/
async discoverAndCreateAccounts({
skippedProviders = [],
}: {
skippedProviders: AccountProviderType[];
}): Promise<{
[AccountProviderType]: Bip44Account<KeyringAccount>[];
}> {
const providers = this.#providers.filter(
(provider) => !skippedProviders.includes(provider.providerType),
);

const results: Record<AccountProviderType, Bip44Account<KeyringAccount>[]> =
{};
const nextIndex = new Map<Bip44Provider, number>(
providers.map((p) => [p, 0]),
);
const stopped = new Set<Bip44Provider>();
const inFlight = new Map<Bip44Provider, Promise<void>>();

let high = 0;

const schedule = (p: Bip44Provider, index: number) => {
const run = (async () => {
try {
const accounts = await p.discoverAndCreateAccounts({
entropySource: this.#entropySource,
groupIndex: index,
});

inFlight.delete(p);

if (accounts.length > 0) {
stopped.add(p);
} else {
results[p.providerType] = [
...(results[p.providerType] ?? []),
...accounts,
];

const next = index + 1;
nextIndex.set(p, next);
if (next > high) {
high = next;

for (const q of providers) {
if (
!stopped.has(q) &&
!inFlight.has(q) &&
(nextIndex.get(q) ?? 0) < high
) {
schedule(q, high);
}
}
}

if (!stopped.has(p)) {
const target = Math.max(high, nextIndex.get(p) ?? 0);
schedule(p, target);
}
}
} catch {
inFlight.delete(p);
stopped.add(p);
}
})();

inFlight.set(p, run);
};

for (const p of providers) {
schedule(p, 0);
}

while (inFlight.size > 0) {
await Promise.race(inFlight.values());
}

await this.alignGroups();

const out: Record<AccountProviderType, Bip44Account<KeyringAccount>[]> = {};

for (const p of providers) {
out[p.providerType] = results[p.providerType] ?? [];
}

return out;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ export abstract class BaseAccountProvider
{
protected readonly messenger: MultichainAccountServiceMessenger;

constructor(messenger: MultichainAccountServiceMessenger) {
readonly providerType: AccountProviderType;

constructor(
messenger: MultichainAccountServiceMessenger,
providerType: AccountProviderType,
) {
this.messenger = messenger;
this.providerType = providerType;
}

#getAccounts(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import type {
EthKeyring,
InternalAccount,
} from '@metamask/keyring-internal-api';
import type { Provider } from '@metamask/network-controller';
import type { Hex } from '@metamask/utils';
import type { MultichainAccountServiceMessenger } from 'src/types';

import {
assertAreBip44Accounts,
assertIsBip44Account,
BaseAccountProvider,
} from './BaseAccountProvider';

Expand All @@ -28,13 +31,34 @@ function assertInternalAccountExists(
}

export class EvmAccountProvider extends BaseAccountProvider {
constructor(messenger: MultichainAccountServiceMessenger) {
super(messenger, AccountProviderType.Evm);
}

isAccountCompatible(account: Bip44Account<InternalAccount>): boolean {
return (
account.type === EthAccountType.Eoa &&
account.metadata.keyring.type === (KeyringTypes.hd as string)
);
}

/**
* Get the Evm provider.
*
* @returns The Evm provider.
*/
getEvmProvider(): Provider {
const networkClientId = this.messenger.call(
'NetworkController:findNetworkClientIdByChainId',
'0x1',
);
const { provider } = this.messenger.call(
'NetworkController:getNetworkClientById',
networkClientId,
);
return provider;
}

async createAccounts({
entropySource,
groupIndex,
Expand Down Expand Up @@ -76,10 +100,62 @@ export class EvmAccountProvider extends BaseAccountProvider {
return accountsArray;
}

async discoverAndCreateAccounts(_: {
/**
* Discover and create accounts for the Evm provider.
*
* NOTE: This method should only be called on a newly created wallet.
* There should be already one existing account on this associated entropy source.
*
* @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 discoverAndCreateAccounts(opts: {
entropySource: EntropySourceId;
groupIndex: number;
}) {
return []; // TODO: Implement account discovery.
const provider = this.getEvmProvider();
const { entropySource, groupIndex } = opts;
// groupIndex starts as +1, because we already have one account in the associated keyring.
const actualGroupIndex = groupIndex + 1;

const [address, didCreate] = await this.withKeyring<
EthKeyring,
[Hex, boolean]
>({ id: entropySource }, async ({ keyring }) => {
const existing = await keyring.getAccounts();
if (actualGroupIndex < existing.length) {
return [existing[actualGroupIndex], false];
}
const need = actualGroupIndex - existing.length + 1;
const added = await keyring.addAccounts(need);
const target = added[added.length - 1];
return [target, true];
});

const countHex = (await provider.request({
method: 'eth_getTransactionCount',
params: [address, 'latest'],
})) as Hex;
const count = parseInt(countHex, 16);

if (count === 0 && didCreate) {
await this.withKeyring<EthKeyring>(
{ id: entropySource },
async ({ keyring }) => {
keyring.removeAccount?.(address);
},
);
return [];
}

const account = this.messenger.call(
'AccountsController:getAccountByAddress',
address,
);
assertInternalAccountExists(account);
assertIsBip44Account(account);
return [account];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ export type RestrictedSnapKeyringCreateAccount = (
export abstract class SnapAccountProvider extends BaseAccountProvider {
readonly snapId: SnapId;

constructor(snapId: SnapId, messenger: MultichainAccountServiceMessenger) {
super(messenger);
constructor(
snapId: SnapId,
messenger: MultichainAccountServiceMessenger,
providerType: AccountProviderType,
) {
super(messenger, providerType);

this.snapId = snapId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export class SolAccountProvider extends SnapAccountProvider {
static SOLANA_SNAP_ID = 'npm:@metamask/solana-wallet-snap' as SnapId;

constructor(messenger: MultichainAccountServiceMessenger) {
super(SolAccountProvider.SOLANA_SNAP_ID, messenger);
super(
SolAccountProvider.SOLANA_SNAP_ID,
messenger,
AccountProviderType.Solana,
);
}

isAccountCompatible(account: Bip44Account<InternalAccount>): boolean {
Expand Down
8 changes: 7 additions & 1 deletion packages/multichain-account-service/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import type {
KeyringControllerStateChangeEvent,
KeyringControllerWithKeyringAction,
} from '@metamask/keyring-controller';
import type {
NetworkControllerFindNetworkClientIdByChainIdAction,
NetworkControllerGetNetworkClientByIdAction,
} from '@metamask/network-controller';
import type { HandleSnapRequest as SnapControllerHandleSnapRequestAction } from '@metamask/snaps-controllers';

import type {
Expand Down Expand Up @@ -88,7 +92,9 @@ export type AllowedActions =
| AccountsControllerGetAccountByAddressAction
| SnapControllerHandleSnapRequestAction
| KeyringControllerWithKeyringAction
| KeyringControllerGetStateAction;
| KeyringControllerGetStateAction
| NetworkControllerGetNetworkClientByIdAction
| NetworkControllerFindNetworkClientIdByChainIdAction;

/**
* All events published by other modules that {@link MultichainAccountService}
Expand Down