Skip to content

Commit 27a8659

Browse files
committed
Add AssetSelector to snaps-sdk
1 parent ea582cf commit 27a8659

File tree

7 files changed

+208
-3
lines changed

7 files changed

+208
-3
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { AssetSelector } from './AssetSelector';
2+
3+
describe('AssetSelector', () => {
4+
it('renders an asset selector', () => {
5+
const result = (
6+
<AssetSelector
7+
addresses={['eip155:0:0x1234567890123456789012345678901234567890']}
8+
/>
9+
);
10+
11+
expect(result).toStrictEqual({
12+
type: 'AssetSelector',
13+
props: {
14+
addresses: ['eip155:0:0x1234567890123456789012345678901234567890'],
15+
},
16+
key: null,
17+
});
18+
});
19+
20+
it('renders an asset selector with optional props', () => {
21+
const result = (
22+
<AssetSelector
23+
addresses={['eip155:0:0x1234567890123456789012345678901234567890']}
24+
chainIds={['eip155:1']}
25+
value="eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f"
26+
disabled={true}
27+
/>
28+
);
29+
30+
expect(result).toStrictEqual({
31+
type: 'AssetSelector',
32+
props: {
33+
addresses: ['eip155:0:0x1234567890123456789012345678901234567890'],
34+
chainIds: ['eip155:1'],
35+
value: 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f',
36+
disabled: true,
37+
},
38+
key: null,
39+
});
40+
});
41+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { CaipAssetTypeOrId, CaipChainId } from '@metamask/utils';
2+
import type { MatchingAddressesCaipAccountIdList } from 'src/types';
3+
4+
import { createSnapComponent } from '../../component';
5+
6+
/**
7+
* The props of the {@link AssetSelector} component.
8+
*
9+
* @property addresses - The addresses of the account to pull the assets from.
10+
* Only one address is supported, but different chains can be used.
11+
* @property chainIds - The chain IDs to filter the assets.
12+
* @property value - The selected value of the asset selector.
13+
* @property disabled - Whether the asset selector is disabled.
14+
*/
15+
export type AssetSelectorProps = {
16+
addresses: MatchingAddressesCaipAccountIdList;
17+
chainIds?: CaipChainId[] | undefined;
18+
value?: CaipAssetTypeOrId | undefined;
19+
disabled?: boolean | undefined;
20+
};
21+
22+
const TYPE = 'AssetSelector';
23+
24+
/**
25+
* An asset selector component, which is used to create an asset selector.
26+
*
27+
* @param props - The props of the component.
28+
* @param props.addresses - The addresses of the account to pull the assets from.
29+
* @param props.chainIds - The chain IDs to filter the assets.
30+
* @param props.value - The selected value of the asset selector.
31+
* @param props.disabled - Whether the asset selector is disabled.
32+
* @returns An asset selector element.
33+
* @example
34+
* <AssetSelector
35+
* addresses={[
36+
* 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv',
37+
* 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1:7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv'
38+
* ]}
39+
* value="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
40+
* />
41+
* @example
42+
* <AssetSelector
43+
* addresses={['eip155:0:0x1234567890123456789012345678901234567890']}
44+
* chainIds={['eip155:1']}
45+
* />
46+
*/
47+
export const AssetSelector = createSnapComponent<
48+
AssetSelectorProps,
49+
typeof TYPE
50+
>(TYPE);
51+
52+
export type AssetSelectorElement = ReturnType<typeof AssetSelector>;

packages/snaps-sdk/src/jsx/components/form/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { AssetSelectorElement } from './AssetSelector';
12
import type { ButtonElement } from './Button';
23
import type { CheckboxElement } from './Checkbox';
34
import type { DropdownElement } from './Dropdown';
@@ -11,6 +12,7 @@ import type { RadioGroupElement } from './RadioGroup';
1112
import type { SelectorElement } from './Selector';
1213
import type { SelectorOptionElement } from './SelectorOption';
1314

15+
export * from './AssetSelector';
1416
export * from './Button';
1517
export * from './Checkbox';
1618
export * from './Dropdown';
@@ -25,6 +27,7 @@ export * from './Selector';
2527
export * from './SelectorOption';
2628

2729
export type StandardFormElement =
30+
| AssetSelectorElement
2831
| ButtonElement
2932
| CheckboxElement
3033
| FormElement

packages/snaps-sdk/src/jsx/validation.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
} from '@metamask/superstruct';
2424
import {
2525
CaipAccountIdStruct,
26+
CaipAssetTypeOrIdStruct,
27+
CaipChainIdStruct,
2628
hasProperty,
2729
HexChecksumAddressStruct,
2830
isPlainObject,
@@ -40,6 +42,7 @@ import type {
4042
StringElement,
4143
} from './component';
4244
import type {
45+
AssetSelectorElement,
4346
AvatarElement,
4447
SkeletonElement,
4548
AddressElement,
@@ -86,7 +89,10 @@ import {
8689
svg,
8790
typedUnion,
8891
} from '../internals';
89-
import type { EmptyObject } from '../types';
92+
import {
93+
MatchingAddressesCaipAccountIdListStruct,
94+
type EmptyObject,
95+
} from '../types';
9096

9197
/**
9298
* A struct for the {@link Key} type.
@@ -407,6 +413,22 @@ export const SelectorStruct: Describe<SelectorElement> = element('Selector', {
407413
disabled: optional(boolean()),
408414
});
409415

416+
/**
417+
* A struct for the {@link AssetSelectorElement} type.
418+
*/
419+
export const AssetSelectorStruct: Describe<AssetSelectorElement> = element(
420+
'AssetSelector',
421+
{
422+
addresses: MatchingAddressesCaipAccountIdListStruct,
423+
chainIds: optional(array(CaipChainIdStruct)) as unknown as Struct<
424+
Infer<typeof CaipChainIdStruct>[] | undefined,
425+
null
426+
>,
427+
value: optional(CaipAssetTypeOrIdStruct),
428+
disabled: optional(boolean()),
429+
},
430+
);
431+
410432
/**
411433
* A struct for the {@link RadioElement} type.
412434
*/
@@ -475,13 +497,15 @@ const BOX_INPUT_BOTH = [
475497
* A subset of JSX elements that are allowed as single children of the Field component.
476498
*/
477499
const FIELD_CHILDREN_ARRAY = [
500+
AssetSelectorStruct,
478501
InputStruct,
479502
DropdownStruct,
480503
RadioGroupStruct,
481504
FileInputStruct,
482505
CheckboxStruct,
483506
SelectorStruct,
484507
] as [
508+
typeof AssetSelectorStruct,
485509
typeof InputStruct,
486510
typeof DropdownStruct,
487511
typeof RadioGroupStruct,
@@ -873,6 +897,7 @@ export const SpinnerStruct: Describe<SpinnerElement> = element('Spinner');
873897
*/
874898
export const BoxChildStruct = typedUnion([
875899
AddressStruct,
900+
AssetSelectorStruct,
876901
BoldStruct,
877902
BoxStruct,
878903
ButtonStruct,
@@ -936,6 +961,7 @@ export const RootJSXElementStruct = typedUnion([
936961
* A struct for the {@link JSXElement} type.
937962
*/
938963
export const JSXElementStruct: Describe<JSXElement> = typedUnion([
964+
AssetSelectorStruct,
939965
ButtonStruct,
940966
InputStruct,
941967
FileInputStruct,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { is } from '@metamask/superstruct';
2+
3+
import { MatchingAddressesCaipAccountIdListStruct } from './caip';
4+
5+
describe('MatchingAddressesCaipAccountIdListStruct', () => {
6+
it('validates an array of matchin addresses', () => {
7+
expect(
8+
is(
9+
[
10+
'eip155:1:0x1234567890123456789012345678901234567890',
11+
'eip155:2:0x1234567890123456789012345678901234567890',
12+
'eip155:3:0x1234567890123456789012345678901234567890',
13+
],
14+
MatchingAddressesCaipAccountIdListStruct,
15+
),
16+
).toBe(true);
17+
});
18+
19+
it("doesn't validate an array of mismatching addresses", () => {
20+
expect(
21+
is(
22+
[
23+
'eip155:1:0x1234567890123456789012345678901234567890',
24+
'eip155:2:0x1234567890123456789012225678901234567890',
25+
'eip155:3:0x1234567890123456789012345678901234567890',
26+
],
27+
MatchingAddressesCaipAccountIdListStruct,
28+
),
29+
).toBe(false);
30+
});
31+
32+
it("doesn't validate an array of mismatching chain namespaces", () => {
33+
expect(
34+
is(
35+
[
36+
'eip155:1:0x1234567890123456789012345678901234567890',
37+
'eip155:2:0x1234567890123456789012345678901234567890',
38+
'foo:3:0x1234567890123456789012345678901234567890',
39+
],
40+
MatchingAddressesCaipAccountIdListStruct,
41+
),
42+
).toBe(false);
43+
});
44+
});

packages/snaps-sdk/src/types/caip.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { CaipAccountId, CaipChainId } from '@metamask/utils';
1+
import type { Infer } from '@metamask/superstruct';
2+
import { array, refine } from '@metamask/superstruct';
3+
import {
4+
CaipAccountIdStruct,
5+
parseCaipAccountId,
6+
type CaipAccountId,
7+
type CaipChainId,
8+
} from '@metamask/utils';
29

310
export type {
411
CaipAccountId,
@@ -22,3 +29,35 @@ export type ChainId = CaipChainId;
2229
* @deprecated Use {@link CaipAccountId} instead.
2330
*/
2431
export type AccountId = CaipAccountId;
32+
33+
/**
34+
* A stuct representing a list of CAIP-10 account IDs where the account addresses and namespaces are the same.
35+
*/
36+
export const MatchingAddressesCaipAccountIdListStruct = refine(
37+
array(CaipAccountIdStruct),
38+
'Matching Addresses Account ID List',
39+
(value) => {
40+
const parsedAccountIds = value.map((accountId) =>
41+
parseCaipAccountId(accountId),
42+
);
43+
44+
if (
45+
!parsedAccountIds.every(
46+
({ address, chain: { namespace } }) =>
47+
address === parsedAccountIds[0].address &&
48+
namespace === parsedAccountIds[0].chain.namespace,
49+
)
50+
) {
51+
return 'All account IDs must have the same address and chain namespace.';
52+
}
53+
54+
return true;
55+
},
56+
);
57+
58+
/**
59+
* A list of CAIP-10 account IDs where the account addresses are the same.
60+
*/
61+
export type MatchingAddressesCaipAccountIdList = Infer<
62+
typeof MatchingAddressesCaipAccountIdListStruct
63+
>;

packages/snaps-sdk/src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import './global';
44
import './images';
55
/* eslint-enable import-x/no-unassigned-import */
66

7-
export type * from './caip';
7+
export * from './caip';
88
export * from './handlers';
99
export * from './methods';
1010
export type * from './permissions';

0 commit comments

Comments
 (0)