Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dynamic array encoding for encodeData #394

Merged
merged 9 commits into from
Mar 22, 2024
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
Loading