From 3e337ea511b3ca466d450249fadb581a53486644 Mon Sep 17 00:00:00 2001 From: Hugo Masclet Date: Thu, 23 Nov 2023 14:50:31 +0100 Subject: [PATCH] feat: add support for multi types in mappings --- src/index.ts | 7 +- src/lib/encodeKeyName.ts | 1 + src/lib/utils.test.ts | 179 +++++++++++++++++++++++++++++++++++++++ src/lib/utils.ts | 164 +++++++++++++++++++++++++++++++++++ 4 files changed, 350 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 4d37d9be..3eb9fc9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import { encodeData, convertIPFSGatewayUrl, generateSchemasFromDynamicKeys, + duplicateMultiTypeERC725SchemaEntry, } from './lib/utils'; import { getSchema } from './lib/schemaParser'; @@ -110,7 +111,11 @@ export class ERC725 { }; this.options = { - schemas: this.validateSchemas(schemas), + schemas: this.validateSchemas( + schemas + .map((schema) => duplicateMultiTypeERC725SchemaEntry(schema)) + .flat(), + ), address, provider: ERC725.initializeProvider( provider, diff --git a/src/lib/encodeKeyName.ts b/src/lib/encodeKeyName.ts index d07e4b32..733146c6 100644 --- a/src/lib/encodeKeyName.ts +++ b/src/lib/encodeKeyName.ts @@ -136,6 +136,7 @@ export const encodeDynamicKeyPart = ( } }; +// This function does not support multi dynamic types such as MyName: export function isDynamicKeyName(name: string) { const keyNameParts = name.split(':'); diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 0ae455ad..a609d10e 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -37,6 +37,8 @@ import { convertIPFSGatewayUrl, generateSchemasFromDynamicKeys, encodeTupleKeyValue, + duplicateMultiTypeERC725SchemaEntry, + splitMultiDynamicKeyNamePart, } from './utils'; import { isDynamicKeyName } from './encodeKeyName'; import { decodeKey } from './decodeData'; @@ -804,4 +806,181 @@ describe('utils', () => { }); }); }); + + describe('splitMultiDynamicKeyNamePart', () => { + it('returns the exact input string if it is not a dynamic string', () => { + const keyName = 'ImNotDynamic'; + + assert.deepStrictEqual(splitMultiDynamicKeyNamePart(keyName), [keyName]); + }); + it('returns an array with each type when the input is a dynamic string', () => { + const keyName = ''; + + assert.deepStrictEqual(splitMultiDynamicKeyNamePart(keyName), [ + 'address', + 'bytes32', + 'uint256', + ]); + }); + }); + + describe('duplicateMultiTypeERC725SchemaEntry', () => { + it('returns the exact input in an array if there is no dynamic type in it', () => { + const schema: ERC725JSONSchema = { + name: 'AddressPermissions[]', // This type is not dynamic + key: '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + keyType: 'Array', + valueType: 'address', + valueContent: 'Address', + }; + + const duplicatedSchemas = duplicateMultiTypeERC725SchemaEntry(schema); + + assert.deepStrictEqual(duplicatedSchemas, [schema]); + }); + it('returns the exact input in an array if there is only one dynamic type in it for Mapping', () => { + const schema: ERC725JSONSchema = { + name: 'LSP1UniversalReceiverDelegate:', + key: '0x0cfc51aec37c55a4d0b10000', + keyType: 'Mapping', + valueType: 'address', + valueContent: 'Address', + }; + + const duplicatedSchemas = duplicateMultiTypeERC725SchemaEntry(schema); + + assert.deepStrictEqual(duplicatedSchemas, [schema]); + }); + it('returns the exact input in an array if there is only one dynamic type in it for MappingWithGrouping', () => { + const schema: ERC725JSONSchema = { + name: 'AddressPermissions:AllowedCalls:
', + key: '0x4b80742de2bf393a64c70000
', + keyType: 'MappingWithGrouping', + valueType: '(bytes4,address,bytes4,bytes4)[CompactBytesArray]', + valueContent: '(BitArray,Address,Bytes4,Bytes4)', + }; + + const duplicatedSchemas = duplicateMultiTypeERC725SchemaEntry(schema); + + assert.deepStrictEqual(duplicatedSchemas, [schema]); + }); + it('splits and returns one schema for each dynamic name type for Mapping', () => { + const schema: ERC725JSONSchema = { + name: 'LSP8MetadataTokenURI:', + key: '0x1339e76a390b7b9ec9010000', + keyType: 'Mapping', + valueType: '(bytes4,string)', + valueContent: '(Bytes4,URI)', + }; + + const expectedSchemas: ERC725JSONSchema[] = [ + { + name: 'LSP8MetadataTokenURI:
', + key: '0x1339e76a390b7b9ec9010000
', + keyType: 'Mapping', + valueType: '(bytes4,string)', + valueContent: '(Bytes4,URI)', + }, + { + name: 'LSP8MetadataTokenURI:', + key: '0x1339e76a390b7b9ec9010000', + keyType: 'Mapping', + valueType: '(bytes4,string)', + valueContent: '(Bytes4,URI)', + }, + { + name: 'LSP8MetadataTokenURI:', + key: '0x1339e76a390b7b9ec9010000', + keyType: 'Mapping', + valueType: '(bytes4,string)', + valueContent: '(Bytes4,URI)', + }, + { + name: 'LSP8MetadataTokenURI:', + key: '0x1339e76a390b7b9ec9010000', + keyType: 'Mapping', + valueType: '(bytes4,string)', + valueContent: '(Bytes4,URI)', + }, + ]; + + const duplicatedSchemas = duplicateMultiTypeERC725SchemaEntry(schema); + + assert.deepStrictEqual(duplicatedSchemas, expectedSchemas); + }); + it('splits and returns one schema for each dynamic name type for MappingWithGrouping', () => { + const schema: ERC725JSONSchema = { + name: 'AddressPermissions:AllowedCalls:', + key: '0x4b80742de2bf393a64c70000', + keyType: 'MappingWithGrouping', + valueType: 'address', + valueContent: 'Address', + }; + + const expectedSchemas: ERC725JSONSchema[] = [ + { + name: 'AddressPermissions:AllowedCalls:
', + key: '0x4b80742de2bf393a64c70000
', + keyType: 'MappingWithGrouping', + valueType: 'address', + valueContent: 'Address', + }, + { + name: 'AddressPermissions:AllowedCalls:', + key: '0x4b80742de2bf393a64c70000', + keyType: 'MappingWithGrouping', + valueType: 'address', + valueContent: 'Address', + }, + ]; + + const duplicatedSchemas = duplicateMultiTypeERC725SchemaEntry(schema); + + assert.deepStrictEqual(duplicatedSchemas, expectedSchemas); + }); + it('splits and returns one schema for each dynamic name type for MappingWithGrouping with multiple types', () => { + const schema: ERC725JSONSchema = { + name: 'MyKeyName::', + key: '0x35e6950bc8d2', + keyType: 'MappingWithGrouping', + valueType: 'address', + valueContent: 'Address', + }; + + const expectedSchemas: ERC725JSONSchema[] = [ + { + name: 'MyKeyName::', + key: '0x35e6950bc8d2', + keyType: 'MappingWithGrouping', + valueType: 'address', + valueContent: 'Address', + }, + { + name: 'MyKeyName::
', + key: '0x35e6950bc8d2
', + keyType: 'MappingWithGrouping', + valueType: 'address', + valueContent: 'Address', + }, + { + name: 'MyKeyName:
:', + key: '0x35e6950bc8d2
', + keyType: 'MappingWithGrouping', + valueType: 'address', + valueContent: 'Address', + }, + { + name: 'MyKeyName:
:
', + key: '0x35e6950bc8d2
', + keyType: 'MappingWithGrouping', + valueType: 'address', + valueContent: 'Address', + }, + ]; + + const duplicatedSchemas = duplicateMultiTypeERC725SchemaEntry(schema); + + assert.deepStrictEqual(duplicatedSchemas, expectedSchemas); + }); + }); }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a29a9a72..99c6680a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -581,3 +581,167 @@ export function patchIPFSUrlsIfApplicable( export function countNumberOfBytes(data: string) { return stripHexPrefix(data).length / 2; } + +/** + * Given an input string which can define dynamic types, will return an array with all types + * In: + * Out: ['address', 'uint256'] + * + * In: NotDynamic + * Out: ['NotDynamic'] + * + * It does not veryfi whether these types are valid. It just processes the string. + * + * @param keyName + */ +export const splitMultiDynamicKeyNamePart = (keyName: string): string[] => { + if (keyName.length <= 1) { + return [keyName]; + } + + if (keyName.charAt(0) !== '<' || keyName.slice(-1) !== '>') { + return [keyName]; + } + + return keyName.substring(1, keyName.length - 1).split('|'); +}; + +/** + * This function helps to duplicate schema entries with multiple types to prepare schemas loaded on init. + * It does not check whether the input schema is valid or not, as long as it have the name, key and keyType keys, it will proceed. + * + * Input: + * { + * "name": "LSP8MetadataTokenURI:", + * "key": "0x1339e76a390b7b9ec9010000", + * "keyType": "Mapping", + * "valueType": "(bytes4,string)", + * "valueContent": "(Bytes4,URI)" + * } + * + * Output: + * + * [{ + * "name": "LSP8MetadataTokenURI:
", + * "key": "0x1339e76a390b7b9ec9010000
", + * "keyType": "Mapping", + * "valueType": "(bytes4,string)", + * "valueContent": "(Bytes4,URI)" + * }, + * { + * "name": "LSP8MetadataTokenURI:", + * "key": "0x1339e76a390b7b9ec9010000", + * "keyType": "Mapping", + * "valueType": "(bytes4,string)", + * "valueContent": "(Bytes4,URI)" + * }] + * + * Having such a duplicated schema for lookup will allow the rest of the lib to behave the same way as it was. + * + * @param schema + */ +export const duplicateMultiTypeERC725SchemaEntry = ( + schema: ERC725JSONSchema, +): ERC725JSONSchema[] => { + if (Array.isArray(schema)) { + throw new Error( + 'Input schema should be a ERC725JSONSchema and not an array.', + ); + } + + if (!('name' in schema) || !('key' in schema) || !('keyType' in schema)) { + throw new Error( + "Input schema object is missing 'name', 'key' and 'keyType' properties. Did you pass a valid ERC725JSONSchema object?", + ); + } + + const lowerCaseKeyType = schema.keyType.toLowerCase(); + + if ( + lowerCaseKeyType !== 'mapping' && + lowerCaseKeyType !== 'mappingwithgrouping' + ) { + return [schema]; + } + + // A very naive way to check for dynamic parts... + if ( + !schema.name.includes('<') || + !schema.name.includes('|') || + !schema.name.includes('>') + ) { + return [schema]; + } + + if (schema.name.indexOf(':') === -1) { + throw new Error( + `Input schema type is Mapping or MappingWithGroups but the key: ${schema.key} is not valid (missing ':').`, + ); + } + + const nameGroups = schema.name.split(':'); // The check above ensures this split is ok + const baseKey = schema.key.substring(0, schema.key.indexOf('<')); + + const baseSchema: ERC725JSONSchema = { + ...schema, + name: nameGroups.shift() as string, // The first element of the key name is never dynamic. + key: baseKey, + }; + + // Case for Mapping, there is only one group left, and this group is dynamic + if (nameGroups.length === 1) { + const dynamicTypes = splitMultiDynamicKeyNamePart(nameGroups[0]); + const finalSchemas = dynamicTypes.map((dynamicType) => { + return { + ...baseSchema, + name: `${baseSchema.name}:<${dynamicType}>`, + key: `${baseSchema.key}<${dynamicType}>`, + }; + }); + + return finalSchemas; + } + + // Case MappingWithGrouping (name groups had multiple :) + // We have 2 cases: + // 1. One dynamic type: Name:LastName: + // 2. Two dynamic types: Name:: + + let finalSchemas: ERC725JSONSchema[]; + + // Case 1 - middle part is not dynamic + if (nameGroups[0].charAt(0) !== '<' || nameGroups[0].slice(-1) !== '>') { + finalSchemas = [ + { + ...baseSchema, + name: `${baseSchema.name}:${nameGroups[0]}`, + key: `${baseSchema.key}`, + }, + ]; + } else { + // Case 2 - middle part is dynamic + const dynamicTypes = splitMultiDynamicKeyNamePart(nameGroups[0]); + finalSchemas = dynamicTypes.map((dynamicType) => { + return { + ...baseSchema, + name: `${baseSchema.name}:<${dynamicType}>`, + key: `${baseSchema.key}<${dynamicType}>`, + }; + }); + } + + // Processing of the last part of the group - which is dynamic + const lastDynamicTypes = splitMultiDynamicKeyNamePart(nameGroups[1]); + + return finalSchemas + .map((finalSchema) => { + return lastDynamicTypes.map((lastDynamicType) => { + return { + ...finalSchema, + name: `${finalSchema.name}:<${lastDynamicType}>`, + key: `${finalSchema.key}<${lastDynamicType}>`, + }; + }); + }) + .flat(); +};