diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index d26b5c78..b2194955 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -444,16 +444,36 @@ _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... | +| 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: - `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. @@ -700,6 +720,52 @@ 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', + }, +]; + +myErc725.encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + totalArrayLength: 23, + startingIndex: 21, + }, + ], + schemas, +); +/** +{ + keys: [ + '0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000015', + '0xdf30dba06db6a30e65354d9a64c6098600000000000000000000000000000016', + ], + values: [ + '0x00000000000000000000000000000017', + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], +} +*/ +``` + +
+ --- ## encodePermissions @@ -1611,7 +1677,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) diff --git a/src/index.test.ts b/src/index.test.ts index 561480b2..db99edaf 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 subset of elements 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', + ], + totalArrayLength: 23, + startingIndex: 21, + }, + ]); + + // Expected result with custom startingIndex and totalArrayLength + 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), 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 8a8ee3f2..27a75b2a 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', + ], + totalArrayLength: 23, + startingIndex: 21, + }, + ], + schemas, + ); + + // Expected result with custom startingIndex and totalArrayLength + 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'], + totalArrayLength: 1, + startingIndex: -1, + }, + ], + schemas, + ); + }; + + assert.throws( + encodeDataWithNegativeStartingIndex, + /Invalid `startingIndex`/, + 'Should throw an error for negative startingIndex', + ); + }); + + it('should throw if totalArrayLength is smaller than elements in provided value array', () => { + const encodeDataWithLowerTotalArrayLength = () => { + encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + totalArrayLength: 1, // 2 elements + startingIndex: 0, + }, + ], + schemas, + ); + }; + + assert.throws( + encodeDataWithLowerTotalArrayLength, + /Invalid `totalArrayLength`/, + 'Should throw an error for totalArrayLength smaller than the number of provided elements', + ); + }); + + it('should start from 0 if startingIndex is not provided', () => { + const result = encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: ['0x983abc616f2442bab7a917e6bb8660df8b01f3bf'], + totalArrayLength: 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 totalArrayLength is not provided', () => { + const result = encodeData( + [ + { + keyName: 'AddressPermissions[]', + value: [ + '0x983abc616f2442bab7a917e6bb8660df8b01f3bf', + '0x56ecbc104136d00eb37aa0dce60e075f10292d81', + ], + // Not specifying totalArrayLength, 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 totalArrayLength is not provided', + ); + }); + }); + describe('isDataAuthentic', () => { it('returns true if data is authentic', () => { const data = 'h3ll0HowAreYou?'; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 132a793f..a447f0b0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -243,6 +243,8 @@ export function encodeKey( | URLDataToEncode | URLDataToEncode[] | boolean, + startingIndex = 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 @@ -260,21 +262,45 @@ export function encodeKey( return null; } + if ( + typeof startingIndex !== 'number' || + typeof totalArrayLength !== 'number' + ) { + throw new Error( + 'Invalid `startingIndex` or `totalArrayLength` parameters. Values must be of type number.', + ); + } + + if (startingIndex < 0) { + throw new Error( + '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.', + ); + } + 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 + // This is totalArrayLength 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', totalArrayLength), }); } + 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 +465,10 @@ export function encodeData( const dataAsArray = Array.isArray(data) ? data : [data]; return dataAsArray.reduce( - (accumulator, { keyName, value, dynamicKeyParts }) => { + ( + accumulator, + { keyName, value, dynamicKeyParts, startingIndex, totalArrayLength }, + ) => { let schemaElement: ERC725JSONSchema | null = null; let encodedValue; // would be nice to type this @@ -453,10 +482,20 @@ export function encodeData( } schemaElement = getSchemaElement(schema, keyName, dynamicKeyParts); - encodedValue = encodeKey(schemaElement, value); + encodedValue = encodeKey( + schemaElement, + value, + startingIndex, + totalArrayLength, + ); } else { schemaElement = getSchemaElement(schema, keyName); - encodedValue = encodeKey(schemaElement, value as any); + encodedValue = encodeKey( + schemaElement, + value as any, + startingIndex, + totalArrayLength, + ); } if (typeof encodedValue === 'string') { diff --git a/src/types/decodeData.ts b/src/types/decodeData.ts index b0e32342..7e3179f3 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[]; + totalArrayLength?: number; + startingIndex?: number; } export interface EncodeDataInput extends DataInput {