Skip to content

Commit 9255484

Browse files
authored
feat(multichain-account-service)!: add error reporting around account creation (#7044)
## Explanation Used the error reporting service to capture exceptions around account creation. ## References n/a ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Integrates `ErrorReportingService` to capture exceptions during multichain account creation, alignment, and discovery, adds a `createSentryError` utility, updates messenger/types, and adjusts tests and peer deps. > > - **multichain-account-service**: > - **Error reporting integration**: > - In `MultichainAccountWallet` and `MultichainAccountGroup`, wrap provider failures in `createSentryError` and call `ErrorReportingService:captureException` during `createAccounts` (EVM and non‑EVM), `alignAccounts`, and `discoverAccounts`. > - **Utilities**: > - Add `utils#createSentryError` to attach `cause` and `context` to errors. > - **Messaging/Types**: > - Extend `MultichainAccountServiceMessenger` allowed actions to include `ErrorReportingService:captureException`; update test messenger delegation. > - **Tests**: > - Update unit tests to register/mock `ErrorReportingService:captureException` and assert calls on provider failures. > - **Changelog/Deps**: > - Update `CHANGELOG.md` with BREAKING note; add `@metamask/error-reporting-service` as a peer dependency (and devDependency). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 19411d9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 1894123 commit 9255484

File tree

10 files changed

+198
-20
lines changed

10 files changed

+198
-20
lines changed

packages/multichain-account-service/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- **BREAKING:** Added error reporting around account creation with the `ErrorReportingService` ([#7044](https://github.com/MetaMask/core/pull/7044))
13+
- The `@metamask/error-reporting-service` is now a peer dependency.
14+
1015
### Changed
1116

1217
- Limit Bitcoin and Tron providers to 3 concurrent account creations by default when creating multichain account groups ([#7052](https://github.com/MetaMask/core/pull/7052))

packages/multichain-account-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"@metamask/account-api": "^0.12.0",
6868
"@metamask/accounts-controller": "^34.0.0",
6969
"@metamask/auto-changelog": "^3.4.4",
70+
"@metamask/error-reporting-service": "^3.0.0",
7071
"@metamask/eth-hd-keyring": "^13.0.0",
7172
"@metamask/keyring-controller": "^24.0.0",
7273
"@metamask/providers": "^22.1.0",
@@ -86,6 +87,7 @@
8687
"peerDependencies": {
8788
"@metamask/account-api": "^0.12.0",
8889
"@metamask/accounts-controller": "^34.0.0",
90+
"@metamask/error-reporting-service": "^3.0.0",
8991
"@metamask/keyring-controller": "^24.0.0",
9092
"@metamask/providers": "^22.0.0",
9193
"@metamask/snaps-controllers": "^14.0.0",

packages/multichain-account-service/src/MultichainAccountGroup.test.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
getRootMessenger,
2424
type RootMessenger,
2525
} from './tests';
26+
import type { MultichainAccountServiceMessenger } from './types';
2627

2728
function setup({
2829
groupIndex = 0,
@@ -44,25 +45,32 @@ function setup({
4445
wallet: MultichainAccountWallet<Bip44Account<InternalAccount>>;
4546
group: MultichainAccountGroup<Bip44Account<InternalAccount>>;
4647
providers: MockAccountProvider[];
48+
messenger: MultichainAccountServiceMessenger;
4749
} {
4850
const providers = accounts.map((providerAccounts) => {
4951
return setupNamedAccountProvider({ accounts: providerAccounts });
5052
});
5153

54+
const serviceMessenger = getMultichainAccountServiceMessenger(messenger);
55+
messenger.registerActionHandler(
56+
'ErrorReportingService:captureException',
57+
jest.fn(),
58+
);
59+
5260
const wallet = new MultichainAccountWallet<Bip44Account<InternalAccount>>({
5361
entropySource: MOCK_WALLET_1_ENTROPY_SOURCE,
54-
messenger: getMultichainAccountServiceMessenger(messenger),
62+
messenger: serviceMessenger,
5563
providers,
5664
});
5765

5866
const group = new MultichainAccountGroup({
5967
wallet,
6068
groupIndex,
6169
providers,
62-
messenger: getMultichainAccountServiceMessenger(messenger),
70+
messenger: serviceMessenger,
6371
});
6472

65-
return { wallet, group, providers };
73+
return { wallet, group, providers, messenger: serviceMessenger };
6674
}
6775

6876
describe('MultichainAccount', () => {
@@ -213,5 +221,22 @@ describe('MultichainAccount', () => {
213221
`Failed to fully align multichain account group for entropy ID: ${wallet.entropySource} and group index: ${groupIndex}, some accounts might be missing`,
214222
);
215223
});
224+
225+
it('captures an error when a provider fails to create its account', async () => {
226+
const groupIndex = 0;
227+
const { group, providers, messenger } = setup({
228+
groupIndex,
229+
accounts: [[MOCK_WALLET_1_EVM_ACCOUNT], []],
230+
});
231+
const providerError = new Error('Unable to create accounts');
232+
providers[1].createAccounts.mockRejectedValueOnce(providerError);
233+
const callSpy = jest.spyOn(messenger, 'call');
234+
await group.alignAccounts();
235+
expect(callSpy).toHaveBeenCalledWith(
236+
'ErrorReportingService:captureException',
237+
new Error('Unable to align accounts with provider "Mocked Provider"'),
238+
);
239+
expect(callSpy.mock.lastCall[1]).toHaveProperty('cause', providerError);
240+
});
216241
});
217242
});

packages/multichain-account-service/src/MultichainAccountGroup.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import type { MultichainAccountWallet } from './MultichainAccountWallet';
1818
import type { NamedAccountProvider } from './providers';
1919
import type { MultichainAccountServiceMessenger } from './types';
20+
import { createSentryError } from './utils';
2021

2122
/**
2223
* A multichain account group that holds multiple accounts.
@@ -236,20 +237,40 @@ export class MultichainAccountGroup<
236237

237238
const results = await Promise.allSettled(
238239
this.#providers.map(async (provider) => {
239-
const accounts = this.#providerToAccounts.get(provider);
240-
if (!accounts || accounts.length === 0) {
240+
try {
241+
const accounts = this.#providerToAccounts.get(provider);
242+
if (!accounts || accounts.length === 0) {
243+
this.#log(
244+
`Found missing accounts for account provider "${provider.getName()}", creating them now...`,
245+
);
246+
const created = await provider.createAccounts({
247+
entropySource: this.wallet.entropySource,
248+
groupIndex: this.groupIndex,
249+
});
250+
this.#log(`Created ${created.length} accounts`);
251+
252+
return created;
253+
}
254+
return Promise.resolve();
255+
} catch (error) {
256+
// istanbul ignore next
241257
this.#log(
242-
`Found missing accounts for account provider "${provider.getName()}", creating them now...`,
258+
`${WARNING_PREFIX} ${error instanceof Error ? error.message : String(error)}`,
243259
);
244-
const created = await provider.createAccounts({
245-
entropySource: this.wallet.entropySource,
246-
groupIndex: this.groupIndex,
247-
});
248-
this.#log(`Created ${created.length} accounts`);
249-
250-
return created;
260+
const sentryError = createSentryError(
261+
`Unable to align accounts with provider "${provider.getName()}"`,
262+
error as Error,
263+
{
264+
groupIndex: this.groupIndex,
265+
provider: provider.getName(),
266+
},
267+
);
268+
this.#messenger.call(
269+
'ErrorReportingService:captureException',
270+
sentryError,
271+
);
272+
throw error;
251273
}
252-
return Promise.resolve();
253274
}),
254275
);
255276

packages/multichain-account-service/src/MultichainAccountWallet.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ function setup({
6767

6868
const serviceMessenger = getMultichainAccountServiceMessenger(messenger);
6969

70+
messenger.registerActionHandler(
71+
'ErrorReportingService:captureException',
72+
jest.fn(),
73+
);
74+
7075
const wallet = new MultichainAccountWallet<Bip44Account<InternalAccount>>({
7176
entropySource,
7277
providers,
@@ -365,6 +370,27 @@ describe('MultichainAccountWallet', () => {
365370
);
366371
});
367372

373+
it('captures an error when a provider fails to create its account', async () => {
374+
const groupIndex = 1;
375+
const { wallet, providers, messenger } = setup({
376+
accounts: [[MOCK_HD_ACCOUNT_1]],
377+
});
378+
const [provider] = providers;
379+
const providerError = new Error('Unable to create accounts');
380+
provider.createAccounts.mockRejectedValueOnce(providerError);
381+
const callSpy = jest.spyOn(messenger, 'call');
382+
await expect(
383+
wallet.createMultichainAccountGroup(groupIndex),
384+
).rejects.toThrow(
385+
'Unable to create multichain account group for index: 1',
386+
);
387+
expect(callSpy).toHaveBeenCalledWith(
388+
'ErrorReportingService:captureException',
389+
new Error('Unable to create account with provider "Mocked Provider 0"'),
390+
);
391+
expect(callSpy.mock.lastCall[1]).toHaveProperty('cause', providerError);
392+
});
393+
368394
it('aggregates non-EVM failures when waiting for all providers', async () => {
369395
const startingIndex = 0;
370396

@@ -704,5 +730,22 @@ describe('MultichainAccountWallet', () => {
704730
// Other provider proceeds normally
705731
expect(providers[1].discoverAccounts).toHaveBeenCalledTimes(1);
706732
});
733+
734+
it('captures an error when a provider fails to discover its accounts', async () => {
735+
const { wallet, providers, messenger } = setup({
736+
accounts: [[], []],
737+
});
738+
const providerError = new Error('Unable to discover accounts');
739+
providers[0].discoverAccounts.mockRejectedValueOnce(providerError);
740+
const callSpy = jest.spyOn(messenger, 'call');
741+
// Ensure the other provider stops immediately to finish the Promise.all
742+
providers[1].discoverAccounts.mockResolvedValueOnce([]);
743+
await wallet.discoverAccounts();
744+
expect(callSpy).toHaveBeenCalledWith(
745+
'ErrorReportingService:captureException',
746+
new Error('Unable to discover accounts'),
747+
);
748+
expect(callSpy.mock.lastCall[1]).toHaveProperty('cause', providerError);
749+
});
707750
});
708751
});

packages/multichain-account-service/src/MultichainAccountWallet.ts

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
import { MultichainAccountGroup } from './MultichainAccountGroup';
2727
import { EvmAccountProvider, type NamedAccountProvider } from './providers';
2828
import type { MultichainAccountServiceMessenger } from './types';
29-
import { toRejectedErrorMessage } from './utils';
29+
import { createSentryError, toRejectedErrorMessage } from './utils';
3030

3131
/**
3232
* The context for a provider discovery.
@@ -244,10 +244,26 @@ export class MultichainAccountWallet<
244244
}): Promise<void> {
245245
if (awaitAll) {
246246
const tasks = providers.map((provider) =>
247-
provider.createAccounts({
248-
entropySource: this.#entropySource,
249-
groupIndex,
250-
}),
247+
provider
248+
.createAccounts({
249+
entropySource: this.#entropySource,
250+
groupIndex,
251+
})
252+
.catch((error) => {
253+
const sentryError = createSentryError(
254+
`Unable to create account with provider "${provider.getName()}"`,
255+
error,
256+
{
257+
groupIndex,
258+
provider: provider.getName(),
259+
},
260+
);
261+
this.#messenger.call(
262+
'ErrorReportingService:captureException',
263+
sentryError,
264+
);
265+
throw error;
266+
}),
251267
);
252268

253269
const results = await Promise.allSettled(tasks);
@@ -276,6 +292,18 @@ export class MultichainAccountWallet<
276292
.catch((error) => {
277293
const errorMessage = `Unable to create multichain account group for index: ${groupIndex} (background mode with provider "${provider.getName()}")`;
278294
this.#log(`${WARNING_PREFIX} ${errorMessage}:`, error);
295+
const sentryError = createSentryError(
296+
`Unable to create account with provider "${provider.getName()}"`,
297+
error,
298+
{
299+
groupIndex,
300+
provider: provider.getName(),
301+
},
302+
);
303+
this.#messenger.call(
304+
'ErrorReportingService:captureException',
305+
sentryError,
306+
);
279307
});
280308
});
281309
}
@@ -411,6 +439,18 @@ export class MultichainAccountWallet<
411439
} catch (error) {
412440
const errorMessage = `Unable to create multichain account group for index: ${groupIndex} with provider "${evmProvider.getName()}". Error: ${(error as Error).message}`;
413441
this.#log(`${ERROR_PREFIX} ${errorMessage}:`, error);
442+
const sentryError = createSentryError(
443+
`Unable to create account with provider "${evmProvider.getName()}"`,
444+
error as Error,
445+
{
446+
groupIndex,
447+
provider: evmProvider.getName(),
448+
},
449+
);
450+
this.#messenger.call(
451+
'ErrorReportingService:captureException',
452+
sentryError,
453+
);
414454
throw new Error(errorMessage);
415455
}
416456

@@ -575,6 +615,18 @@ export class MultichainAccountWallet<
575615
),
576616
error,
577617
);
618+
const sentryError = createSentryError(
619+
'Unable to discover accounts',
620+
error as Error,
621+
{
622+
provider: providerName,
623+
groupIndex: targetGroupIndex,
624+
},
625+
);
626+
this.#messenger.call(
627+
'ErrorReportingService:captureException',
628+
sentryError,
629+
);
578630
break;
579631
}
580632

packages/multichain-account-service/src/tests/messenger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function getMultichainAccountServiceMessenger(
5555
'AccountsController:getAccount',
5656
'AccountsController:getAccountByAddress',
5757
'AccountsController:listMultichainAccounts',
58+
'ErrorReportingService:captureException',
5859
'SnapController:handleRequest',
5960
'KeyringController:withKeyring',
6061
'KeyringController:getState',

packages/multichain-account-service/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
AccountsControllerGetAccountByAddressAction,
1212
AccountsControllerListMultichainAccountsAction,
1313
} from '@metamask/accounts-controller';
14+
import type { ErrorReportingServiceCaptureExceptionAction } from '@metamask/error-reporting-service';
1415
import type { KeyringAccount } from '@metamask/keyring-api';
1516
import type {
1617
KeyringControllerAddNewKeyringAction,
@@ -135,7 +136,8 @@ type AllowedActions =
135136
| KeyringControllerGetKeyringsByTypeAction
136137
| KeyringControllerAddNewKeyringAction
137138
| NetworkControllerGetNetworkClientByIdAction
138-
| NetworkControllerFindNetworkClientIdByChainIdAction;
139+
| NetworkControllerFindNetworkClientIdByChainIdAction
140+
| ErrorReportingServiceCaptureExceptionAction;
139141

140142
/**
141143
* All events published by other modules that {@link MultichainAccountService}

packages/multichain-account-service/src/utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,28 @@ export const toRejectedErrorMessage = <Result>(
1010
}
1111
return errorMessage;
1212
};
13+
14+
/**
15+
* Creates a Sentry error from an error message, an inner error and a context.
16+
*
17+
* NOTE: Sentry defaults to a depth of 3 when extracting non-native attributes.
18+
* As such, the context depth shouldn't be too deep.
19+
*
20+
* @param msg - The error message to create a Sentry error from.
21+
* @param innerError - The inner error to create a Sentry error from.
22+
* @param context - The context to add to the Sentry error.
23+
* @returns A Sentry error.
24+
*/
25+
export const createSentryError = (
26+
msg: string,
27+
innerError: Error,
28+
context: Record<string, unknown>,
29+
) => {
30+
const error = new Error(msg) as Error & {
31+
cause: Error;
32+
context: typeof context;
33+
};
34+
error.cause = innerError;
35+
error.context = context;
36+
return error;
37+
};

yarn.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4197,6 +4197,7 @@ __metadata:
41974197
"@metamask/accounts-controller": "npm:^34.0.0"
41984198
"@metamask/auto-changelog": "npm:^3.4.4"
41994199
"@metamask/base-controller": "npm:^9.0.0"
4200+
"@metamask/error-reporting-service": "npm:^3.0.0"
42004201
"@metamask/eth-hd-keyring": "npm:^13.0.0"
42014202
"@metamask/eth-snap-keyring": "npm:^18.0.0"
42024203
"@metamask/key-tree": "npm:^10.1.1"
@@ -4227,6 +4228,7 @@ __metadata:
42274228
peerDependencies:
42284229
"@metamask/account-api": ^0.12.0
42294230
"@metamask/accounts-controller": ^34.0.0
4231+
"@metamask/error-reporting-service": ^3.0.0
42304232
"@metamask/keyring-controller": ^24.0.0
42314233
"@metamask/providers": ^22.0.0
42324234
"@metamask/snaps-controllers": ^14.0.0

0 commit comments

Comments
 (0)