From f0f495c69e8b4768468437cad24f1b77d53a379f Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Wed, 20 Mar 2024 14:16:34 +0100 Subject: [PATCH 1/9] feat: add dynamic array encoding --- src/lib/utils.ts | 52 ++++++++++++++++++++++++++++++++++++----- src/types/decodeData.ts | 2 ++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 132a793f..6f2a8fe2 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -243,6 +243,8 @@ export function encodeKey( | URLDataToEncode | URLDataToEncode[] | boolean, + startingIndex = 0, + arrayLength = Array.isArray(value) ? value.length : 0, ) { // NOTE: This will not guarantee order of array as on chain. Assumes developer must set correct order @@ -260,21 +262,46 @@ export function encodeKey( return null; } + // Check if an starting index and array lenght are correct types + if ( + typeof startingIndex !== 'number' || + typeof arrayLength !== 'number' + ) { + throw new Error( + 'Invalid startingIndex or arrayLength parameters. Values must be of type number', + ); + } + + if (startingIndex < 0) { + throw new Error( + 'Invalid startingIndex parameter. Value cannot be negative.', + ); + } + + if (arrayLength < value.length) { + throw new Error( + 'Invalid arrayLength parameter. Array length must be at least as large as the number of elements of the value array.', + ); + } + const results: { key: string; value: string }[] = []; for (let index = 0; index < value.length; index++) { - const dataElement = value[index]; if (index === 0) { // This is arrayLength as the first element in the raw array // encoded as uint128 results.push({ key: schema.key, - value: encodeValueType('uint128', value.length), + // Encode the explicitly provided or default array length + value: encodeValueType('uint128', arrayLength), }); } + const elementIndex = startingIndex + index; + const dataElement = value[index]; + results.push({ - key: encodeArrayKey(schema.key, index), + key: encodeArrayKey(schema.key, elementIndex), value: encodeKeyValue( schema.valueContent, schema.valueType, @@ -439,7 +466,10 @@ export function encodeData( const dataAsArray = Array.isArray(data) ? data : [data]; return dataAsArray.reduce( - (accumulator, { keyName, value, dynamicKeyParts }) => { + ( + accumulator, + { keyName, value, dynamicKeyParts, startingIndex, arrayLength }, + ) => { let schemaElement: ERC725JSONSchema | null = null; let encodedValue; // would be nice to type this @@ -453,10 +483,20 @@ export function encodeData( } schemaElement = getSchemaElement(schema, keyName, dynamicKeyParts); - encodedValue = encodeKey(schemaElement, value); + encodedValue = encodeKey( + schemaElement, + value, + startingIndex, + arrayLength, + ); } else { schemaElement = getSchemaElement(schema, keyName); - encodedValue = encodeKey(schemaElement, value as any); + encodedValue = encodeKey( + schemaElement, + value as any, + startingIndex, + arrayLength, + ); } if (typeof encodedValue === 'string') { diff --git a/src/types/decodeData.ts b/src/types/decodeData.ts index b0e32342..b7d62c33 100644 --- a/src/types/decodeData.ts +++ b/src/types/decodeData.ts @@ -4,6 +4,8 @@ export interface DataInput { keyName: string; // can be the name or the hex/hash value; dynamicKeyParts?: string | string[]; + arrayLength?: number; + startingIndex?: number; } export interface EncodeDataInput extends DataInput { From 6590536cc01c71862d85f85c58d4f6584e22c6e1 Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Wed, 20 Mar 2024 14:16:49 +0100 Subject: [PATCH 2/9] test: add dynamic array encoding tests --- src/lib/utils.test.ts | 162 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 8a8ee3f2..396102ef 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -720,6 +720,168 @@ describe('utils', () => { }); }); + describe('encodeData with custom array length and starting index', () => { + const schemas: ERC725JSONSchema[] = [ + { + name: 'AddressPermissions[]', + key: '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + keyType: 'Array', + valueType: 'address', + valueContent: 'Address', + }, + ]; + + it('should be able to specify the array length + starting index', () => { + const encodedArraySection = encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + arrayLength: 23, + startingIndex: 21, + }, + ], + schemas, + ); + + // Expected result with custom startingIndex and arrayLength + const expectedResult = { + keys: [ + '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000015', // 21 + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000016', // 22 + ], + values: [ + '0x00000000000000000000000000000017', // 23 + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + }; + + assert.deepStrictEqual( + encodedArraySection, + expectedResult, + 'Encoding with custom starting index and array length should match the expected result.', + ); + }); + + it('should throw if startingIndex is negative', () => { + const encodeDataWithNegativeStartingIndex = () => { + encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: ['0x983abc616f2442bab7a917e6bb8660df8b01f3bf'], + arrayLength: 1, + startingIndex: -1, + }, + ], + schemas, + ); + }; + + assert.throws( + encodeDataWithNegativeStartingIndex, + /Invalid startingIndex/, + 'Should throw an error for negative startingIndex', + ); + }); + + it('should throw if arrayLength is smaller than elements in provided value array', () => { + const encodeDataWithLowerArrayLenght = () => { + encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + arrayLength: 1, // 2 elements + startingIndex: 0, + }, + ], + schemas, + ); + }; + + assert.throws( + encodeDataWithLowerArrayLenght, + /Invalid arrayLength/, + 'Should throw an error for arrayLength smaller than the number of provided elements', + ); + }); + + it('should start from 0 if startingIndex is not provided', () => { + const result = encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: ['0x983abc616f2442bab7a917e6bb8660df8b01f3bf'], + arrayLength: 1, + }, + ], + schemas, + ); + + const expectedResult = { + keys: [ + '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000000', + ], + values: [ + '0x00000000000000000000000000000001', + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + ], + }; + + assert.deepStrictEqual( + result, + expectedResult, + 'Should encode starting from index 0 if startingIndex is not provided', + ); + }); + + it('should use the number of elements in value field if arrayLength is not provided', () => { + const result = encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + // Not specifying arrayLength, it should default to the number of elements in the value array + startingIndex: 0, + }, + ], + schemas, + ); + + const expectedResult = { + keys: [ + '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000000', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000001', + ], + values: [ + '0x00000000000000000000000000000002', + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + }; + + assert.deepStrictEqual( + result, + expectedResult, + 'should use the number of elements in value field if arrayLength is not provided', + ); + }); + }); + describe('isDataAuthentic', () => { it('returns true if data is authentic', () => { const data = 'h3ll0HowAreYou?'; From 329d886f77d2fb98ef15a788778437f8d3852909 Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Wed, 20 Mar 2024 18:43:41 +0100 Subject: [PATCH 3/9] fix typos --- src/lib/encoder.test.ts | 10 +++++----- src/lib/utils.test.ts | 4 ++-- src/lib/utils.ts | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/lib/encoder.test.ts b/src/lib/encoder.test.ts index 07e40637..ed170fca 100644 --- a/src/lib/encoder.test.ts +++ b/src/lib/encoder.test.ts @@ -633,7 +633,7 @@ describe('encoder', () => { }); }); - describe('when encoding a value that exceeds the maximal lenght of bytes than its type', () => { + describe('when encoding a value that exceeds the maximal length of bytes than its type', () => { const validTestCases = [ { valueType: 'bytes32', @@ -796,13 +796,13 @@ describe('encoder', () => { }); describe('when encoding uintN[CompactBytesArray]', () => { - it('should throw if trying to encode a value that exceeds the maximal lenght of bytes for this type', async () => { + it('should throw if trying to encode a value that exceeds the maximal length of bytes for this type', async () => { expect(() => { encodeValueType('uint8[CompactBytesArray]', [15, 178, 266]); }).to.throw('Hex uint8 value at index 2 does not fit in 1 bytes'); }); - it('should throw if trying to decode a value that exceeds the maximal lenght of bytes for this type', async () => { + it('should throw if trying to decode a value that exceeds the maximal length of bytes for this type', async () => { expect(() => { decodeValueType( 'uint8[CompactBytesArray]', @@ -813,7 +813,7 @@ describe('encoder', () => { }); describe('when encoding bytesN[CompactBytesArray]', () => { - it('should throw if trying to encode a value that exceeds the maximal lenght of bytes for this type', async () => { + it('should throw if trying to encode a value that exceeds the maximal length of bytes for this type', async () => { expect(() => { encodeValueType('bytes4[CompactBytesArray]', [ '0xe6520726', @@ -824,7 +824,7 @@ describe('encoder', () => { }).to.throw('Hex bytes4 value at index 3 does not fit in 4 bytes'); }); - it('should throw if trying to decode a value that exceeds the maximal lenght of bytes for this type', async () => { + it('should throw if trying to decode a value that exceeds the maximal length of bytes for this type', async () => { expect(() => { decodeValueType( 'bytes4[CompactBytesArray]', diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 396102ef..c6184eca 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -791,7 +791,7 @@ describe('utils', () => { }); it('should throw if arrayLength is smaller than elements in provided value array', () => { - const encodeDataWithLowerArrayLenght = () => { + const encodeDataWithLowerArrayLength = () => { encodeData( [ { @@ -809,7 +809,7 @@ describe('utils', () => { }; assert.throws( - encodeDataWithLowerArrayLenght, + encodeDataWithLowerArrayLength, /Invalid arrayLength/, 'Should throw an error for arrayLength smaller than the number of provided elements', ); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6f2a8fe2..c6ddc33e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -262,13 +262,12 @@ export function encodeKey( return null; } - // Check if an starting index and array lenght are correct types if ( typeof startingIndex !== 'number' || typeof arrayLength !== 'number' ) { throw new Error( - 'Invalid startingIndex or arrayLength parameters. Values must be of type number', + 'Invalid startingIndex or arrayLength parameters. Values must be of type number.', ); } From c46f44f8164ee781c7d426b394d2ff44dbb19beb Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Wed, 20 Mar 2024 18:44:10 +0100 Subject: [PATCH 4/9] add parameter passdown test for startingIndex and arrayLength --- src/index.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 561480b2..ac613cc3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1004,6 +1004,45 @@ describe('Running @erc725/erc725.js tests...', () => { assert.deepStrictEqual(results, intendedResult); }); + it('encodes dynamic data values for keyType "Array" in naked class instance', () => { + const schemas: ERC725JSONSchema[] = [ + { + name: 'AddressPermissions[]', + key: '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + keyType: 'Array', + valueType: 'address', + valueContent: 'Address', + }, + ]; + const erc725 = new ERC725(schemas); + const encodedArraySection = erc725.encodeData([ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + arrayLength: 23, + startingIndex: 21, + }, + ]); + + // Expected result with custom startingIndex and arrayLength + const expectedResult = { + keys: [ + '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000015', // 21 + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000016', // 22 + ], + values: [ + '0x00000000000000000000000000000017', // 23 + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + }; + assert.deepStrictEqual(encodedArraySection, expectedResult); + }); + it(`decode all data values for keyType "Array" in naked class instance: ${schemaElement.name}`, async () => { const values = allGraphData.filter( (e) => e.key.slice(0, 34) === schemaElement.key.slice(0, 34), From ec79a4a8cb58df4fb518208ba2450413f1dd4f9e Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Wed, 20 Mar 2024 18:47:03 +0100 Subject: [PATCH 5/9] update documentation --- docs/classes/ERC725.md | 48 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index d26b5c78..a8b85885 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -449,6 +449,8 @@ An array of objects containing the following properties: | `keyName` | string | Can be either the named key (i.e. `LSP3Profile`, `LSP12IssuedAssetsMap:
`) or the hashed key (with or without `0x` prefix, i.e. `0x5ef...` or `5ef...`). | | `dynamicKeyParts` (optional) | string or
string[ ] | The dynamic parts of the `keyName` that will be used for encoding the key. | | `value` | string or
string[ ]
JSON todo | The value that should be encoded. Can be a string, an array of string or a JSON... | +| `startingIndex` (optional) | number | Starting index for `Array` types to encode a subset of elements. Defaults t `0`. | +| `arrayLength` (optional) | number | Parameter for `Array` types, specifying the total length when encoding a subset of elements. Defaults to the number of elements in the `value` field. | The `keyName` also supports dynamic keys for [`Mapping`](https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#mapping) and [`MappingWithGrouping`](https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#mapping). Therefore, you can use variables in the key name such as `LSP12IssuedAssetsMap:
`. In that case, the value should also set the `dynamicKeyParts` property: @@ -700,6 +702,50 @@ myErc725.encodeData([ +
+ Encode a subset of array elements + +```javascript title="Encode a subset of array elements" +const schemas = [ + { + name: 'AddressPermissions[]', + key: '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + keyType: 'Array', + valueType: 'address', + valueContent: 'Address', + }, +]; + +const myErc725 = new ERC725(schemas); +myErc725.encodeData([ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + arrayLength: 23, + startingIndex: 21, + }, +]); +/** +{ + keys: [ + '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000015', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000016', + ], + values: [ + '0x00000000000000000000000000000017', + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], +} +*/ +``` + +
+ --- ## encodePermissions @@ -1611,7 +1657,7 @@ Either a string of the hexadecimal `interfaceID` as defined by [ERC165](https:// The `interfaceName` will only check for the latest version of the standard's `interfaceID`, which can be found in `src/constants/interfaces`. For LSPs, the `interfaceIDs` are taken from the latest release of the [@lukso/lsp-smart-contracts](https://github.com/lukso-network/lsp-smart-contracts) library. -:::info +::: ##### 2. `options` - Object (optional) From 0fea6428318369b58f3b506e238a3d1e16744649 Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Thu, 21 Mar 2024 16:34:41 +0100 Subject: [PATCH 6/9] update example + rename arrayLength to totalArrayLength --- docs/classes/ERC725.md | 40 +++++++++++++++++++++------------------- src/index.test.ts | 4 ++-- src/lib/utils.test.ts | 26 +++++++++++++------------- src/lib/utils.ts | 20 ++++++++++---------- src/types/decodeData.ts | 2 +- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index a8b85885..68dff2be 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -444,13 +444,13 @@ _Example 2: input `1122334455` encoded as `bytes4` --> will encode as `0x42e576f An array of objects containing the following properties: -| Name | Type | Description | -| :--------------------------- | :--------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `keyName` | string | Can be either the named key (i.e. `LSP3Profile`, `LSP12IssuedAssetsMap:
`) or the hashed key (with or without `0x` prefix, i.e. `0x5ef...` or `5ef...`). | -| `dynamicKeyParts` (optional) | string or
string[ ] | The dynamic parts of the `keyName` that will be used for encoding the key. | -| `value` | string or
string[ ]
JSON todo | The value that should be encoded. Can be a string, an array of string or a JSON... | -| `startingIndex` (optional) | number | Starting index for `Array` types to encode a subset of elements. Defaults t `0`. | -| `arrayLength` (optional) | number | Parameter for `Array` types, specifying the total length when encoding a subset of elements. Defaults to the number of elements in the `value` field. | +| Name | Type | Description | +| :---------------------------- | :--------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `keyName` | string | Can be either the named key (i.e. `LSP3Profile`, `LSP12IssuedAssetsMap:
`) or the hashed key (with or without `0x` prefix, i.e. `0x5ef...` or `5ef...`). | +| `dynamicKeyParts` (optional) | string or
string[ ] | The dynamic parts of the `keyName` that will be used for encoding the key. | +| `value` | string or
string[ ]
JSON todo | The value that should be encoded. Can be a string, an array of string or a JSON... | +| `startingIndex` (optional) | number | Starting index for `Array` types to encode a subset of elements. Defaults t `0`. | +| `totalArrayLength` (optional) | number | Parameter for `Array` types, specifying the total length when encoding a subset of elements. Defaults to the number of elements in the `value` field. | The `keyName` also supports dynamic keys for [`Mapping`](https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#mapping) and [`MappingWithGrouping`](https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#mapping). Therefore, you can use variables in the key name such as `LSP12IssuedAssetsMap:
`. In that case, the value should also set the `dynamicKeyParts` property: @@ -716,18 +716,20 @@ const schemas = [ }, ]; -const myErc725 = new ERC725(schemas); -myErc725.encodeData([ - { - keyName: 'AddressPermissions[]', - value: [ - '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', - '0x56ecbc104136d00eb37aa0dce60e075f10292d81', - ], - arrayLength: 23, - startingIndex: 21, - }, -]); +myErc725.encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + totalArrayLength: 23, + startingIndex: 21, + }, + ], + schemas, +); /** { keys: [ diff --git a/src/index.test.ts b/src/index.test.ts index ac613cc3..de108e06 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1022,12 +1022,12 @@ describe('Running @erc725/erc725.js tests...', () => { '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', '0x56ecbc104136d00eb37aa0dce60e075f10292d81', ], - arrayLength: 23, + totalArrayLength: 23, startingIndex: 21, }, ]); - // Expected result with custom startingIndex and arrayLength + // Expected result with custom startingIndex and totalArrayLength const expectedResult = { keys: [ '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index c6184eca..d6d9ae75 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -740,14 +740,14 @@ describe('utils', () => { '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', '0x56ecbc104136d00eb37aa0dce60e075f10292d81', ], - arrayLength: 23, + totalArrayLength: 23, startingIndex: 21, }, ], schemas, ); - // Expected result with custom startingIndex and arrayLength + // Expected result with custom startingIndex and totalArrayLength const expectedResult = { keys: [ '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', @@ -775,7 +775,7 @@ describe('utils', () => { { keyName: 'AddressPermissions[]', value: ['0x983abc616f2442bab7a917e6bb8660df8b01f3bf'], - arrayLength: 1, + totalArrayLength: 1, startingIndex: -1, }, ], @@ -790,8 +790,8 @@ describe('utils', () => { ); }); - it('should throw if arrayLength is smaller than elements in provided value array', () => { - const encodeDataWithLowerArrayLength = () => { + it('should throw if totalArrayLength is smaller than elements in provided value array', () => { + const encodeDataWithLowerTotalArrayLength = () => { encodeData( [ { @@ -800,7 +800,7 @@ describe('utils', () => { '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', '0x56ecbc104136d00eb37aa0dce60e075f10292d81', ], - arrayLength: 1, // 2 elements + totalArrayLength: 1, // 2 elements startingIndex: 0, }, ], @@ -809,9 +809,9 @@ describe('utils', () => { }; assert.throws( - encodeDataWithLowerArrayLength, - /Invalid arrayLength/, - 'Should throw an error for arrayLength smaller than the number of provided elements', + encodeDataWithLowerTotalArrayLength, + /Invalid totalArrayLength/, + 'Should throw an error for totalArrayLength smaller than the number of provided elements', ); }); @@ -821,7 +821,7 @@ describe('utils', () => { { keyName: 'AddressPermissions[]', value: ['0x983abc616f2442bab7a917e6bb8660df8b01f3bf'], - arrayLength: 1, + totalArrayLength: 1, }, ], schemas, @@ -845,7 +845,7 @@ describe('utils', () => { ); }); - it('should use the number of elements in value field if arrayLength is not provided', () => { + it('should use the number of elements in value field if totalArrayLength is not provided', () => { const result = encodeData( [ { @@ -854,7 +854,7 @@ describe('utils', () => { '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', '0x56ecbc104136d00eb37aa0dce60e075f10292d81', ], - // Not specifying arrayLength, it should default to the number of elements in the value array + // Not specifying totalArrayLength, it should default to the number of elements in the value array startingIndex: 0, }, ], @@ -877,7 +877,7 @@ describe('utils', () => { assert.deepStrictEqual( result, expectedResult, - 'should use the number of elements in value field if arrayLength is not provided', + 'should use the number of elements in value field if totalArrayLength is not provided', ); }); }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c6ddc33e..d7bbb4a7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -244,7 +244,7 @@ export function encodeKey( | URLDataToEncode[] | boolean, startingIndex = 0, - arrayLength = Array.isArray(value) ? value.length : 0, + totalArrayLength = Array.isArray(value) ? value.length : 0, ) { // NOTE: This will not guarantee order of array as on chain. Assumes developer must set correct order @@ -264,10 +264,10 @@ export function encodeKey( if ( typeof startingIndex !== 'number' || - typeof arrayLength !== 'number' + typeof totalArrayLength !== 'number' ) { throw new Error( - 'Invalid startingIndex or arrayLength parameters. Values must be of type number.', + 'Invalid startingIndex or totalArrayLength parameters. Values must be of type number.', ); } @@ -277,9 +277,9 @@ export function encodeKey( ); } - if (arrayLength < value.length) { + if (totalArrayLength < value.length) { throw new Error( - 'Invalid arrayLength parameter. Array length must be at least as large as the number of elements of the value array.', + 'Invalid totalArrayLength parameter. Array length must be at least as large as the number of elements of the value array.', ); } @@ -287,12 +287,12 @@ export function encodeKey( for (let index = 0; index < value.length; index++) { if (index === 0) { - // This is arrayLength as the first element in the raw array + // This is totalArrayLength as the first element in the raw array // encoded as uint128 results.push({ key: schema.key, // Encode the explicitly provided or default array length - value: encodeValueType('uint128', arrayLength), + value: encodeValueType('uint128', totalArrayLength), }); } @@ -467,7 +467,7 @@ export function encodeData( return dataAsArray.reduce( ( accumulator, - { keyName, value, dynamicKeyParts, startingIndex, arrayLength }, + { keyName, value, dynamicKeyParts, startingIndex, totalArrayLength }, ) => { let schemaElement: ERC725JSONSchema | null = null; let encodedValue; // would be nice to type this @@ -486,7 +486,7 @@ export function encodeData( schemaElement, value, startingIndex, - arrayLength, + totalArrayLength, ); } else { schemaElement = getSchemaElement(schema, keyName); @@ -494,7 +494,7 @@ export function encodeData( schemaElement, value as any, startingIndex, - arrayLength, + totalArrayLength, ); } diff --git a/src/types/decodeData.ts b/src/types/decodeData.ts index b7d62c33..7e3179f3 100644 --- a/src/types/decodeData.ts +++ b/src/types/decodeData.ts @@ -4,7 +4,7 @@ export interface DataInput { keyName: string; // can be the name or the hex/hash value; dynamicKeyParts?: string | string[]; - arrayLength?: number; + totalArrayLength?: number; startingIndex?: number; } From 740e5a1f41a53d5c65e5a0d562876febebb40392 Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Thu, 21 Mar 2024 17:07:29 +0100 Subject: [PATCH 7/9] add further parameter explanation and warning --- docs/classes/ERC725.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index 68dff2be..b2194955 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -456,6 +456,24 @@ The `keyName` also supports dynamic keys for [`Mapping`](https://github.com/luks - `dynamicKeyParts`: string or string[ ] which holds the variables that needs to be encoded. +:::info Handling array subsets + +The `totalArrayLength` parameter must be explicitly provided to ensure integrity when encoding subsets or modifying existing array elements. Its value specifies the total length of the array **after the operation is completed**, not just the size of the encoded subset. + +**When to Use `totalArrayLength`** + +- **Adding Elements:** When adding new elements to an array, `totalArrayLength` should equal the sum of the current array's length plus the number of new elements added. +- **Modifying Elements:** If modifying elements within an existing array without changing the total number of elements, `totalArrayLength` should match the previous length of the array. +- **Removing Elements:** In cases where elements are removed, `totalArrayLength` should reflect the number of elements left. + +::: + +:::caution Encoding array lengths + +Please be careful when updating existing contract data. Incorrect usage of `startingIndex` and `totalArrayLength` can lead to improperly encoded data that changes the intended structure of the data field. + +::: + ##### 2. `schemas` - Array of Objects (optional) An array of extra [LSP-2 ERC725YJSONSchema] objects that can be used to find the schema. If called on an instance, it is optional and it will be concatenated with the schema provided on instantiation. From 72705935ef017937087fab20001bbbafc513b9ab Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Thu, 21 Mar 2024 18:45:21 +0100 Subject: [PATCH 8/9] improve test name and error outputs --- src/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.test.ts b/src/index.test.ts index de108e06..db99edaf 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1004,7 +1004,7 @@ describe('Running @erc725/erc725.js tests...', () => { assert.deepStrictEqual(results, intendedResult); }); - it('encodes dynamic data values for keyType "Array" in naked class instance', () => { + it('encodes subset of elements for keyType "Array" in naked class instance', () => { const schemas: ERC725JSONSchema[] = [ { name: 'AddressPermissions[]', From b1911cada6fb6d64fa413200da40957d1c51e474 Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt Date: Fri, 22 Mar 2024 11:21:45 +0100 Subject: [PATCH 9/9] update error messages with code styling --- src/lib/utils.test.ts | 4 ++-- src/lib/utils.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index d6d9ae75..27a75b2a 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -785,7 +785,7 @@ describe('utils', () => { assert.throws( encodeDataWithNegativeStartingIndex, - /Invalid startingIndex/, + /Invalid `startingIndex`/, 'Should throw an error for negative startingIndex', ); }); @@ -810,7 +810,7 @@ describe('utils', () => { assert.throws( encodeDataWithLowerTotalArrayLength, - /Invalid totalArrayLength/, + /Invalid `totalArrayLength`/, 'Should throw an error for totalArrayLength smaller than the number of provided elements', ); }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d7bbb4a7..a447f0b0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -267,19 +267,19 @@ export function encodeKey( typeof totalArrayLength !== 'number' ) { throw new Error( - 'Invalid startingIndex or totalArrayLength parameters. Values must be of type number.', + 'Invalid `startingIndex` or `totalArrayLength` parameters. Values must be of type number.', ); } if (startingIndex < 0) { throw new Error( - 'Invalid startingIndex parameter. Value cannot be negative.', + 'Invalid `startingIndex` parameter. Value cannot be negative.', ); } if (totalArrayLength < value.length) { throw new Error( - 'Invalid totalArrayLength parameter. Array length must be at least as large as the number of elements of the value array.', + 'Invalid `totalArrayLength` parameter. Array length must be at least as large as the number of elements of the value array.', ); }