diff --git a/src/index.ts b/src/index.ts index fb8cab52..d571f779 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ import { encodeKeyName, isDynamicKeyName } from './lib/encodeKeyName'; import { ERC725Config, ERC725Options } from './types/Config'; import { Permissions } from './types/Method'; import { + DynamicNameSchema, ERC725JSONSchema, ERC725JSONSchemaKeyType, ERC725JSONSchemaValueContent, @@ -96,6 +97,7 @@ export { export { getDataFromExternalSources } from './lib/getDataFromExternalSources'; export { encodePermissions, decodePermissions } from './lib/permissions'; export { checkPermissions } from './lib/detector'; +export { getSchema } from './lib/schemaParser'; // PRIVATE FUNCTION function initializeProvider(providerOrRpcUrl, gasInfo) { @@ -358,15 +360,19 @@ export class ERC725 { getSchema( keyOrKeys: string[], providedSchemas?: ERC725JSONSchema[], - ): Record; + ): Record; getSchema( keyOrKeys: string, providedSchemas?: ERC725JSONSchema[], - ): ERC725JSONSchema | null; + ): ERC725JSONSchema | DynamicNameSchema | null; getSchema( keyOrKeys: string | string[], providedSchemas?: ERC725JSONSchema[], - ): ERC725JSONSchema | null | Record { + ): + | ERC725JSONSchema + | DynamicNameSchema + | null + | Record { return getSchema( keyOrKeys, this.options.schemas.concat(providedSchemas || []), diff --git a/src/lib/encodeKeyName.ts b/src/lib/encodeKeyName.ts index 24328a64..7cf4c0da 100644 --- a/src/lib/encodeKeyName.ts +++ b/src/lib/encodeKeyName.ts @@ -34,7 +34,7 @@ import { DynamicKeyParts } from '../types/dynamicKeys'; const dynamicTypes = ['', '
', '']; // https://docs.soliditylang.org/en/v0.8.14/abi-spec.html#types -const dynamicTypesRegex = /<(uint|int|bytes)(\d+)>/; +export const dynamicTypesRegex = /<(uint|int|bytes)(\d+)>/; /** * diff --git a/src/lib/schemaParser.test.ts b/src/lib/schemaParser.test.ts index 21a1f370..58960c7a 100644 --- a/src/lib/schemaParser.test.ts +++ b/src/lib/schemaParser.test.ts @@ -105,14 +105,15 @@ describe('schemaParser getSchema', () => { name: 'SupportedStandards:??????', key: '0xeafec4d89fa9619884b60000f4d7faed14a1ab658d46d385bc29fb1eeaa56d0b', keyType: 'Mapping', - valueContent: '?', + valueContent: '0x5ef83ad9', valueType: 'bytes4', }); }); it('finds Known Mapping:
', () => { const address = 'af3bf2ffb025098b79caddfbdd113b3681817744'; - const name = `MyCoolAddress:${address}`; + const name = 'MyCoolAddress:
'; + const dynamicName = `MyCoolAddress:0x${address}`; const key = `0x22496f48a493035f00000000${address}`; const extraSchema: ERC725JSONSchema = { @@ -125,14 +126,20 @@ describe('schemaParser getSchema', () => { const schema = getSchema(key, [extraSchema]); - assert.deepStrictEqual(schema, extraSchema); + assert.deepStrictEqual(schema, { + ...extraSchema, + dynamicKeyPart: `0x${address}`, + dynamicName, + }); }); it('finds known SomeBytes32Mapping:', () => { const bytes32Value = '1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff'; - const name = `SomeBytes32Mapping:${bytes32Value}`; - const key = `0x0cfc51aec37c55a4d0b10000${bytes32Value.slice(0, 42)}`; + const name = 'SomeBytes32Mapping:'; + const dynamicPart = bytes32Value.slice(0, 40); + const dynamicName = `SomeBytes32Mapping:0x${dynamicPart}`; + const key = `0x0cfc51aec37c55a4d0b10000${dynamicPart}`; const extraSchema: ERC725JSONSchema = { name, @@ -144,12 +151,17 @@ describe('schemaParser getSchema', () => { const schema = getSchema(key, [extraSchema]); - assert.deepStrictEqual(schema, extraSchema); + assert.deepStrictEqual(schema, { + ...extraSchema, + dynamicName, + dynamicKeyPart: `0x${dynamicPart}`, + }); }); it('finds known SomeSelectorMap:', () => { const bytes4Value = 'beefbeef'; - const name = `SomeSelectorMap:${bytes4Value}`; + const name = 'SomeSelectorMap:'; + const dynamicName = `SomeSelectorMap:0x${bytes4Value}`; const key = `0x0cfc51aec37c55a4d0b10000${bytes4Value}00000000000000000000000000000000`; const extraSchema: ERC725JSONSchema = { @@ -162,20 +174,52 @@ describe('schemaParser getSchema', () => { const schema = getSchema(key, [extraSchema]); - assert.deepStrictEqual(schema, extraSchema); + assert.deepStrictEqual(schema, { + ...extraSchema, + dynamicName, + dynamicKeyPart: `0x${bytes4Value}`, + }); + }); + + it('finds Known LSP1UniversalReceiverDelegate: ', () => { + const bytes32value = + 'cafecafecafecafecafecafecafecafecafecafef00df00df00df00df00df00d'; + const name = 'LSP1UniversalReceiverDelegate:'; + const dynamicPart = bytes32value.slice(0, 40); + const dynamicName = `LSP1UniversalReceiverDelegate:0x${dynamicPart}`; + const key = `0x0cfc51aec37c55a4d0b10000${dynamicPart}`; + + const extraSchema: ERC725JSONSchema = { + name, + key, + keyType: 'Mapping', + valueContent: 'Address', + valueType: 'address', + }; + + const schema = getSchema(key, [extraSchema]); + + assert.deepStrictEqual(schema, { + ...extraSchema, + dynamicName, + dynamicKeyPart: `0x${dynamicPart}`, + }); }); }); describe('MappingWithGrouping', () => { it('finds MappingWithGrouping', () => { const address = 'af3bf2ffb025098b79caddfbdd113b3681817744'; - const name = `AddressPermissions:Permissions:${address}`; + const name = 'AddressPermissions:Permissions:
'; + const dynamicName = `AddressPermissions:Permissions:0x${address}`; const key = `0x4b80742de2bf82acb3630000${address}`; const schema = getSchema(key); assert.deepStrictEqual(schema, { name, + dynamicName, key, + dynamicKeyPart: `0x${address}`, keyType: 'MappingWithGrouping', valueContent: 'BitArray', valueType: 'bytes32', diff --git a/src/lib/schemaParser.ts b/src/lib/schemaParser.ts index d2e94c52..fee9f607 100644 --- a/src/lib/schemaParser.ts +++ b/src/lib/schemaParser.ts @@ -16,15 +16,15 @@ * @author Hugo Masclet <@Hugoo> * @date 2022 */ - import { keccak256 } from 'web3-utils'; import allSchemas from '../schemas'; import { + DynamicNameSchema, ERC725JSONSchema, ERC725JSONSchemaKeyType, } from '../types/ERC725JSONSchema'; -import { isDynamicKeyName } from './encodeKeyName'; +import { dynamicTypesRegex, isDynamicKeyName } from './encodeKeyName'; const getSchemasByKeyType = ( schemas: ERC725JSONSchema[], @@ -39,6 +39,59 @@ const getSchemasByKeyType = ( }; }; +const fillDynamicKeyPart = ( + key: string, + keySchema: ERC725JSONSchema, +): ERC725JSONSchema | DynamicNameSchema => { + const result: ERC725JSONSchema | DynamicNameSchema = { ...keySchema, key }; + + const keyNameParts = keySchema.name.split(':'); + const secondWordHex = key.substring(26); + + // 2. "Semi defined mappings" i.e. "SupportedStandards:??????" + let dynamicPartName = '??????'; // default for "unknown" + + // replace dynamic placeholder in the map part (e.g:
, ) with the hex value + if (isDynamicKeyName(keySchema.name)) { + dynamicPartName = secondWordHex; + + let dynamicName = `${keyNameParts[0]}:0x${dynamicPartName}`; + let dynamicKeyPart = `0x${secondWordHex}`; + + const dynamicPartType = keyNameParts[1].match(dynamicTypesRegex); + + if (dynamicPartType) { + const byteSize = + dynamicPartType[1] === 'uint' || dynamicPartType[1] === 'int' + ? Number.parseInt(dynamicPartType[2]) / 8 // e.g: uint128 -> 128 / 8 -> 16 bytes + : Number.parseInt(dynamicPartType[2]); // e.g: bytes8 -> 8 bytes + + if (byteSize < 20) { + dynamicName = `${keyNameParts[0]}:0x${dynamicPartName.slice( + 0, + byteSize * 2, + )}`; + + dynamicKeyPart = `0x${secondWordHex.slice(0, byteSize * 2)}`; + } + } + + (result as DynamicNameSchema).dynamicName = dynamicName; + (result as DynamicNameSchema).dynamicKeyPart = dynamicKeyPart; + + return result; + } + + // if first 20 bytes of the hash of second word in schema match, + // display the map part as plain word + if (keccak256(keyNameParts[1]).substring(0, 42) === `0x${secondWordHex}`) { + dynamicPartName = keyNameParts[1]; + } + result.name = `${keyNameParts[0]}:${dynamicPartName}`; + + return result; +}; + const findSingletonSchemaForKey = ( key: string, schemas: ERC725JSONSchema[], @@ -87,22 +140,17 @@ const findArraySchemaForKey = ( const findMappingSchemaForKey = ( key: string, schemas: ERC725JSONSchema[], -): ERC725JSONSchema | null => { +): ERC725JSONSchema | DynamicNameSchema | null => { const firstWordHex = key.substring(0, 26); - const secondWordHex = key.substring(26); - // Should detect: - // 1. Known/defined mapping + // Known/defined mapping let keySchema = schemas.find((schema) => schema.key === key) || null; if (keySchema) { - return keySchema; + return fillDynamicKeyPart(key, keySchema); } - // 2. "Semi defined mappings" i.e. "SupportedStandards:??????" - let dynamicPart = '??????'; // default for "unknown" - keySchema = schemas.find( (schema) => `${schema.key.substring(0, 22)}0000` === firstWordHex, @@ -112,47 +160,32 @@ const findMappingSchemaForKey = ( return null; } - const keyNameParts = keySchema.name.split(':'); - - // replace dynamic placeholder in the map part (e.g:
, ) with the hex value - if (isDynamicKeyName(keySchema.name)) { - dynamicPart = secondWordHex; - } - - // if first 20 bytes of the hash of second word in schema match, - // display the map part as plain word - if (keccak256(keyNameParts[1]).substring(0, 26) === secondWordHex) { - [, dynamicPart] = keyNameParts; - } - - // TODO: Handle the SupportedStandard Keys; we can get the valueContent from the Keys - return { - ...keySchema, - valueContent: '?', - name: `${keyNameParts[0]}:${dynamicPart}`, - key, - }; + return fillDynamicKeyPart(key, keySchema); }; const findMappingWithGroupingSchemaForKey = ( key: string, schemas: ERC725JSONSchema[], -): ERC725JSONSchema | null => { +): ERC725JSONSchema | DynamicNameSchema | null => { const keySchema = schemas.find( (schema) => schema.key.substring(0, 26) === key.substring(0, 26), ) || null; - const address = key.substring(26); - if (keySchema) { + const keyNameParts = keySchema.name.split(':'); + + const dynamicKeyPart = key.substring(26); + + if (isDynamicKeyName(keySchema.name)) { + (keySchema as DynamicNameSchema).dynamicName = + `${keyNameParts[0]}:${keyNameParts[1]}:0x${dynamicKeyPart}`; + (keySchema as DynamicNameSchema).dynamicKeyPart = `0x${dynamicKeyPart}`; + } + return { ...keySchema, key, - name: `${keySchema.name.substring( - 0, - keySchema.name.lastIndexOf(':'), - )}:${address}`, }; } @@ -162,7 +195,7 @@ const findMappingWithGroupingSchemaForKey = ( function schemaParser( key: string, schemas: ERC725JSONSchema[], -): ERC725JSONSchema | null { +): ERC725JSONSchema | DynamicNameSchema | null { const schemasByKeyType = getSchemasByKeyType(schemas); let foundSchema: ERC725JSONSchema | null = null; @@ -196,20 +229,23 @@ function schemaParser( export function getSchema( keyOrKeys: string | string[], providedSchemas?: ERC725JSONSchema[], -): ERC725JSONSchema | null | Record { +): + | ERC725JSONSchema + | DynamicNameSchema + | null + | Record { let fullSchema: ERC725JSONSchema[] = allSchemas; if (providedSchemas) { fullSchema = fullSchema.concat(providedSchemas); } if (Array.isArray(keyOrKeys)) { - return keyOrKeys.reduce>( - (acc, key) => { - acc[key] = schemaParser(key, fullSchema); - return acc; - }, - {}, - ); + return keyOrKeys.reduce< + Record + >((acc, key) => { + acc[key] = schemaParser(key, fullSchema); + return acc; + }, {}); } return schemaParser(keyOrKeys, fullSchema); diff --git a/src/types/ERC725JSONSchema.ts b/src/types/ERC725JSONSchema.ts index 5eb0eced..1aa64f08 100644 --- a/src/types/ERC725JSONSchema.ts +++ b/src/types/ERC725JSONSchema.ts @@ -163,3 +163,10 @@ export interface ERC725JSONSchema { valueContent: ERC725JSONSchemaValueContent | string; // string holds '0x1345ABCD...' If the value content are specific bytes, than the returned value is expected to equal those bytes. valueType: ERC725JSONSchemaValueType | string; // The type of the value. This is used to determine how the value should be encoded / decode (`string` for tuples and CompactBytesArray). } + +// The dynamic part placeholder in the `name` of ERC725JSONSchema is preserved to allow re-encoding after the schema +// of a hex data key got retrieved via `getSchema(...)`. +export interface DynamicNameSchema extends ERC725JSONSchema { + dynamicName: string; // Describes the name of the key where the dynamic part (
,