Skip to content

Commit

Permalink
Merge pull request #428 from ERC725Alliance/feat/dynamic-name-schema
Browse files Browse the repository at this point in the history
feat: add `dynamicName` in schema returned by `getSchema`
  • Loading branch information
CJ42 authored May 3, 2024
2 parents a218ef3 + 6b73a7e commit 23e211d
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 59 deletions.
12 changes: 9 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -358,15 +360,19 @@ export class ERC725 {
getSchema(
keyOrKeys: string[],
providedSchemas?: ERC725JSONSchema[],
): Record<string, ERC725JSONSchema | null>;
): Record<string, ERC725JSONSchema | DynamicNameSchema | null>;
getSchema(
keyOrKeys: string,
providedSchemas?: ERC725JSONSchema[],
): ERC725JSONSchema | null;
): ERC725JSONSchema | DynamicNameSchema | null;
getSchema(
keyOrKeys: string | string[],
providedSchemas?: ERC725JSONSchema[],
): ERC725JSONSchema | null | Record<string, ERC725JSONSchema | null> {
):
| ERC725JSONSchema
| DynamicNameSchema
| null
| Record<string, ERC725JSONSchema | DynamicNameSchema | null> {
return getSchema(
keyOrKeys,
this.options.schemas.concat(providedSchemas || []),
Expand Down
2 changes: 1 addition & 1 deletion src/lib/encodeKeyName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { DynamicKeyParts } from '../types/dynamicKeys';
const dynamicTypes = ['<string>', '<address>', '<bool>'];

// 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+)>/;

/**
*
Expand Down
62 changes: 53 additions & 9 deletions src/lib/schemaParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ describe('schemaParser getSchema', () => {
name: 'SupportedStandards:??????',
key: '0xeafec4d89fa9619884b60000f4d7faed14a1ab658d46d385bc29fb1eeaa56d0b',
keyType: 'Mapping',
valueContent: '?',
valueContent: '0x5ef83ad9',
valueType: 'bytes4',
});
});

it('finds Known Mapping:<address> ', () => {
const address = 'af3bf2ffb025098b79caddfbdd113b3681817744';
const name = `MyCoolAddress:${address}`;
const name = 'MyCoolAddress:<address>';
const dynamicName = `MyCoolAddress:0x${address}`;
const key = `0x22496f48a493035f00000000${address}`;

const extraSchema: ERC725JSONSchema = {
Expand All @@ -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:<bytes32>', () => {
const bytes32Value =
'1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff';
const name = `SomeBytes32Mapping:${bytes32Value}`;
const key = `0x0cfc51aec37c55a4d0b10000${bytes32Value.slice(0, 42)}`;
const name = 'SomeBytes32Mapping:<bytes32>';
const dynamicPart = bytes32Value.slice(0, 40);
const dynamicName = `SomeBytes32Mapping:0x${dynamicPart}`;
const key = `0x0cfc51aec37c55a4d0b10000${dynamicPart}`;

const extraSchema: ERC725JSONSchema = {
name,
Expand All @@ -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:<bytes4>', () => {
const bytes4Value = 'beefbeef';
const name = `SomeSelectorMap:${bytes4Value}`;
const name = 'SomeSelectorMap:<bytes4>';
const dynamicName = `SomeSelectorMap:0x${bytes4Value}`;
const key = `0x0cfc51aec37c55a4d0b10000${bytes4Value}00000000000000000000000000000000`;

const extraSchema: ERC725JSONSchema = {
Expand All @@ -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:<bytes32> ', () => {
const bytes32value =
'cafecafecafecafecafecafecafecafecafecafef00df00df00df00df00df00d';
const name = 'LSP1UniversalReceiverDelegate:<bytes32>';
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:<address>';
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',
Expand Down
128 changes: 82 additions & 46 deletions src/lib/schemaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -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: <address>, <bytes32>) 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[],
Expand Down Expand Up @@ -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,
Expand All @@ -112,47 +160,32 @@ const findMappingSchemaForKey = (
return null;
}

const keyNameParts = keySchema.name.split(':');

// replace dynamic placeholder in the map part (e.g: <address>, <bytes32>) 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}`,
};
}

Expand All @@ -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;
Expand Down Expand Up @@ -196,20 +229,23 @@ function schemaParser(
export function getSchema(
keyOrKeys: string | string[],
providedSchemas?: ERC725JSONSchema[],
): ERC725JSONSchema | null | Record<string, ERC725JSONSchema | null> {
):
| ERC725JSONSchema
| DynamicNameSchema
| null
| Record<string, ERC725JSONSchema | DynamicNameSchema | null> {
let fullSchema: ERC725JSONSchema[] = allSchemas;
if (providedSchemas) {
fullSchema = fullSchema.concat(providedSchemas);
}

if (Array.isArray(keyOrKeys)) {
return keyOrKeys.reduce<Record<string, ERC725JSONSchema | null>>(
(acc, key) => {
acc[key] = schemaParser(key, fullSchema);
return acc;
},
{},
);
return keyOrKeys.reduce<
Record<string, ERC725JSONSchema | DynamicNameSchema | null>
>((acc, key) => {
acc[key] = schemaParser(key, fullSchema);
return acc;
}, {});
}

return schemaParser(keyOrKeys, fullSchema);
Expand Down
7 changes: 7 additions & 0 deletions src/types/ERC725JSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<address>, <bytes32) is replaced by the actual mapped value.
dynamicKeyPart: string;
}

0 comments on commit 23e211d

Please sign in to comment.