Skip to content

Commit ce71df1

Browse files
committed
feat(multichain-account-service): use withKeyringV2 for Snap account providers
1 parent f77569d commit ce71df1

7 files changed

Lines changed: 224 additions & 229 deletions

File tree

packages/multichain-account-service/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12+
- **BREAKING:** Replace `KeyringController:withKeyring` with `KeyringController:withKeyringV2` for the Snap account providers ([#8732](https://github.com/MetaMask/core/pull/8732))
1213
- **BREAKING:** The service messenger now requires the `SnapAccountService:ensureReady` action to be declared ([#8715](https://github.com/MetaMask/core/pull/8715))
1314
- **BREAKING:** Delegate Snap platform readiness to `@metamask/snap-account-service` ([#8715](https://github.com/MetaMask/core/pull/8715)), ([#8752](https://github.com/MetaMask/core/pull/8752))
1415
- Removed `MultichainAccountService.ensureCanUseSnapPlatform()` method and the corresponding `MultichainAccountService:ensureCanUseSnapPlatform` messenger action.

packages/multichain-account-service/src/providers/BaseBip44AccountProvider.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type {
1111
} from '@metamask/keyring-api/v2';
1212
import type {
1313
KeyringMetadata,
14-
KeyringSelector,
1514
KeyringSelectorV2,
1615
} from '@metamask/keyring-controller';
1716
import type { InternalAccount } from '@metamask/keyring-internal-api';
@@ -159,40 +158,6 @@ export abstract class BaseBip44AccountProvider<
159158
) as unknown as Account;
160159
}
161160

162-
/**
163-
* Run an operation against a V1 keyring selected by `selector`.
164-
*
165-
* Forwards to `KeyringController:withKeyring`. Use this for keyrings that
166-
* have not yet migrated to the unified V2 `Keyring` interface (e.g. the
167-
* snap keyring).
168-
*
169-
* @param selector - The selector identifying the keyring.
170-
* @param operation - The operation to run with the selected keyring.
171-
* @returns The result of the operation.
172-
*/
173-
protected async withKeyring<SelectedKeyring, CallbackResult = void>(
174-
selector: KeyringSelector,
175-
operation: ({
176-
keyring,
177-
metadata,
178-
}: {
179-
keyring: SelectedKeyring;
180-
metadata: KeyringMetadata;
181-
}) => Promise<CallbackResult>,
182-
): Promise<CallbackResult> {
183-
const result = await this.messenger.call(
184-
'KeyringController:withKeyring',
185-
selector,
186-
({ keyring, metadata }) =>
187-
operation({
188-
keyring: keyring as SelectedKeyring,
189-
metadata,
190-
}),
191-
);
192-
193-
return result as CallbackResult;
194-
}
195-
196161
/**
197162
* Run an operation against a V2 keyring selected by `selector`.
198163
*

packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts

Lines changed: 37 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { isBip44Account } from '@metamask/account-api';
2-
import type { SnapKeyring } from '@metamask/eth-snap-keyring';
32
import { AccountCreationType, BtcAccountType } from '@metamask/keyring-api';
43
import type { KeyringMetadata } from '@metamask/keyring-controller';
5-
import type {
6-
EthKeyring,
7-
InternalAccount,
8-
} from '@metamask/keyring-internal-api';
4+
import type { InternalAccount } from '@metamask/keyring-internal-api';
95
import { SnapControllerState } from '@metamask/snaps-controllers';
106
import deepmerge from 'deepmerge';
117

@@ -69,9 +65,9 @@ class MockBtcKeyring {
6965
return Number(index);
7066
}
7167

72-
createAccount: SnapKeyring['createAccount'] = jest
68+
createAccount = jest
7369
.fn()
74-
.mockImplementation((_, { derivationPath, index, ...options }) => {
70+
.mockImplementation(({ derivationPath, index, ...options }) => {
7571
// Determine the group index to use - either from derivationPath parsing, explicit index, or fallback
7672
let groupIndex: number;
7773

@@ -110,34 +106,32 @@ class MockBtcKeyring {
110106
return account;
111107
});
112108

113-
createAccounts: SnapKeyring['createAccounts'] = jest
114-
.fn()
115-
.mockImplementation((_, options) => {
116-
const groupIndices =
117-
options.type === 'bip44:derive-index'
118-
? [options.groupIndex]
119-
: toGroupIndexRangeArray(options.range);
120-
121-
return groupIndices.map((groupIndex) => {
122-
const found = this.accounts.find(
123-
(account) =>
124-
isBip44Account(account) &&
125-
account.options.entropy.groupIndex === groupIndex,
126-
);
127-
128-
if (found) {
129-
return found; // Idempotent.
130-
}
131-
132-
const account = MockAccountBuilder.from(MOCK_BTC_P2WPKH_ACCOUNT_1)
133-
.withUuid()
134-
.withAddressSuffix(`${groupIndex}`)
135-
.withGroupIndex(groupIndex)
136-
.get();
137-
this.accounts.push(account);
138-
return account;
139-
});
109+
createAccounts = jest.fn().mockImplementation((options) => {
110+
const groupIndices =
111+
options.type === 'bip44:derive-index'
112+
? [options.groupIndex]
113+
: toGroupIndexRangeArray(options.range);
114+
115+
return groupIndices.map((groupIndex) => {
116+
const found = this.accounts.find(
117+
(account) =>
118+
isBip44Account(account) &&
119+
account.options.entropy.groupIndex === groupIndex,
120+
);
121+
122+
if (found) {
123+
return found; // Idempotent.
124+
}
125+
126+
const account = MockAccountBuilder.from(MOCK_BTC_P2WPKH_ACCOUNT_1)
127+
.withUuid()
128+
.withAddressSuffix(`${groupIndex}`)
129+
.withGroupIndex(groupIndex)
130+
.get();
131+
this.accounts.push(account);
132+
return account;
140133
});
134+
});
141135
}
142136

143137
class MockBtcAccountProvider extends BtcAccountProvider {
@@ -212,12 +206,10 @@ function setup({
212206
);
213207

214208
messenger.registerActionHandler(
215-
'KeyringController:withKeyring',
209+
'KeyringController:withKeyringV2',
216210
async (_, operation) =>
217211
operation({
218-
// We type-cast here, since `withKeyring` defaults to `EthKeyring` and the
219-
// Snap keyring doesn't really implement this interface (this is expected).
220-
keyring: keyring as unknown as EthKeyring,
212+
keyring,
221213
metadata: keyring.metadata,
222214
}),
223215
);
@@ -243,8 +235,8 @@ function setup({
243235
mocks: {
244236
handleRequest: mockHandleRequest,
245237
keyring: {
246-
createAccount: keyring.createAccount as jest.Mock,
247-
createAccounts: keyring.createAccounts as jest.Mock,
238+
createAccount: keyring.createAccount,
239+
createAccounts: keyring.createAccounts,
248240
},
249241
trace: mockTrace,
250242
},
@@ -483,14 +475,11 @@ describe('BtcAccountProvider', () => {
483475
});
484476
expect(newAccounts).toHaveLength(1);
485477
// Batch endpoint must be called, NOT the singular one.
486-
expect(mocks.keyring.createAccounts).toHaveBeenCalledWith(
487-
BtcAccountProvider.BTC_SNAP_ID,
488-
{
489-
type: AccountCreationType.Bip44DeriveIndex,
490-
entropySource: MOCK_HD_KEYRING_1.metadata.id,
491-
groupIndex: newGroupIndex,
492-
},
493-
);
478+
expect(mocks.keyring.createAccounts).toHaveBeenCalledWith({
479+
type: AccountCreationType.Bip44DeriveIndex,
480+
entropySource: MOCK_HD_KEYRING_1.metadata.id,
481+
groupIndex: newGroupIndex,
482+
});
494483
expect(mocks.keyring.createAccount).not.toHaveBeenCalled();
495484
});
496485

packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
KeyringCapabilities,
1717
} from '@metamask/keyring-api';
1818
import type { EntropySourceId, KeyringAccount } from '@metamask/keyring-api';
19+
import type { KeyringMetadata } from '@metamask/keyring-controller';
1920
import type { InternalAccount } from '@metamask/keyring-internal-api';
2021
import type { JsonRpcRequest, SnapId } from '@metamask/snaps-sdk';
2122
import deepmerge from 'deepmerge';
@@ -153,10 +154,12 @@ const setup = ({
153154
config: configOverride = {},
154155
messenger = getRootMessenger(),
155156
accounts = [],
157+
keyring: keyringOverrides = {},
156158
}: {
157159
config?: DeepPartial<SnapAccountProviderConfig>;
158160
messenger?: RootMessenger;
159161
accounts?: InternalAccount[];
162+
keyring?: { type?: string; snapId?: SnapId };
160163
} = {}) => {
161164
const mocks = {
162165
AccountsController: {
@@ -165,6 +168,9 @@ const setup = ({
165168
ErrorReportingService: {
166169
captureException: jest.fn(),
167170
},
171+
KeyringController: {
172+
withKeyringV2: jest.fn(),
173+
},
168174
SnapController: {
169175
handleKeyringRequest: {
170176
getAccount: jest.fn(),
@@ -223,18 +229,30 @@ const setup = ({
223229
);
224230

225231
const keyring = {
232+
type: keyringOverrides.type ?? 'snap',
233+
snapId: keyringOverrides.snapId ?? TEST_SNAP_ID,
226234
createAccount: jest.fn(),
227235
createAccounts: jest.fn(),
228236
removeAccount: jest.fn(),
237+
lookupByAddress: jest
238+
.fn()
239+
.mockImplementation((address: string) =>
240+
accounts.map(asKeyringAccount).find((a) => a.address === address),
241+
),
229242
};
243+
const metadata = { id: 'mock-keyring-id', name: '' } as KeyringMetadata;
230244

245+
mocks.KeyringController.withKeyringV2.mockImplementation(
246+
async (selector, operation) => {
247+
if (selector.filter && !selector.filter(keyring, metadata)) {
248+
throw new Error('No keyring matches the selector');
249+
}
250+
return await operation({ keyring, metadata });
251+
},
252+
);
231253
messenger.registerActionHandler(
232-
'KeyringController:withKeyring',
233-
jest
234-
.fn()
235-
.mockImplementation(
236-
async (_ /* selector */, operation) => await operation({ keyring }),
237-
),
254+
'KeyringController:withKeyringV2',
255+
mocks.KeyringController.withKeyringV2,
238256
);
239257

240258
const serviceMessenger = getMultichainAccountServiceMessenger(messenger);
@@ -866,9 +884,8 @@ describe('SnapAccountProvider', () => {
866884
).toHaveBeenCalledWith(extraSnapAccount2.id);
867885

868886
// Should remove from keyring and recreate the missing account
869-
expect(keyring.removeAccount).toHaveBeenCalledWith(
870-
mockAccounts[1].address,
871-
);
887+
// (the keyring has extraSnapAccount2 with the same address as mockAccounts[1])
888+
expect(keyring.removeAccount).toHaveBeenCalledWith(extraSnapAccount2.id);
872889
expect(createAccountsSpy).toHaveBeenCalledWith({
873890
entropySource: mockAccounts[1].options.entropy.id,
874891
groupIndex: mockAccounts[1].options.entropy.groupIndex,
@@ -949,6 +966,37 @@ describe('SnapAccountProvider', () => {
949966
});
950967
});
951968

969+
describe('withKeyringV2 selector', () => {
970+
const mockAccounts = [
971+
MockAccountBuilder.from(MOCK_HD_ACCOUNT_1)
972+
.withUuid()
973+
.withSnapId(TEST_SNAP_ID)
974+
.get(),
975+
].filter(isBip44Account);
976+
977+
it('rejects when the keyring type is not a Snap keyring', async () => {
978+
const { provider } = setup({
979+
accounts: mockAccounts,
980+
keyring: { type: 'not-a-snap-keyring' },
981+
});
982+
983+
await expect(provider.resyncAccounts(mockAccounts)).rejects.toThrow(
984+
'No keyring matches the selector',
985+
);
986+
});
987+
988+
it('rejects when the Snap keyring is for a different Snap ID', async () => {
989+
const { provider } = setup({
990+
accounts: mockAccounts,
991+
keyring: { snapId: 'npm:@metamask/other-snap' as SnapId },
992+
});
993+
994+
await expect(provider.resyncAccounts(mockAccounts)).rejects.toThrow(
995+
'No keyring matches the selector',
996+
);
997+
});
998+
});
999+
9521000
describe('ensureReady', () => {
9531001
it('delegates Snap platform readiness check to SnapAccountService:ensureReady', async () => {
9541002
const { provider, mocks } = setup();

0 commit comments

Comments
 (0)