diff --git a/packages/starknet-snap/src/utils/superstruct.test.ts b/packages/starknet-snap/src/utils/superstruct.test.ts index b6f1870d..81f90f04 100644 --- a/packages/starknet-snap/src/utils/superstruct.test.ts +++ b/packages/starknet-snap/src/utils/superstruct.test.ts @@ -1,5 +1,5 @@ import { constants } from 'starknet'; -import { StructError, assert } from 'superstruct'; +import { StructError, assert, object, number, string } from 'superstruct'; import transactionExample from '../__tests__/fixture/transactionExample.json'; import typedDataExample from '../__tests__/fixture/typedDataExample.json'; @@ -11,6 +11,7 @@ import { CairoVersionStruct, CallDataStruct, ChainIdStruct, + createStructWithAdditionalProperties, TxVersionStruct, TypeDataStruct, } from './superstruct'; @@ -174,3 +175,52 @@ describe('CallDataStruct', () => { ); }); }); + +describe('createStructWithAdditionalProperties', () => { + const predefinedProperties = object({ + name: string(), + age: number(), + }); + + const additionalPropertyTypes = string(); // Additional properties should be strings + const ExtendedPropStruct = createStructWithAdditionalProperties( + predefinedProperties, + additionalPropertyTypes, + ); + it('should validate predefined properties correctly', () => { + const validData = { + name: 'John', + age: 30, + }; + const [error, result] = ExtendedPropStruct.validate(validData); + + expect(error).toBeUndefined(); + expect(result).toStrictEqual(validData); + }); + + it('should validate additional properties correctly', () => { + const validDataWithExtra = { + name: 'John', + age: 30, + nickname: 'Johnny', + }; + + const [error, result] = ExtendedPropStruct.validate(validDataWithExtra); + + expect(error).toBeUndefined(); + expect(result).toStrictEqual(validDataWithExtra); + }); + + it('should fail validation if additional properties are of the wrong type', () => { + const invalidData = { + name: 'John', + age: 30, + nickname: 12345, // Invalid type for additional property + }; + + const [error] = ExtendedPropStruct.validate(invalidData); + + expect(error).toBeDefined(); + expect(error?.message).toContain('Expected a string, but received'); + }); +}); diff --git a/packages/starknet-snap/src/utils/superstruct.ts b/packages/starknet-snap/src/utils/superstruct.ts index ce1f9dcb..b4659c01 100644 --- a/packages/starknet-snap/src/utils/superstruct.ts +++ b/packages/starknet-snap/src/utils/superstruct.ts @@ -1,5 +1,6 @@ import { union } from '@metamask/snaps-sdk'; import { constants, validateAndParseAddress } from 'starknet'; +import type { Struct } from 'superstruct'; import { boolean, enums, @@ -11,6 +12,8 @@ import { any, number, array, + dynamic, + assign, } from 'superstruct'; import { CAIRO_VERSION_LEGACY, CAIRO_VERSION } from './constants'; @@ -91,3 +94,38 @@ export const CairoVersionStruct = enums([CAIRO_VERSION, CAIRO_VERSION_LEGACY]); export const TxVersionStruct = enums( Object.values(constants.TRANSACTION_VERSION), ); + +/** + * Creates a struct that combines predefined properties with additional dynamic properties. + * + * This function generates a Superstruct schema that includes both the predefined properties + * and any additional properties found in the input. The additional properties are validated + * according to the specified `additionalPropertyTypes`, or `any` if not provided. + * + * @param predefinedProperties - A Superstruct schema defining the base set of properties that are expected. + * @param additionalPropertyTypes - A Superstruct schema that defines the types for any additional properties. + * Defaults to `any`, allowing any additional properties. + * @returns A dynamic struct that first validates against the predefined properties and then + * includes any additional properties that match the `additionalPropertyTypes` schema. + */ +export const createStructWithAdditionalProperties = ( + predefinedProperties: Struct, + additionalPropertyTypes: Struct = any(), +) => { + return dynamic((value) => { + if (typeof value !== 'object' || value === null) { + return predefinedProperties; + } + + const additionalProperties = Object.keys(value).reduce< + Record + >((schema, key) => { + if (!(key in predefinedProperties.schema)) { + schema[key] = additionalPropertyTypes; + } + return schema; + }, {}); + + return assign(predefinedProperties, object(additionalProperties)); + }); +};