diff --git a/packages/starknet-snap/package.json b/packages/starknet-snap/package.json index 1abb080f..dc96fec7 100644 --- a/packages/starknet-snap/package.json +++ b/packages/starknet-snap/package.json @@ -44,7 +44,8 @@ "ethereum-unit-converter": "^0.0.17", "ethers": "^5.5.1", "starknet": "6.11.0", - "starknet_v4.22.0": "npm:starknet@4.22.0" + "starknet_v4.22.0": "npm:starknet@4.22.0", + "superstruct": "^2.0.2" }, "devDependencies": { "@babel/preset-typescript": "^7.23.3", diff --git a/packages/starknet-snap/src/utils/rpc.test.ts b/packages/starknet-snap/src/utils/rpc.test.ts new file mode 100644 index 00000000..c2211057 --- /dev/null +++ b/packages/starknet-snap/src/utils/rpc.test.ts @@ -0,0 +1,51 @@ +import { InvalidParamsError, SnapError } from '@metamask/snaps-sdk'; +import { object } from 'superstruct'; +import type { Struct } from 'superstruct'; + +import { validateRequest, validateResponse } from './rpc'; +import { AddressStruct } from './superstruct'; + +const struct = object({ + signerAddress: AddressStruct, +}); + +const params = { + signerAddress: + '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', +}; + +describe('validateRequest', () => { + it('does not throw error if the request is valid', () => { + expect(() => + validateRequest(params, struct as unknown as Struct), + ).not.toThrow(); + }); + + it('throws `InvalidParamsError` if the request is invalid', () => { + const requestParams = { + signerAddress: 1234, + }; + + expect(() => + validateRequest(requestParams, struct as unknown as Struct), + ).toThrow(InvalidParamsError); + }); +}); + +describe('validateResponse', () => { + it('does not throw error if the response is valid', () => { + expect(() => + validateResponse(params, struct as unknown as Struct), + ).not.toThrow(); + }); + + it('throws `Invalid Response` error if the response is invalid', () => { + const response = { + signerAddress: 1234, + }; + + expect(() => + validateResponse(response, struct as unknown as Struct), + ).toThrow(new SnapError('Invalid Response')); + }); +}); diff --git a/packages/starknet-snap/src/utils/rpc.ts b/packages/starknet-snap/src/utils/rpc.ts new file mode 100644 index 00000000..4e384a3c --- /dev/null +++ b/packages/starknet-snap/src/utils/rpc.ts @@ -0,0 +1,35 @@ +import { InvalidParamsError, SnapError } from '@metamask/snaps-sdk'; +import type { Struct } from 'superstruct'; +import { assert } from 'superstruct'; + +/** + * Validates that the request parameters conform to the expected structure defined by the provided struct. + * + * @template Params - The expected structure of the request parameters. + * @param requestParams - The request parameters to validate. + * @param struct - The expected structure of the request parameters. + * @throws {InvalidParamsError} If the request parameters do not conform to the expected structure. + */ +export function validateRequest(requestParams: Params, struct: Struct) { + try { + assert(requestParams, struct); + } catch (error) { + throw new InvalidParamsError(error.message) as unknown as Error; + } +} + +/** + * Validates that the response conforms to the expected structure defined by the provided struct. + * + * @template Params - The expected structure of the response. + * @param response - The response to validate. + * @param struct - The expected structure of the response. + * @throws {SnapError} If the response does not conform to the expected structure. + */ +export function validateResponse(response: Params, struct: Struct) { + try { + assert(response, struct); + } catch (error) { + throw new SnapError('Invalid Response') as unknown as Error; + } +} diff --git a/packages/starknet-snap/src/utils/superstruct.test.ts b/packages/starknet-snap/src/utils/superstruct.test.ts new file mode 100644 index 00000000..2e183aae --- /dev/null +++ b/packages/starknet-snap/src/utils/superstruct.test.ts @@ -0,0 +1,21 @@ +import { StructError, assert } from 'superstruct'; + +import { AddressStruct } from './superstruct'; + +describe('AddressStruct', () => { + it.each([ + '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', + '4882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', + ])('does not throw error if the address is valid - %s', (address) => { + expect(() => assert(address, AddressStruct)).not.toThrow(); + }); + + it.each([ + // non hex string - charactor is not within [0-9a-fA-F] + '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f2zzz', + // invalid length - 66/63 chars expected + '372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', + ])('throws error if the address is invalid - %s', (address) => { + expect(() => assert(address, AddressStruct)).toThrow(StructError); + }); +}); diff --git a/packages/starknet-snap/src/utils/superstruct.ts b/packages/starknet-snap/src/utils/superstruct.ts new file mode 100644 index 00000000..5e214e34 --- /dev/null +++ b/packages/starknet-snap/src/utils/superstruct.ts @@ -0,0 +1,22 @@ +import { validateAndParseAddress } from 'starknet'; +import { refine, string } from 'superstruct'; + +export const AddressStruct = refine( + string(), + 'AddressStruct', + (value: string) => { + try { + const trimmedAddress = value.toString().replace(/^0x0?/u, ''); + + // Check if the address is 63 characters long, the expected length of a StarkNet address exclude 0x0. + if (trimmedAddress.length !== 63) { + return 'Invalid address format'; + } + + validateAndParseAddress(trimmedAddress); + } catch (error) { + return 'Invalid address format'; + } + return true; + }, +); diff --git a/yarn.lock b/yarn.lock index 398c76f8..3f22854b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2274,6 +2274,7 @@ __metadata: sinon-chai: ^3.7.0 starknet: 6.11.0 starknet_v4.22.0: "npm:starknet@4.22.0" + superstruct: ^2.0.2 ts-jest: ^29.1.0 ts-node: ^10.9.2 typescript: ^4.7.4 @@ -25626,6 +25627,13 @@ __metadata: languageName: node linkType: hard +"superstruct@npm:^2.0.2": + version: 2.0.2 + resolution: "superstruct@npm:2.0.2" + checksum: a5f75b72cb8b14b86f4f7f750dae8c5ab0e4e1d92414b55e7625bae07bbcafad81c92486e7e32ccacd6ae1f553caff2b92a50ff42ad5093fd35b9cb7f4e5ec86 + languageName: node + linkType: hard + "supports-color@npm:8.1.1, supports-color@npm:^8.0.0": version: 8.1.1 resolution: "supports-color@npm:8.1.1"