Skip to content

Commit

Permalink
Merge pull request #394 from fhildeb/feat/dynamic-array-encoding
Browse files Browse the repository at this point in the history
feat: add dynamic array encoding for encodeData
  • Loading branch information
fhildeb authored Mar 22, 2024
2 parents 8a78336 + b1911ca commit 5fca5be
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 18 deletions.
78 changes: 72 additions & 6 deletions docs/classes/ERC725.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<address>`) or the hashed key (with or without `0x` prefix, i.e. `0x5ef...` or `5ef...`). |
| `dynamicKeyParts` (optional) | string or <br/> string[&nbsp;] | The dynamic parts of the `keyName` that will be used for encoding the key. |
| `value` | string or <br/> string[&nbsp;] <br/> 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:<address>`) or the hashed key (with or without `0x` prefix, i.e. `0x5ef...` or `5ef...`). |
| `dynamicKeyParts` (optional) | string or <br/> string[&nbsp;] | The dynamic parts of the `keyName` that will be used for encoding the key. |
| `value` | string or <br/> string[&nbsp;] <br/> 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:<address>`. In that case, the value should also set the `dynamicKeyParts` property:
- `dynamicKeyParts`: string or string[&nbsp;] 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.
Expand Down Expand Up @@ -700,6 +720,52 @@ myErc725.encodeData([
</details>
<details>
<summary>Encode a subset of array elements</summary>
```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',
],
}
*/
```
</details>
---
## encodePermissions
Expand Down Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
10 changes: 5 additions & 5 deletions src/lib/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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]',
Expand All @@ -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',
Expand All @@ -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]',
Expand Down
162 changes: 162 additions & 0 deletions src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?';
Expand Down
Loading

0 comments on commit 5fca5be

Please sign in to comment.