Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 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
7 changes: 7 additions & 0 deletions packages/multichain-account-service/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `discoverAndCreateAccounts` methods for EVM and Solana providers ([#6397](https://github.com/MetaMask/core/pull/6397))
- Add `discoverAndCreateAccounts` method to `MultichainAccountWallet` to orchestrate provider discovery ([#6397](https://github.com/MetaMask/core/pull/6397))
- Add `createMultichainAccountWallet` method to create a new multichain account wallet from a mnemonic ([#6397](https://github.com/MetaMask/core/pull/6397))
- An action handler was also registered for this method so that it can be called from the clients.

## [0.6.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {
} from '@metamask/account-api';
import type { Bip44Account } from '@metamask/account-api';
import type { AccountSelector } from '@metamask/account-api';
import type { AccountProvider } from '@metamask/account-api';
import { type KeyringAccount } from '@metamask/keyring-api';

import type { MultichainAccountWallet } from './MultichainAccountWallet';
import type { NamedAccountProvider } from './providers';

/**
* A multichain account group that holds multiple accounts.
Expand All @@ -24,11 +24,17 @@ export class MultichainAccountGroup<

readonly #groupIndex: number;

readonly #providers: AccountProvider<Account>[];
readonly #providers: NamedAccountProvider<Account>[];

readonly #providerToAccounts: Map<AccountProvider<Account>, Account['id'][]>;
readonly #providerToAccounts: Map<
NamedAccountProvider<Account>,
Account['id'][]
>;

readonly #accountToProvider: Map<Account['id'], AccountProvider<Account>>;
readonly #accountToProvider: Map<
Account['id'],
NamedAccountProvider<Account>
>;

constructor({
groupIndex,
Expand All @@ -37,7 +43,7 @@ export class MultichainAccountGroup<
}: {
groupIndex: number;
wallet: MultichainAccountWallet<Account>;
providers: AccountProvider<Account>[];
providers: NamedAccountProvider<Account>[];
}) {
this.#id = toMultichainAccountGroupId(wallet.id, groupIndex);
this.#groupIndex = groupIndex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Mocks = {
KeyringController: {
keyrings: KeyringObject[];
getState: jest.Mock;
withKeyring: jest.Mock;
};
AccountsController: {
listMultichainAccounts: jest.Mock;
Expand Down Expand Up @@ -102,13 +103,17 @@ function setup({
KeyringController: {
keyrings,
getState: jest.fn(),
withKeyring: jest.fn(),
},
AccountsController: {
listMultichainAccounts: jest.fn(),
},
EvmAccountProvider: makeMockAccountProvider(),
SolAccountProvider: makeMockAccountProvider(),
};
// Default provider names can be overridden per test using mockImplementation
mocks.EvmAccountProvider.getName.mockImplementation(() => 'EVM');
mocks.SolAccountProvider.getName.mockImplementation(() => 'Solana');

mocks.KeyringController.getState.mockImplementation(() => ({
isUnlocked: true,
Expand All @@ -120,6 +125,11 @@ function setup({
mocks.KeyringController.getState,
);

messenger.registerActionHandler(
'KeyringController:withKeyring',
mocks.KeyringController.withKeyring,
);

if (accounts) {
mocks.AccountsController.listMultichainAccounts.mockImplementation(
() => accounts,
Expand Down Expand Up @@ -832,6 +842,26 @@ describe('MultichainAccountService', () => {

expect(isInProgress).toBe(false);
});

it('creates a multichain account wallet with MultichainAccountService:createMultichainAccountWallet', async () => {
const { messenger, mocks } = setup({ accounts: [], keyrings: [] });

mocks.KeyringController.withKeyring.mockImplementationOnce(
async (_selector, op, _opts) => {
const keyring = { getAccounts: jest.fn().mockResolvedValue([]) };
const metadata = { id: 'abc', type: KeyringTypes.hd };
return op({ keyring, metadata });
},
);

const [wallet, entropySource] = await messenger.call(
'MultichainAccountService:createMultichainAccountWallet',
{ mnemonic: 'test' },
);

expect(wallet).toBeDefined();
expect(entropySource).toBe('abc');
});
});

describe('setBasicFunctionality', () => {
Expand Down Expand Up @@ -954,4 +984,48 @@ describe('MultichainAccountService', () => {
expect(wrapper.isAccountCompatible(MOCK_HD_ACCOUNT_1)).toBe(false);
});
});

describe('createMultichainAccountWallet', () => {
it('creates a multichain account wallet with MultichainAccountService:createMultichainAccountWallet', async () => {
const { mocks, service } = setup({
accounts: [],
keyrings: [],
});

// Make the messenger withKeyring call invoke our operation so the service code runs
mocks.KeyringController.withKeyring.mockImplementationOnce(
async (_selector, op, _opts) => {
const keyring = { getAccounts: jest.fn().mockResolvedValue([]) };
const metadata = { id: 'abc', type: KeyringTypes.hd };
return op({ keyring, metadata });
},
);

const [wallet, entropySource] =
await service.createMultichainAccountWallet({
mnemonic: 'test',
});

expect(wallet).toBeDefined();
expect(entropySource).toBe('abc');
});

it("throws an error if there's already an existing keyring with accounts", async () => {
const { service, mocks } = setup({ accounts: [], keyrings: [] });

mocks.KeyringController.withKeyring.mockImplementationOnce(
async (_selector, op, _opts) => {
const keyring = {
getAccounts: jest.fn().mockResolvedValue(['0xabc']),
};
const metadata = { id: 'abc', type: KeyringTypes.hd };
return op({ keyring, metadata });
},
);

await expect(
service.createMultichainAccountWallet({ mnemonic: 'test' }),
).rejects.toThrow('Expected keyring with no accounts');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ import {
import type {
MultichainAccountWalletId,
Bip44Account,
AccountProvider,
} from '@metamask/account-api';
import type { EntropySourceId, KeyringAccount } from '@metamask/keyring-api';
import { KeyringTypes } from '@metamask/keyring-controller';
import {
type KeyringMetadata,
KeyringTypes,
} from '@metamask/keyring-controller';
import type { EthKeyring } from '@metamask/keyring-internal-api';
import { type Hex } from '@metamask/utils';

import type { MultichainAccountGroup } from './MultichainAccountGroup';
import { MultichainAccountWallet } from './MultichainAccountWallet';
import type { NamedAccountProvider } from './providers';
import {
AccountProviderWrapper,
isAccountProviderWrapper,
Expand All @@ -27,7 +32,7 @@ export const serviceName = 'MultichainAccountService';
*/
type MultichainAccountServiceOptions = {
messenger: MultichainAccountServiceMessenger;
providers?: AccountProvider<Bip44Account<KeyringAccount>>[];
providers?: NamedAccountProvider[];
};

/** Reverse mapping object used to map account IDs and their wallet/multichain account. */
Expand All @@ -42,7 +47,7 @@ type AccountContext<Account extends Bip44Account<KeyringAccount>> = {
export class MultichainAccountService {
readonly #messenger: MultichainAccountServiceMessenger;

readonly #providers: AccountProvider<Bip44Account<KeyringAccount>>[];
readonly #providers: NamedAccountProvider[];

readonly #wallets: Map<
MultichainAccountWalletId,
Expand Down Expand Up @@ -124,6 +129,10 @@ export class MultichainAccountService {
'MultichainAccountService:getIsAlignmentInProgress',
() => this.getIsAlignmentInProgress(),
);
this.#messenger.registerActionHandler(
'MultichainAccountService:createMultichainAccountWallet',
(...args) => this.createMultichainAccountWallet(...args),
);

this.#messenger.subscribe('AccountsController:accountAdded', (account) =>
this.#handleOnAccountAdded(account),
Expand Down Expand Up @@ -293,6 +302,58 @@ export class MultichainAccountService {
return Array.from(this.#wallets.values());
}

/**
* Creates a new multichain account wallet with the given mnemonic.
*
* NOTE: This method should only be called in client code where a mutex lock is acquired.
* `discoverAndCreateAccounts` should be called after this method to discover and create accounts.
*
* @param options - Options.
* @param options.mnemonic - The mnemonic to use to create the new wallet.
* @returns The a tuple of the new multichain account wallet and the entropy source id.
*/
async createMultichainAccountWallet({
mnemonic,
}: {
mnemonic: string;
}): Promise<
[MultichainAccountWallet<Bip44Account<KeyringAccount>>, EntropySourceId]
> {
// create a new wallet with the given mnemonic
const result = (await this.#messenger.call(
'KeyringController:withKeyring',
// We intentionally use index -1 to create a new keyring.
{ type: KeyringTypes.hd, index: -1 },
async ({
keyring,
metadata,
}: {
keyring: EthKeyring;
metadata: KeyringMetadata;
}) => {
const accounts = await keyring.getAccounts();
return [accounts, metadata];
},
{ createIfMissing: true, createWithData: { mnemonic } },
)) as [Hex[], KeyringMetadata];

const [accounts, metadata] = result;

// Make sure the keyring has no accounts after creating it.
if (accounts.length > 0) {
throw new Error('Expected keyring with no accounts');
}

const wallet = new MultichainAccountWallet({
providers: this.#providers,
entropySource: metadata.id,
});

this.#wallets.set(wallet.id, wallet);

return [wallet, metadata.id];
}

/**
* Gets a reference to the multichain account group matching this entropy source
* and a group index.
Expand Down
Loading
Loading