Skip to content

Commit

Permalink
feat: add support for multi types in mappings
Browse files Browse the repository at this point in the history
  • Loading branch information
Hugoo committed Nov 23, 2023
1 parent c931219 commit 3e337ea
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 1 deletion.
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
encodeData,
convertIPFSGatewayUrl,
generateSchemasFromDynamicKeys,
duplicateMultiTypeERC725SchemaEntry,
} from './lib/utils';

import { getSchema } from './lib/schemaParser';
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/lib/encodeKeyName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export const encodeDynamicKeyPart = (
}
};

// This function does not support multi dynamic types such as MyName:<string|address>
export function isDynamicKeyName(name: string) {
const keyNameParts = name.split(':');

Expand Down
179 changes: 179 additions & 0 deletions src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
convertIPFSGatewayUrl,
generateSchemasFromDynamicKeys,
encodeTupleKeyValue,
duplicateMultiTypeERC725SchemaEntry,
splitMultiDynamicKeyNamePart,
} from './utils';
import { isDynamicKeyName } from './encodeKeyName';
import { decodeKey } from './decodeData';
Expand Down Expand Up @@ -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 = '<address|bytes32|uint256>';

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:<bytes32>',
key: '0x0cfc51aec37c55a4d0b10000<bytes32>',
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:<address>',
key: '0x4b80742de2bf393a64c70000<address>',
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:<address|uint256|bytes32|string>',
key: '0x1339e76a390b7b9ec9010000<address|uint256|bytes32|string>',
keyType: 'Mapping',
valueType: '(bytes4,string)',
valueContent: '(Bytes4,URI)',
};

const expectedSchemas: ERC725JSONSchema[] = [
{
name: 'LSP8MetadataTokenURI:<address>',
key: '0x1339e76a390b7b9ec9010000<address>',
keyType: 'Mapping',
valueType: '(bytes4,string)',
valueContent: '(Bytes4,URI)',
},
{
name: 'LSP8MetadataTokenURI:<uint256>',
key: '0x1339e76a390b7b9ec9010000<uint256>',
keyType: 'Mapping',
valueType: '(bytes4,string)',
valueContent: '(Bytes4,URI)',
},
{
name: 'LSP8MetadataTokenURI:<bytes32>',
key: '0x1339e76a390b7b9ec9010000<bytes32>',
keyType: 'Mapping',
valueType: '(bytes4,string)',
valueContent: '(Bytes4,URI)',
},
{
name: 'LSP8MetadataTokenURI:<string>',
key: '0x1339e76a390b7b9ec9010000<string>',
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:<address|string>',
key: '0x4b80742de2bf393a64c70000<address|string>',
keyType: 'MappingWithGrouping',
valueType: 'address',
valueContent: 'Address',
};

const expectedSchemas: ERC725JSONSchema[] = [
{
name: 'AddressPermissions:AllowedCalls:<address>',
key: '0x4b80742de2bf393a64c70000<address>',
keyType: 'MappingWithGrouping',
valueType: 'address',
valueContent: 'Address',
},
{
name: 'AddressPermissions:AllowedCalls:<string>',
key: '0x4b80742de2bf393a64c70000<string>',
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:<bytes2|address>:<uint32|address>',
key: '0x35e6950bc8d2<bytes2|address><uint32|address>',
keyType: 'MappingWithGrouping',
valueType: 'address',
valueContent: 'Address',
};

const expectedSchemas: ERC725JSONSchema[] = [
{
name: 'MyKeyName:<bytes2>:<uint32>',
key: '0x35e6950bc8d2<bytes2><uint32>',
keyType: 'MappingWithGrouping',
valueType: 'address',
valueContent: 'Address',
},
{
name: 'MyKeyName:<bytes2>:<address>',
key: '0x35e6950bc8d2<bytes2><address>',
keyType: 'MappingWithGrouping',
valueType: 'address',
valueContent: 'Address',
},
{
name: 'MyKeyName:<address>:<uint32>',
key: '0x35e6950bc8d2<address><uint32>',
keyType: 'MappingWithGrouping',
valueType: 'address',
valueContent: 'Address',
},
{
name: 'MyKeyName:<address>:<address>',
key: '0x35e6950bc8d2<address><address>',
keyType: 'MappingWithGrouping',
valueType: 'address',
valueContent: 'Address',
},
];

const duplicatedSchemas = duplicateMultiTypeERC725SchemaEntry(schema);

assert.deepStrictEqual(duplicatedSchemas, expectedSchemas);
});
});
});
164 changes: 164 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <address|uint256>
* 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:<address|uint256>",
* "key": "0x1339e76a390b7b9ec9010000<address|uint256>",
* "keyType": "Mapping",
* "valueType": "(bytes4,string)",
* "valueContent": "(Bytes4,URI)"
* }
*
* Output:
*
* [{
* "name": "LSP8MetadataTokenURI:<address>",
* "key": "0x1339e76a390b7b9ec9010000<address>",
* "keyType": "Mapping",
* "valueType": "(bytes4,string)",
* "valueContent": "(Bytes4,URI)"
* },
* {
* "name": "LSP8MetadataTokenURI:<uint256>",
* "key": "0x1339e76a390b7b9ec9010000<uint256>",
* "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:<address|uint256>
// 2. Two dynamic types: Name:<address|bytes>:<address|uint256>

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();
};

0 comments on commit 3e337ea

Please sign in to comment.