Skip to content

Commit 97d2969

Browse files
committed
add account handling
1 parent c6bdba7 commit 97d2969

File tree

8 files changed

+360
-9
lines changed

8 files changed

+360
-9
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"branches": 93.36,
3-
"functions": 97.06,
2+
"branches": 93.37,
3+
"functions": 97.08,
44
"lines": 98.26,
5-
"statements": 97.99
5+
"statements": 98
66
}

packages/snaps-controllers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"@metamask/json-rpc-engine": "^10.0.2",
8585
"@metamask/json-rpc-middleware-stream": "^8.0.7",
8686
"@metamask/key-tree": "^10.0.2",
87+
"@metamask/keyring-internal-api": "^6.0.0",
8788
"@metamask/object-multiplex": "^2.1.0",
8889
"@metamask/permission-controller": "^11.0.6",
8990
"@metamask/phishing-controller": "^12.3.2",

packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ describe('SnapInterfaceController', () => {
185185
);
186186

187187
expect(rootMessenger.call).toHaveBeenNthCalledWith(
188-
3,
188+
4,
189189
'MultichainAssetsController:getState',
190190
);
191191
});
@@ -443,6 +443,66 @@ describe('SnapInterfaceController', () => {
443443
);
444444
});
445445

446+
it('throws if an address passed to an asset selector is not available in the client', async () => {
447+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
448+
const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger(
449+
rootMessenger,
450+
false,
451+
);
452+
453+
rootMessenger.registerActionHandler(
454+
'PhishingController:maybeUpdateState',
455+
jest.fn(),
456+
);
457+
458+
rootMessenger.registerActionHandler(
459+
'PhishingController:testOrigin',
460+
() => ({ result: true, type: 'all' }),
461+
);
462+
463+
rootMessenger.registerActionHandler(
464+
'MultichainAssetsController:getState',
465+
() => ({
466+
assetsMetadata: {},
467+
accountsAssets: {},
468+
}),
469+
);
470+
471+
rootMessenger.registerActionHandler(
472+
'AccountsController:getAccountByAddress',
473+
() => undefined,
474+
);
475+
476+
// eslint-disable-next-line no-new
477+
new SnapInterfaceController({
478+
messenger: controllerMessenger,
479+
});
480+
481+
const element = (
482+
<Box>
483+
<AssetSelector
484+
name="foo"
485+
addresses={['eip155:1:0x1234567890123456789012345678901234567890']}
486+
/>
487+
</Box>
488+
);
489+
490+
await expect(
491+
rootMessenger.call(
492+
'SnapInterfaceController:createInterface',
493+
MOCK_SNAP_ID,
494+
element,
495+
),
496+
).rejects.toThrow(
497+
'Could not find account for address: eip155:1:0x1234567890123456789012345678901234567890',
498+
);
499+
500+
expect(rootMessenger.call).toHaveBeenNthCalledWith(
501+
3,
502+
'AccountsController:getAccountByAddress',
503+
'0x1234567890123456789012345678901234567890',
504+
);
505+
});
446506
it('throws if UI content is too large', async () => {
447507
const rootMessenger = getRootSnapInterfaceControllerMessenger();
448508
const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger(

packages/snaps-controllers/src/interface/SnapInterfaceController.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
ControllerStateChangeEvent,
99
} from '@metamask/base-controller';
1010
import { BaseController } from '@metamask/base-controller';
11+
import type { InternalAccount } from '@metamask/keyring-internal-api';
1112
import type {
1213
MaybeUpdateState,
1314
TestOrigin,
@@ -22,14 +23,15 @@ import type {
2223
import { ContentType } from '@metamask/snaps-sdk';
2324
import type { JSXElement } from '@metamask/snaps-sdk/jsx';
2425
import { getJsonSizeUnsafe, validateJsxLinks } from '@metamask/snaps-utils';
25-
import type { CaipAssetType, Json } from '@metamask/utils';
26-
import { assert, hasProperty } from '@metamask/utils';
26+
import type { CaipAccountId, CaipAssetType, Json } from '@metamask/utils';
27+
import { assert, hasProperty, parseCaipAccountId } from '@metamask/utils';
2728
import { castDraft } from 'immer';
2829
import { nanoid } from 'nanoid';
2930

3031
import {
3132
constructState,
3233
getJsxInterface,
34+
validateAssetSelector,
3335
validateInterfaceContext,
3436
} from './utils';
3537
import type { GetSnap } from '../snaps';
@@ -68,6 +70,11 @@ export type ResolveInterface = {
6870
handler: SnapInterfaceController['resolveInterface'];
6971
};
7072

73+
type AccountsControllerGetAccountByAddressAction = {
74+
type: `AccountsController:getAccountByAddress`;
75+
handler: (address: string) => InternalAccount | undefined;
76+
};
77+
7178
export type SnapInterfaceControllerGetStateAction = ControllerGetStateAction<
7279
typeof controllerName,
7380
SnapInterfaceControllerState
@@ -89,7 +96,8 @@ export type SnapInterfaceControllerAllowedActions =
8996
| HasApprovalRequest
9097
| AcceptRequest
9198
| GetSnap
92-
| MultichainAssetsControllerGetStateAction;
99+
| MultichainAssetsControllerGetStateAction
100+
| AccountsControllerGetAccountByAddressAction;
93101

94102
export type SnapInterfaceControllerActions =
95103
| CreateInterface
@@ -442,6 +450,27 @@ export class SnapInterfaceController extends BaseController<
442450
);
443451
}
444452

453+
/**
454+
* Get an account by its address.
455+
*
456+
* @param address - The account address.
457+
* @returns The account or undefined if not found.
458+
*/
459+
#getAccountByAddress(address: CaipAccountId) {
460+
const { address: parsedAddress } = parseCaipAccountId(address);
461+
462+
return this.messagingSystem.call(
463+
'AccountsController:getAccountByAddress',
464+
parsedAddress,
465+
);
466+
}
467+
468+
/**
469+
* Get the asset metadata for a given asset ID.
470+
*
471+
* @param assetId - The asset ID.
472+
* @returns The asset metadata or undefined if not found.
473+
*/
445474
#getAssetMetadata(assetId: CaipAssetType) {
446475
// TODO: Introduce an action in MultichainAssetsController to get a specific asset metadata.
447476
const assets = this.messagingSystem.call(
@@ -472,6 +501,8 @@ export class SnapInterfaceController extends BaseController<
472501
this.#checkPhishingList.bind(this),
473502
(id: string) => this.messagingSystem.call('SnapController:get', id),
474503
);
504+
505+
validateAssetSelector(element, this.#getAccountByAddress.bind(this));
475506
}
476507

477508
#onNotificationsListUpdated(notificationsList: Notification[]) {

packages/snaps-controllers/src/interface/utils.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
constructState,
2424
getAssetSelectorStateValue,
2525
getJsxInterface,
26+
validateAssetSelector,
2627
} from './utils';
2728

