From 7ed9aefb53fe1245c41276c1201b89adc6e56bbc Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Tue, 18 Feb 2025 17:50:49 -0500 Subject: [PATCH 01/16] add address input component --- .../jsx/components/form/AddressInput.test.tsx | 16 +++++++++ .../src/jsx/components/form/AddressInput.ts | 28 +++++++++++++++ .../src/jsx/components/form/Field.test.tsx | 25 +++++++++++++ .../src/jsx/components/form/Field.ts | 4 ++- .../src/jsx/components/form/index.ts | 3 ++ .../snaps-sdk/src/jsx/validation.test.tsx | 35 +++++++++++++++++++ packages/snaps-sdk/src/jsx/validation.ts | 20 +++++++++-- 7 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 packages/snaps-sdk/src/jsx/components/form/AddressInput.test.tsx create mode 100644 packages/snaps-sdk/src/jsx/components/form/AddressInput.ts diff --git a/packages/snaps-sdk/src/jsx/components/form/AddressInput.test.tsx b/packages/snaps-sdk/src/jsx/components/form/AddressInput.test.tsx new file mode 100644 index 0000000000..2fcc03b39f --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/form/AddressInput.test.tsx @@ -0,0 +1,16 @@ +import { AddressInput } from './AddressInput'; + +describe('AddressInput', () => { + it('renders an address input', () => { + const result = ; + + expect(result).toStrictEqual({ + type: 'AddressInput', + props: { + name: 'address', + chainId: 'eip155:1', + }, + key: null, + }); + }); +}); diff --git a/packages/snaps-sdk/src/jsx/components/form/AddressInput.ts b/packages/snaps-sdk/src/jsx/components/form/AddressInput.ts new file mode 100644 index 0000000000..8318ab5644 --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/form/AddressInput.ts @@ -0,0 +1,28 @@ +import type { CaipChainId } from '@metamask/utils'; + +import { createSnapComponent } from '../../component'; + +export type AddressInputProps = { + name: string; + value?: string | undefined; + chainId: CaipChainId; +}; + +const TYPE = 'AddressInput'; + +/** + * An input component for entering an address. Resolves the address to a display name and avatar. + * + * @param props - The props of the component. + * @param props.name - The name of the input field. + * @param props.value - The value of the input field. + * @param props.chainId - The CAIP-2 chain ID of the address. + * @returns An input element. + * @example + * + */ +export const AddressInput = createSnapComponent( + TYPE, +); + +export type AddressInputElement = ReturnType; diff --git a/packages/snaps-sdk/src/jsx/components/form/Field.test.tsx b/packages/snaps-sdk/src/jsx/components/form/Field.test.tsx index 9098b07035..82939aa583 100644 --- a/packages/snaps-sdk/src/jsx/components/form/Field.test.tsx +++ b/packages/snaps-sdk/src/jsx/components/form/Field.test.tsx @@ -1,5 +1,6 @@ import { Box } from '../Box'; import { Text } from '../Text'; +import { AddressInput } from './AddressInput'; import { Button } from './Button'; import { Dropdown } from './Dropdown'; import { Field } from './Field'; @@ -323,6 +324,30 @@ describe('Field', () => { }); }); + it('renders a field element with an address input', () => { + const result = ( + + + + ); + + expect(result).toStrictEqual({ + type: 'Field', + key: null, + props: { + label: 'Label', + children: { + type: 'AddressInput', + key: null, + props: { + name: 'address', + chainId: 'eip155:1', + }, + }, + }, + }); + }); + it('renders a field with a conditional', () => { const result = ( diff --git a/packages/snaps-sdk/src/jsx/components/form/Field.ts b/packages/snaps-sdk/src/jsx/components/form/Field.ts index 0493311842..0823850fdc 100644 --- a/packages/snaps-sdk/src/jsx/components/form/Field.ts +++ b/packages/snaps-sdk/src/jsx/components/form/Field.ts @@ -1,5 +1,6 @@ import type { GenericSnapChildren } from '../../component'; import { createSnapComponent } from '../../component'; +import type { AddressInputElement } from './AddressInput'; import type { CheckboxElement } from './Checkbox'; import type { DropdownElement } from './Dropdown'; import type { FileInputElement } from './FileInput'; @@ -26,7 +27,8 @@ export type FieldProps = { | FileInputElement | InputElement | CheckboxElement - | SelectorElement; + | SelectorElement + | AddressInputElement; }; const TYPE = 'Field'; diff --git a/packages/snaps-sdk/src/jsx/components/form/index.ts b/packages/snaps-sdk/src/jsx/components/form/index.ts index 3dc4221376..022ae28b98 100644 --- a/packages/snaps-sdk/src/jsx/components/form/index.ts +++ b/packages/snaps-sdk/src/jsx/components/form/index.ts @@ -1,3 +1,4 @@ +import type { AddressInputElement } from './AddressInput'; import type { ButtonElement } from './Button'; import type { CheckboxElement } from './Checkbox'; import type { DropdownElement } from './Dropdown'; @@ -23,8 +24,10 @@ export * from './Form'; export * from './Input'; export * from './Selector'; export * from './SelectorOption'; +export * from './AddressInput'; export type StandardFormElement = + | AddressInputElement | ButtonElement | CheckboxElement | FormElement diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx index d4d3089e75..e815d9fecd 100644 --- a/packages/snaps-sdk/src/jsx/validation.test.tsx +++ b/packages/snaps-sdk/src/jsx/validation.test.tsx @@ -35,6 +35,7 @@ import { Avatar, Banner, Skeleton, + AddressInput, } from './components'; import { AddressStruct, @@ -74,6 +75,7 @@ import { AvatarStruct, BannerStruct, SkeletonStruct, + AddressInputStruct, } from './validation'; describe('KeyStruct', () => { @@ -207,6 +209,36 @@ describe('ButtonStruct', () => { }); }); +describe('AddressInputStruct', () => { + it.each([ + , + , + ])('validates an address input element', (value) => { + expect(is(value, AddressInputStruct)).toBe(true); + }); + + it.each([ + 'foo', + 42, + null, + undefined, + {}, + [], + // @ts-expect-error - Invalid props. + , + // @ts-expect-error - Invalid props. + , + // @ts-expect-error - Invalid props. + , + ])('does not validate "%p"', (value) => { + expect(is(value, AddressInputStruct)).toBe(false); + }); +}); + describe('InputStruct', () => { it.each([ , @@ -312,6 +344,9 @@ describe('FieldStruct', () => { , + + + , ])('validates a field element', (value) => { expect(is(value, FieldStruct)).toBe(true); }); diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index 994a622c25..f74022f2a8 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -23,6 +23,7 @@ import { } from '@metamask/superstruct'; import { CaipAccountIdStruct, + CaipChainIdStruct, hasProperty, HexChecksumAddressStruct, isPlainObject, @@ -37,7 +38,7 @@ import { svg, typedUnion, } from '../internals'; -import type { EmptyObject } from '../types'; +import type { CaipChainId, EmptyObject } from '../types'; import type { GenericSnapChildren, GenericSnapElement, @@ -51,6 +52,7 @@ import type { import type { AvatarElement, SkeletonElement } from './components'; import { type AddressElement, + type AddressInputElement, type BoldElement, type BoxElement, type ButtonElement, @@ -338,6 +340,15 @@ export const InputStruct: Describe = elementWithSelectiveProps( }, ); +export const AddressInputStruct: Describe = element( + 'AddressInput', + { + name: string(), + chainId: CaipChainIdStruct as unknown as Struct, + value: optional(string()), + }, +); + /** * A struct for the {@link OptionElement} type. */ @@ -474,6 +485,7 @@ const BOX_INPUT_BOTH = [ * A subset of JSX elements that are allowed as single children of the Field component. */ const FIELD_CHILDREN_ARRAY = [ + AddressInputStruct, InputStruct, DropdownStruct, RadioGroupStruct, @@ -481,6 +493,7 @@ const FIELD_CHILDREN_ARRAY = [ CheckboxStruct, SelectorStruct, ] as [ + typeof AddressInputStruct, typeof InputStruct, typeof DropdownStruct, typeof RadioGroupStruct, @@ -525,7 +538,8 @@ const FieldChildStruct = selectiveUnion((value) => { | FileInputElement | InputElement | CheckboxElement - | SelectorElement, + | SelectorElement + | AddressInputElement, null >; @@ -873,6 +887,7 @@ export const SpinnerStruct: Describe = element('Spinner'); */ export const BoxChildStruct = typedUnion([ AddressStruct, + AddressInputStruct, BoldStruct, BoxStruct, ButtonStruct, @@ -936,6 +951,7 @@ export const RootJSXElementStruct = typedUnion([ * A struct for the {@link JSXElement} type. */ export const JSXElementStruct: Describe = typedUnion([ + AddressInputStruct, ButtonStruct, InputStruct, FileInputStruct, From fed768a6dc9ab17fe20c27d9cf142f550e1f5505 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Tue, 18 Feb 2025 17:59:02 -0500 Subject: [PATCH 02/16] rebuild --- packages/examples/packages/browserify-plugin/snap.manifest.json | 2 +- packages/examples/packages/browserify/snap.manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index ff95620d0c..f276281464 100644 --- a/packages/examples/packages/browserify-plugin/snap.manifest.json +++ b/packages/examples/packages/browserify-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "6q7hDfHkTpecnN0cdclgC5ovyAjcEVy02E0NqWc1mnw=", + "shasum": "KG7wMWVcqMSE09h+FeCVC3sNrAOXx+yhH/kT2DBhSgM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify/snap.manifest.json b/packages/examples/packages/browserify/snap.manifest.json index 1a6fadaaf6..d87d80cc72 100644 --- a/packages/examples/packages/browserify/snap.manifest.json +++ b/packages/examples/packages/browserify/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "01h/ih5+fbuodnJpydhY26YnNWPqJclCFS/MAAQicd8=", + "shasum": "IGJr5GnnMkfNDBfqpvhPz6foYnmzr0IN1tCGU432KnQ=", "location": { "npm": { "filePath": "dist/bundle.js", From 4c014c3899cfe7497f04afaf15e33c56e396a807 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Tue, 18 Feb 2025 18:16:25 -0500 Subject: [PATCH 03/16] fix tests --- .../snaps-rpc-methods/src/permitted/createInterface.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx b/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx index 1403fa95a3..05e18d9420 100644 --- a/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx +++ b/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx @@ -141,7 +141,7 @@ describe('snap_createInterface', () => { error: { code: -32602, message: - 'Invalid params: At path: ui -- Expected type to be one of: "Address", "Bold", "Box", "Button", "Copyable", "Divider", "Dropdown", "RadioGroup", "Field", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Avatar", "Banner", "Skeleton", "Container", but received: undefined.', + 'Invalid params: At path: ui -- Expected type to be one of: "Address", "AddressInput", "Bold", "Box", "Button", "Copyable", "Divider", "Dropdown", "RadioGroup", "Field", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Avatar", "Banner", "Skeleton", "Container", but received: undefined.', stack: expect.any(String), }, id: 1, @@ -191,7 +191,7 @@ describe('snap_createInterface', () => { error: { code: -32602, message: - 'Invalid params: At path: ui.props.children.props.children -- Expected type to be one of: "Input", "Dropdown", "RadioGroup", "FileInput", "Checkbox", "Selector", but received: "Copyable".', + 'Invalid params: At path: ui.props.children.props.children -- Expected type to be one of: "AddressInput", "Input", "Dropdown", "RadioGroup", "FileInput", "Checkbox", "Selector", but received: "Copyable".', stack: expect.any(String), }, id: 1, From 2e33cd0b0713e7286fc74f24d8ef2ed6813f9eb1 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Wed, 19 Feb 2025 09:01:06 -0500 Subject: [PATCH 04/16] add address input to state creation utils --- .../src/interface/utils.test.tsx | 44 +++++++++++++++++++ .../snaps-controllers/src/interface/utils.ts | 16 ++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/snaps-controllers/src/interface/utils.test.tsx b/packages/snaps-controllers/src/interface/utils.test.tsx index c00f9de819..d2e3d5791b 100644 --- a/packages/snaps-controllers/src/interface/utils.test.tsx +++ b/packages/snaps-controllers/src/interface/utils.test.tsx @@ -15,6 +15,7 @@ import { Selector, Card, SelectorOption, + AddressInput, } from '@metamask/snaps-sdk/jsx'; import { assertNameIsUnique, constructState, getJsxInterface } from './utils'; @@ -283,6 +284,49 @@ describe('constructState', () => { }); }); + it('handles root level AddressInput with value', () => { + const element = ( + + + + ); + + const result = constructState({}, element); + expect(result).toStrictEqual({ + foo: '0x123', + }); + }); + + it('handles root level AddressInput without value', () => { + const element = ( + + + + ); + + const result = constructState({}, element); + expect(result).toStrictEqual({ + foo: null, + }); + }); + + it('handles AddressInput in forms', () => { + const element = ( + +
+ + + +
+
+ ); + + const result = constructState({}, element); + expect(result).toStrictEqual({ + form: { foo: null }, + }); + }); + it('sets default value for root level dropdown', () => { const element = ( diff --git a/packages/snaps-controllers/src/interface/utils.ts b/packages/snaps-controllers/src/interface/utils.ts index 24923138a4..24c3ebadbc 100644 --- a/packages/snaps-controllers/src/interface/utils.ts +++ b/packages/snaps-controllers/src/interface/utils.ts @@ -17,6 +17,7 @@ import type { RadioElement, SelectorElement, SelectorOptionElement, + AddressInputElement, } from '@metamask/snaps-sdk/jsx'; import { isJSXElementUnsafe } from '@metamask/snaps-sdk/jsx'; import { @@ -70,7 +71,8 @@ function constructComponentSpecificDefaultState( | DropdownElement | RadioGroupElement | CheckboxElement - | SelectorElement, + | SelectorElement + | AddressInputElement, ) { switch (element.type) { case 'Dropdown': { @@ -111,7 +113,8 @@ function getComponentStateValue( | DropdownElement | RadioGroupElement | CheckboxElement - | SelectorElement, + | SelectorElement + | AddressInputElement, ) { switch (element.type) { case 'Checkbox': @@ -138,7 +141,8 @@ function constructInputState( | RadioGroupElement | FileInputElement | CheckboxElement - | SelectorElement, + | SelectorElement + | AddressInputElement, form?: string, ) { const oldStateUnwrapped = form ? (oldState[form] as FormState) : oldState; @@ -196,7 +200,8 @@ export function constructState( component.type === 'RadioGroup' || component.type === 'FileInput' || component.type === 'Checkbox' || - component.type === 'Selector') + component.type === 'Selector' || + component.type === 'AddressInput') ) { const formState = newState[currentForm.name] as FormState; assertNameIsUnique(formState, component.props.name); @@ -215,7 +220,8 @@ export function constructState( component.type === 'RadioGroup' || component.type === 'FileInput' || component.type === 'Checkbox' || - component.type === 'Selector' + component.type === 'Selector' || + component.type === 'AddressInput' ) { assertNameIsUnique(newState, component.props.name); newState[component.props.name] = constructInputState(oldState, component); From d9e8cc878feb7b3f1cb8c611def12dd90b0638d3 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Wed, 19 Feb 2025 09:47:50 -0500 Subject: [PATCH 05/16] rebuild --- packages/examples/packages/browserify-plugin/snap.manifest.json | 2 +- packages/examples/packages/browserify/snap.manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index f276281464..72c8e4842d 100644 --- a/packages/examples/packages/browserify-plugin/snap.manifest.json +++ b/packages/examples/packages/browserify-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "KG7wMWVcqMSE09h+FeCVC3sNrAOXx+yhH/kT2DBhSgM=", + "shasum": "BoeFiKxZOqrrtzzygrGv4MtzvjAgLMJTdE5V1SHjN0Q=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify/snap.manifest.json b/packages/examples/packages/browserify/snap.manifest.json index d87d80cc72..a149a1565e 100644 --- a/packages/examples/packages/browserify/snap.manifest.json +++ b/packages/examples/packages/browserify/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "IGJr5GnnMkfNDBfqpvhPz6foYnmzr0IN1tCGU432KnQ=", + "shasum": "sem1lPfjK/7gff2NoaFo3d+ne9JLTaUnnGbcojaFo9c=", "location": { "npm": { "filePath": "dist/bundle.js", From 4a52469f5768401931583f7b7cceaa6df6409ba8 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 20 Feb 2025 12:25:02 -0500 Subject: [PATCH 06/16] update addressinput state --- packages/snaps-controllers/src/interface/utils.test.tsx | 2 +- packages/snaps-controllers/src/interface/utils.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/snaps-controllers/src/interface/utils.test.tsx b/packages/snaps-controllers/src/interface/utils.test.tsx index d2e3d5791b..d0ac8bf87b 100644 --- a/packages/snaps-controllers/src/interface/utils.test.tsx +++ b/packages/snaps-controllers/src/interface/utils.test.tsx @@ -293,7 +293,7 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - foo: '0x123', + foo: 'eip155:1:0x123', }); }); diff --git a/packages/snaps-controllers/src/interface/utils.ts b/packages/snaps-controllers/src/interface/utils.ts index 24c3ebadbc..39bed8d706 100644 --- a/packages/snaps-controllers/src/interface/utils.ts +++ b/packages/snaps-controllers/src/interface/utils.ts @@ -120,6 +120,14 @@ function getComponentStateValue( case 'Checkbox': return element.props.checked; + case 'AddressInput': + if (!element.props.value) { + return null; + } + + // Construct CAIP-10 Id + return `${element.props.chainId}:${element.props.value}`; + default: return element.props.value; } From 04ff7d0a303fbcc663a0bdeee5fffed73f7193d0 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 20 Feb 2025 13:00:51 -0500 Subject: [PATCH 07/16] lint fix --- packages/snaps-sdk/src/jsx/components/form/Field.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snaps-sdk/src/jsx/components/form/Field.ts b/packages/snaps-sdk/src/jsx/components/form/Field.ts index 0823850fdc..875e46469c 100644 --- a/packages/snaps-sdk/src/jsx/components/form/Field.ts +++ b/packages/snaps-sdk/src/jsx/components/form/Field.ts @@ -1,5 +1,3 @@ -import type { GenericSnapChildren } from '../../component'; -import { createSnapComponent } from '../../component'; import type { AddressInputElement } from './AddressInput'; import type { CheckboxElement } from './Checkbox'; import type { DropdownElement } from './Dropdown'; @@ -7,6 +5,8 @@ import type { FileInputElement } from './FileInput'; import type { InputElement } from './Input'; import type { RadioGroupElement } from './RadioGroup'; import type { SelectorElement } from './Selector'; +import type { GenericSnapChildren } from '../../component'; +import { createSnapComponent } from '../../component'; /** * The props of the {@link Field} component. From e5c19d1e66b8e3bccf1f7b8c38f7e4f0c2febf73 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 20 Feb 2025 13:20:45 -0500 Subject: [PATCH 08/16] update coverage --- packages/snaps-controllers/coverage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index c73f8baa74..de6f9685e7 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -1,5 +1,5 @@ { - "branches": 93.31, + "branches": 93.35, "functions": 97.05, "lines": 98.25, "statements": 97.98 From 63b992e55b412c9f1bf98b784b2fff3fc3b143cd Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 24 Feb 2025 12:42:56 -0500 Subject: [PATCH 09/16] update types, use util to construct caip account id --- packages/snaps-controllers/src/interface/utils.ts | 8 +++++--- packages/snaps-sdk/src/types/interface.ts | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/snaps-controllers/src/interface/utils.ts b/packages/snaps-controllers/src/interface/utils.ts index 39bed8d706..9ac5f195bc 100644 --- a/packages/snaps-controllers/src/interface/utils.ts +++ b/packages/snaps-controllers/src/interface/utils.ts @@ -26,6 +26,7 @@ import { getJsxElementFromComponent, walkJsx, } from '@metamask/snaps-utils'; +import { toCaipAccountId } from '@metamask/utils'; /** * Get a JSX element from a component or JSX element. If the component is a @@ -120,14 +121,15 @@ function getComponentStateValue( case 'Checkbox': return element.props.checked; - case 'AddressInput': + case 'AddressInput': { if (!element.props.value) { return null; } // Construct CAIP-10 Id - return `${element.props.chainId}:${element.props.value}`; - + const [namespace, reference] = element.props.chainId.split(':'); + return toCaipAccountId(namespace, reference, element.props.value); + } default: return element.props.value; } diff --git a/packages/snaps-sdk/src/types/interface.ts b/packages/snaps-sdk/src/types/interface.ts index c11a36601a..d421968227 100644 --- a/packages/snaps-sdk/src/types/interface.ts +++ b/packages/snaps-sdk/src/types/interface.ts @@ -6,7 +6,12 @@ import { string, union, } from '@metamask/superstruct'; -import { JsonStruct, hasProperty, isObject } from '@metamask/utils'; +import { + CaipAccountIdStruct, + JsonStruct, + hasProperty, + isObject, +} from '@metamask/utils'; import { FileStruct } from './handlers'; import { selectiveUnion } from '../internals'; @@ -22,7 +27,12 @@ import { ComponentStruct } from '../ui'; * either the value of an input or a sub-state of a form. */ -export const StateStruct = union([FileStruct, string(), boolean()]); +export const StateStruct = union([ + FileStruct, + string(), + boolean(), + CaipAccountIdStruct, +]); export const FormStateStruct = record(string(), nullable(StateStruct)); From 78291c8e3ea425f3ee992245d86a3b390bcc8af5 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 24 Feb 2025 13:03:53 -0500 Subject: [PATCH 10/16] rebuild --- packages/examples/packages/browserify-plugin/snap.manifest.json | 2 +- packages/examples/packages/browserify/snap.manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index 72c8e4842d..690f1fe7b1 100644 --- a/packages/examples/packages/browserify-plugin/snap.manifest.json +++ b/packages/examples/packages/browserify-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "BoeFiKxZOqrrtzzygrGv4MtzvjAgLMJTdE5V1SHjN0Q=", + "shasum": "TcwYfkj5gklI0N6ajcCTaOeFZAxOq/VSIlsSFQT9Iso=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify/snap.manifest.json b/packages/examples/packages/browserify/snap.manifest.json index a149a1565e..01cc8ddaad 100644 --- a/packages/examples/packages/browserify/snap.manifest.json +++ b/packages/examples/packages/browserify/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "sem1lPfjK/7gff2NoaFo3d+ne9JLTaUnnGbcojaFo9c=", + "shasum": "Tw0tVSsCoHqM0JY7mbYOA9Ehi5j++BNJiDGujUrYFJk=", "location": { "npm": { "filePath": "dist/bundle.js", From d2c9374a92d43009062eab58176563facf14c050 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 24 Feb 2025 14:22:44 -0500 Subject: [PATCH 11/16] Add placeholder and disabled prop --- packages/snaps-sdk/src/jsx/components/form/AddressInput.ts | 4 ++++ packages/snaps-sdk/src/jsx/validation.test.tsx | 6 ++++++ packages/snaps-sdk/src/jsx/validation.ts | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/packages/snaps-sdk/src/jsx/components/form/AddressInput.ts b/packages/snaps-sdk/src/jsx/components/form/AddressInput.ts index 8318ab5644..af86807b83 100644 --- a/packages/snaps-sdk/src/jsx/components/form/AddressInput.ts +++ b/packages/snaps-sdk/src/jsx/components/form/AddressInput.ts @@ -6,6 +6,8 @@ export type AddressInputProps = { name: string; value?: string | undefined; chainId: CaipChainId; + placeholder?: string | undefined; + disabled?: boolean | undefined; }; const TYPE = 'AddressInput'; @@ -17,6 +19,8 @@ const TYPE = 'AddressInput'; * @param props.name - The name of the input field. * @param props.value - The value of the input field. * @param props.chainId - The CAIP-2 chain ID of the address. + * @param props.placeholder - The placeholder text of the input field. + * @param props.disabled - Whether the input field is disabled. * @returns An input element. * @example * diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx index fb55fae558..d06da54984 100644 --- a/packages/snaps-sdk/src/jsx/validation.test.tsx +++ b/packages/snaps-sdk/src/jsx/validation.test.tsx @@ -217,6 +217,8 @@ describe('AddressInputStruct', () => { chainId="eip155:1" value="0x1234567890abcdef1234567890abcdef12345678" />, + , + , ])('validates an address input element', (value) => { expect(is(value, AddressInputStruct)).toBe(true); }); @@ -234,6 +236,10 @@ describe('AddressInputStruct', () => { , // @ts-expect-error - Invalid props. , + // @ts-expect-error - Invalid props. + , + // @ts-expect-error - Invalid props. + , ])('does not validate "%p"', (value) => { expect(is(value, AddressInputStruct)).toBe(false); }); diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index d90e575a9f..38c41ebd83 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -341,12 +341,17 @@ export const InputStruct: Describe = elementWithSelectiveProps( }, ); +/** + * A struct for the {@link AddressInputElement} type. + */ export const AddressInputStruct: Describe = element( 'AddressInput', { name: string(), chainId: CaipChainIdStruct as unknown as Struct, value: optional(string()), + placeholder: optional(string()), + disabled: optional(boolean()), }, ); From 2f342d3a98793713cf631905b02a43c11c039072 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 24 Feb 2025 14:24:35 -0500 Subject: [PATCH 12/16] rebuild --- packages/examples/packages/browserify-plugin/snap.manifest.json | 2 +- packages/examples/packages/browserify/snap.manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index 690f1fe7b1..6dd1c2f30b 100644 --- a/packages/examples/packages/browserify-plugin/snap.manifest.json +++ b/packages/examples/packages/browserify-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "TcwYfkj5gklI0N6ajcCTaOeFZAxOq/VSIlsSFQT9Iso=", + "shasum": "o388f8Ok93eiNS56maRtdc2RLI4ieYlONqZY+ZpGSEY=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify/snap.manifest.json b/packages/examples/packages/browserify/snap.manifest.json index 01cc8ddaad..5d034eb660 100644 --- a/packages/examples/packages/browserify/snap.manifest.json +++ b/packages/examples/packages/browserify/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "Tw0tVSsCoHqM0JY7mbYOA9Ehi5j++BNJiDGujUrYFJk=", + "shasum": "3GrrVE0mbpG4zyCONoukNum5N1ovmMJi7xafPJ+m9Hk=", "location": { "npm": { "filePath": "dist/bundle.js", From 92e5d837e74629cd02686303fd22cd1d576234c7 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 24 Feb 2025 15:54:50 -0500 Subject: [PATCH 13/16] use parser from utils --- packages/snaps-controllers/src/interface/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snaps-controllers/src/interface/utils.ts b/packages/snaps-controllers/src/interface/utils.ts index 9ac5f195bc..39d6c12688 100644 --- a/packages/snaps-controllers/src/interface/utils.ts +++ b/packages/snaps-controllers/src/interface/utils.ts @@ -26,7 +26,7 @@ import { getJsxElementFromComponent, walkJsx, } from '@metamask/snaps-utils'; -import { toCaipAccountId } from '@metamask/utils'; +import { toCaipAccountId, parseCaipChainId } from '@metamask/utils'; /** * Get a JSX element from a component or JSX element. If the component is a @@ -127,7 +127,7 @@ function getComponentStateValue( } // Construct CAIP-10 Id - const [namespace, reference] = element.props.chainId.split(':'); + const { namespace, reference } = parseCaipChainId(element.props.chainId); return toCaipAccountId(namespace, reference, element.props.value); } default: From 79a5bf662f004f5803f63a85eb02edb01bf16366 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Wed, 26 Feb 2025 13:22:46 -0500 Subject: [PATCH 14/16] update send flow snap --- .../packages/send-flow/snap.manifest.json | 2 +- .../send-flow/src/components/SendFlow.tsx | 5 ----- .../send-flow/src/components/SendForm.tsx | 19 +++---------------- .../send-flow/src/images/jazzicon3.svg | 11 ----------- .../examples/packages/send-flow/src/index.tsx | 2 -- .../examples/packages/send-flow/src/utils.tsx | 1 - 6 files changed, 4 insertions(+), 36 deletions(-) delete mode 100644 packages/examples/packages/send-flow/src/images/jazzicon3.svg diff --git a/packages/examples/packages/send-flow/snap.manifest.json b/packages/examples/packages/send-flow/snap.manifest.json index 72d1fd67e1..e20c50c0f1 100644 --- a/packages/examples/packages/send-flow/snap.manifest.json +++ b/packages/examples/packages/send-flow/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "FFP7+q2qlyOMqmtm01zXewHHczyLyTsgbFYpkoSkw7s=", + "shasum": "9YAT2soLTWY6yInzFfNkwCY5aD6+OTv9flf5fflyJyw=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/send-flow/src/components/SendFlow.tsx b/packages/examples/packages/send-flow/src/components/SendFlow.tsx index 61c9254b3e..c571d53d84 100644 --- a/packages/examples/packages/send-flow/src/components/SendFlow.tsx +++ b/packages/examples/packages/send-flow/src/components/SendFlow.tsx @@ -15,7 +15,6 @@ import type { Account, Currency } from '../types'; * @property selectedCurrency - The selected currency to display. * @property total - The total cost of the transaction. * @property fees - The fees for the transaction. - * @property displayClearIcon - Whether to display the clear icon or not. * @property flushToAddress - Whether to flush the address field or not. * @property errors - The form errors. */ @@ -25,7 +24,6 @@ export type SendFlowProps = { selectedCurrency: 'BTC' | '$'; total: Currency; fees: Currency; - displayClearIcon: boolean; flushToAddress?: boolean; errors?: { amount?: string; @@ -43,7 +41,6 @@ export type SendFlowProps = { * @param props.total - The total cost of the transaction. * @param props.errors - The form errors. * @param props.fees - The fees for the transaction. - * @param props.displayClearIcon - Whether to display the clear icon or not. * @param props.flushToAddress - Whether to flush the address field or not. * @returns The SendFlow component. */ @@ -53,7 +50,6 @@ export const SendFlow: SnapComponent = ({ selectedCurrency, total, fees, - displayClearIcon, flushToAddress, errors, }) => { @@ -66,7 +62,6 @@ export const SendFlow: SnapComponent = ({ accounts={accounts} selectedCurrency={selectedCurrency} flushToAddress={flushToAddress} - displayClearIcon={displayClearIcon} errors={errors} /> diff --git a/packages/examples/packages/send-flow/src/components/SendForm.tsx b/packages/examples/packages/send-flow/src/components/SendForm.tsx index 8a7dcf03e0..15bf4403bd 100644 --- a/packages/examples/packages/send-flow/src/components/SendForm.tsx +++ b/packages/examples/packages/send-flow/src/components/SendForm.tsx @@ -6,13 +6,13 @@ import { Icon, Image, Input, + AddressInput, Text, type SnapComponent, } from '@metamask/snaps-sdk/jsx'; import { AccountSelector } from './AccountSelector'; import btcIcon from '../images/btc.svg'; -import jazzicon3 from '../images/jazzicon3.svg'; import type { Account, SendFormErrors } from '../types'; /** @@ -22,7 +22,6 @@ import type { Account, SendFormErrors } from '../types'; * @property accounts - The available accounts. * @property errors - The form errors. * @property selectedCurrency - The selected currency to display. - * @property displayClearIcon - Whether to display the clear icon or not. * @property flushToAddress - Whether to flush the address field or not. */ export type SendFormProps = { @@ -30,7 +29,6 @@ export type SendFormProps = { accounts: Account[]; errors?: SendFormErrors; selectedCurrency: 'BTC' | '$'; - displayClearIcon: boolean; flushToAddress?: boolean; }; @@ -42,7 +40,6 @@ export type SendFormProps = { * @param props.accounts - The available accounts. * @param props.errors - The form errors. * @param props.selectedCurrency - The selected currency to display. - * @param props.displayClearIcon - Whether to display the clear icon or not. * @param props.flushToAddress - Whether to flush the address field or not. * @returns The SendForm component. */ @@ -51,7 +48,6 @@ export const SendForm: SnapComponent = ({ accounts, errors, selectedCurrency, - displayClearIcon, flushToAddress, }) => (
@@ -69,21 +65,12 @@ export const SendForm: SnapComponent = ({ - - - - - {displayClearIcon && ( - - - - )} ); diff --git a/packages/examples/packages/send-flow/src/images/jazzicon3.svg b/packages/examples/packages/send-flow/src/images/jazzicon3.svg deleted file mode 100644 index c43767a6d7..0000000000 --- a/packages/examples/packages/send-flow/src/images/jazzicon3.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/packages/examples/packages/send-flow/src/index.tsx b/packages/examples/packages/send-flow/src/index.tsx index 1323a37c28..371d2ef683 100644 --- a/packages/examples/packages/send-flow/src/index.tsx +++ b/packages/examples/packages/send-flow/src/index.tsx @@ -111,7 +111,6 @@ export const onUserInput: OnUserInputHandler = async ({ selectedCurrency={selectedCurrency} total={total} fees={fees} - displayClearIcon={Boolean(sendForm.to) && sendForm.to !== ''} errors={formErrors} /> ), @@ -138,7 +137,6 @@ export const onUserInput: OnUserInputHandler = async ({ total={total} fees={fees} flushToAddress={true} - displayClearIcon={false} errors={formErrors} /> ), diff --git a/packages/examples/packages/send-flow/src/utils.tsx b/packages/examples/packages/send-flow/src/utils.tsx index 75021dd5a6..9219ed5c2e 100644 --- a/packages/examples/packages/send-flow/src/utils.tsx +++ b/packages/examples/packages/send-flow/src/utils.tsx @@ -37,7 +37,6 @@ export async function generateSendFlow({ selectedCurrency="BTC" total={{ amount: 0, fiat: 0 }} fees={fees} - displayClearIcon={false} /> ), context: { From 921eea3cbf81f06dd56e1140e0a70e9c03a71365 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 24 Mar 2025 11:27:23 -0400 Subject: [PATCH 15/16] remove dupe --- packages/snaps-sdk/src/jsx/validation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index 4bd7460f6b..994d7eda6d 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -41,7 +41,6 @@ import type { StringElement, } from './component'; import type { - AddressInputElement, AssetSelectorElement, AvatarElement, SkeletonElement, From 261ca7dd89586f727b9322d2b841ca067c422514 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 24 Mar 2025 11:29:17 -0400 Subject: [PATCH 16/16] rebuild --- packages/examples/packages/send-flow/snap.manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/packages/send-flow/snap.manifest.json b/packages/examples/packages/send-flow/snap.manifest.json index b8160937ee..ccb1f66ca6 100644 --- a/packages/examples/packages/send-flow/snap.manifest.json +++ b/packages/examples/packages/send-flow/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "TBt+UJnVWgfYB6xDuI94gT0Ns1WPw4LdJKFcxeUH8Rg=", + "shasum": "K4N1OVwbY1dfSZpc4QvAWprZVI0FQSR8KB5NZoOXPFY=", "location": { "npm": { "filePath": "dist/bundle.js",