2829
describe('getJsxInterface', () => {
@@ -885,3 +886,37 @@ describe('getAssetSelectorStateValue', () => {
885886
).toBeNull();
886887
});
887888
});
889+
890+
describe('validateAssetSelector', () => {
891+
it('passes if the address is available in the client', () => {
892+
const getAccountByAddress = jest.fn().mockReturnValue({
893+
id: 'foo',
894+
});
895+
896+
expect(
897+
validateAssetSelector(
898+
<AssetSelector
899+
name="foo"
900+
addresses={['eip155:0:0x1234567890123456789012345678901234567890']}
901+
/>,
902+
getAccountByAddress,
903+
),
904+
).toBeUndefined();
905+
});
906+
907+
it('throws if the address is not available in the client', () => {
908+
const getAccountByAddress = jest.fn().mockReturnValue(undefined);
909+
910+
expect(() =>
911+
validateAssetSelector(
912+
<AssetSelector
913+
name="foo"
914+
addresses={['eip155:0:0x1234567890123456789012345678901234567890']}
915+
/>,
916+
getAccountByAddress,
917+
),
918+
).toThrow(
919+
`Could not find account for address: eip155:0:0x1234567890123456789012345678901234567890`,
920+
);
921+
});
922+
});

packages/snaps-controllers/src/interface/utils.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { InternalAccount } from '@metamask/keyring-internal-api';
12
import { assert } from '@metamask/snaps-sdk';
23
import type {
34
FormState,
@@ -28,11 +29,10 @@ import {
2829
getJsxElementFromComponent,
2930
walkJsx,
3031
} from '@metamask/snaps-utils';
31-
import type { CaipAssetType } from '@metamask/utils';
32+
import { type CaipAssetType, type CaipAccountId } from '@metamask/utils';
3233

3334
/**
3435
* A function to get asset metadata.
35-
* This is used to get the metadata of an asset by its ID.
3636
*
3737
* @param assetId - The asset ID.
3838
* @returns The asset metadata or undefined if not found.
@@ -41,6 +41,16 @@ type GetAssetMetadata = (
4141
assetId: CaipAssetType,
4242
) => FungibleAssetMetadata | undefined;
4343

44+
/**
45+
* A function to get an account by its address.
46+
*
47+
* @param address - The account address.
48+
* @returns The account or undefined if not found.
49+
*/
50+
type GetAccountByAddress = (
51+
address: CaipAccountId,
52+
) => InternalAccount | undefined;
53+
4454
/**
4555
* Data getters for elements.
4656
* This is used to get data from elements that is not directly accessible from the element itself.
@@ -80,6 +90,33 @@ export function assertNameIsUnique(state: InterfaceState, name: string) {
8090
);
8191
}
8292

93+
/**
94+
* Validate the asset selector component.
95+
*
96+
* @param node - The JSX node to validate.
97+
* @param getAccountByAddress - A function to get an account by its address.
98+
*
99+
* @throws If the asset selector is invalid.
100+
*/
101+
export function validateAssetSelector(
102+
node: JSXElement,
103+
getAccountByAddress: GetAccountByAddress,
104+
) {
105+
walkJsx(node, (childNode) => {
106+
if (childNode.type !== 'AssetSelector') {
107+
return;
108+
}
109+
110+
// We can assume that the addresses are the same for all CAIP account IDs
111+
const account = getAccountByAddress(childNode.props.addresses[0]);
112+
113+
assert(
114+
account,
115+
`Could not find account for address: ${childNode.props.addresses[0]}`,
116+
);
117+
});
118+
}
119+
83120
/**
84121
* Construct default state for a component.
85122
*

packages/snaps-controllers/src/test-utils/controller.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,7 @@ export const getRestrictedSnapInterfaceControllerMessenger = (
780780
'ApprovalController:hasRequest',
781781
'ApprovalController:acceptRequest',
782782
'MultichainAssetsController:getState',
783+
'AccountsController:getAccountByAddress',
783784
],
784785
allowedEvents: [
785786
'NotificationServicesController:notificationsListUpdated',
@@ -805,6 +806,16 @@ export const getRestrictedSnapInterfaceControllerMessenger = (
805806
accountsAssets: {},
806807
}),
807808
);
809+
810+
messenger.registerActionHandler(
811+
'AccountsController:getAccountByAddress',
812+
// @ts-expect-error partial mock
813+
(address: string) => ({
814+
address,
815+
id: 'foo',
816+
scopes: ['eip155:0'],
817+
}),
818+
);
808819
}
809820

810821
return snapInterfaceControllerMessenger;

0 commit comments

Comments
 (0)