From a6fe7c8470688f573426b59fc2023a08da0cbd36 Mon Sep 17 00:00:00 2001 From: Jean Cvllr <31145285+CJ42@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:03:31 +0000 Subject: [PATCH 1/2] feat: add `encode/decodeValueType` as public callable methods (#325) * feat: add `encode/decodeValueType` as public callable methods * docs: improve parameter names + add examples in documentation --- docs/classes/ERC725.md | 289 +++++++++++++++++++++++++++++------------ src/index.ts | 62 +++++++-- src/lib/encoder.ts | 10 +- 3 files changed, 262 insertions(+), 99 deletions(-) diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index d50f7c76..269870ce 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -342,6 +342,67 @@ myErc725.decodePermissions('0x00000000000000000000000000000000000000000000000000 --- +## decodeValueType + +```js +myErc725.decodeValueType(type, data); +``` + +```js +ERC725.decodeValueType(type, data); +``` + +Decode some data according to a provided value type. + +#### Parameters + +| Name | Type | Description | +| :----- | :----- | :---------------------------------------------------------------------------- | +| `type` | string | The value type to decode the data (i.e. `uint256`, `bool`, `bytes4`, etc...). | +| `data` | string | A hex encoded string starting with `0x` to decode | + +#### Returns + +| Name | Type | Description | +| :------------- | :--------------------- | :----------------------------------- | +| `decodedValue` | string or
number | A value decoded according to `type`. | + +#### Examples + +```javascript +myErc725.decodeValueType('uint128', '0x0000000000000000000000000000000a'); +// 10 + +myErc725.decodeValueType('bool', '0x01'); +// true + +myErc725.decodeValueType('string', '0x48656c6c6f21'); +// 'Hello!'; + +// also available for ABI encoded array + CompactBytesArray +myErc725.decodeValueType( + 'uint256[]', + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e', +); +// [ 10, 20, 30 ] + +myErc725.decodeValueType( + 'uint256[CompactBytesArray]'', + '0x0020000000000000000000000000000000000000000000000000000000000000000500200000000000000000000000000000000000000000000000000000000000000008' +) +// [ 5, 8 ] +``` + +This method is also available as a static method: + +```js +ERC725.decodeValueType( + 'uint256', + '0x000000000000000000000000000000000000000000000000000000000000002a', +); +// 42 +``` + ## encodeData ```js @@ -617,6 +678,154 @@ myErc725.encodeData([ --- +## encodePermissions + +```js +ERC725.encodePermissions(permissions); +``` + +Encodes permissions into a hexadecimal string as defined by the [LSP6 KeyManager Standard](https://docs.lukso.tech/standards/universal-profile/lsp6-key-manager). + +:::info + +`encodePermissions` is available as either a static or non-static method so can be called without instantiating an ERC725 object. + +::: + +#### Parameters + +##### 1. `permissions` - Object + +An object with [LSP6 KeyManager Permissions] as keys and a `boolean` as value. Any ommited permissions will default to `false`. + +#### Returns + +| Type | Description | +| :----- | :---------------------------------------------------------------------------------------- | +| string | The permissions encoded as a hexadecimal string defined by the [LSP6 KeyManager Standard] | + +#### Example + +```javascript title="Encoding permissions" +ERC725.encodePermissions({ + CHANGEOWNER: false, + ADDCONTROLLER: false, + EDITPERMISSIONS: false, + ADDEXTENSIONS: false, + CHANGEEXTENSIONS: true, + ADDUNIVERSALRECEIVERDELEGATE: false, + CHANGEUNIVERSALRECEIVERDELEGATE: false, + REENTRANCY: false, + SUPER_TRANSFERVALUE: true, + TRANSFERVALUE: true, + SUPER_CALL: false, + CALL: true, + SUPER_STATICCALL: false, + STATICCALL: false, + SUPER_DELEGATECALL: false, + DELEGATECALL: false, + DEPLOY: false, + SUPER_SETDATA: false, + SETDATA: false, + ENCRYPT: false, + DECRYPT: false, + SIGN: false, + EXECUTE_RELAY_CALL: false +}), +// '0x0000000000000000000000000000000000000000000000000000000000000110' + +// Any ommited Permissions will default to false +ERC725.encodePermissions({ + ADDCONTROLLER: true, + ADDEXTENSIONS: true, +}), +// '0x000000000000000000000000000000000000000000000000000000000000000a' +ERC725.encodePermissions({ + EDITPERMISSIONS: true, + CHANGEEXTENSIONS: true, + CHANGEUNIVERSALRECEIVERDELEGATE: true, + SETDATA: true, +}), +// '0x0000000000000000000000000000000000000000000000000000000000040054' + + +// This method is also available on the instance: +myErc725.encodePermissions({ + EDITPERMISSIONS: true, + SETDATA: true, +}), +``` + +--- + +## encodeValueType + +```js +myErc725.encodeValueType(type, value); +``` + +```js +ERC725.encodeValueType(type, value); +``` + +#### Parameters + +| Name | Type | Description | +| :------ | :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | +| `type` | string | The value type to encode the value (i.e. `uint256`, `bool`, `bytes4`, etc...). | +| `value` | string or
string[ ] or
number or
number[ ] or
boolean or
boolean[] | The value that should be encoded as `type` | + +#### Returns + +| Name | Type | Description | +| :----------------- | :----- | :------------------------------------------------------- | +| `encodedValueType` | string | A hex string representing the `value` encoded as `type`. | + +After the `value` is encoded, the hex string can be used to be stored inside the ERC725Y smart contract. + +#### Examples + +```javascript +myErc725.encodeValueType('uint256', 5); +// '0x0000000000000000000000000000000000000000000000000000000000000005' + +myErc725.encodeValueType('bool', true); +// '0x01' + +// the word `boolean` (Name of the Typescript type) is also available +myErc725.encodeValueType('boolean', true); +// '0x01' + +// `bytesN` type will pad on the right if the value contains less than N bytes +myErc725.encodeValueType('bytes4', '0xcafe'); +// '0xcafe0000' +myErc725.encodeValueType('bytes32', '0xcafe'); +// '0xcafe000000000000000000000000000000000000000000000000000000000000' + +// `bytesN` type will throw an error if the value contains more than N bytes +myERC725.encodeValueType('bytes4', '0xcafecafebeef'); +// Error: Can't convert 0xcafecafebeef to bytes4. Too many bytes, expected at most 4 bytes, received 6. + +// Can also be used to encode arrays as `CompactBytesArray` +myERC725.encodeValueType('uint256[CompactBytesArray]', [1, 2, 3]); +// '0x002000000000000000000000000000000000000000000000000000000000000000010020000000000000000000000000000000000000000000000000000000000000000200200000000000000000000000000000000000000000000000000000000000000003' + +myERC725.encodeValueType('bytes[CompactBytesArray]', [ + '0xaaaaaaaa', + '0xbbbbbbbbbbbbbbbbbb', +]); +// '0x0004aaaaaaaa0009bbbbbbbbbbbbbbbbbb' +``` + +This method is also available as a static method. + +```javascript +ERC725.encodeValueType('string', 'Hello'); +// '0x48656c6c6f' +``` + +--- + ## encodeKeyName ```js @@ -766,86 +975,6 @@ myErc725.decodeMappingKey( --- -## encodePermissions - -```js -ERC725.encodePermissions(permissions); -``` - -Encodes permissions into a hexadecimal string as defined by the [LSP6 KeyManager Standard](https://docs.lukso.tech/standards/universal-profile/lsp6-key-manager). - -:::info - -`encodePermissions` is available as either a static or non-static method so can be called without instantiating an ERC725 object. - -::: - -#### Parameters - -##### 1. `permissions` - Object - -An object with [LSP6 KeyManager Permissions] as keys and a `boolean` as value. Any ommited permissions will default to `false`. - -#### Returns - -| Type | Description | -| :----- | :---------------------------------------------------------------------------------------- | -| string | The permissions encoded as a hexadecimal string defined by the [LSP6 KeyManager Standard] | - -#### Example - -```javascript title="Encoding permissions" -ERC725.encodePermissions({ - CHANGEOWNER: false, - ADDCONTROLLER: false, - EDITPERMISSIONS: false, - ADDEXTENSIONS: false, - CHANGEEXTENSIONS: true, - ADDUNIVERSALRECEIVERDELEGATE: false, - CHANGEUNIVERSALRECEIVERDELEGATE: false, - REENTRANCY: false, - SUPER_TRANSFERVALUE: true, - TRANSFERVALUE: true, - SUPER_CALL: false, - CALL: true, - SUPER_STATICCALL: false, - STATICCALL: false, - SUPER_DELEGATECALL: false, - DELEGATECALL: false, - DEPLOY: false, - SUPER_SETDATA: false, - SETDATA: false, - ENCRYPT: false, - DECRYPT: false, - SIGN: false, - EXECUTE_RELAY_CALL: false -}), -// '0x0000000000000000000000000000000000000000000000000000000000000110' - -// Any ommited Permissions will default to false -ERC725.encodePermissions({ - ADDCONTROLLER: true, - ADDEXTENSIONS: true, -}), -// '0x000000000000000000000000000000000000000000000000000000000000000a' -ERC725.encodePermissions({ - EDITPERMISSIONS: true, - CHANGEEXTENSIONS: true, - CHANGEUNIVERSALRECEIVERDELEGATE: true, - SETDATA: true, -}), -// '0x0000000000000000000000000000000000000000000000000000000000040054' - - -// This method is also available on the instance: -myErc725.encodePermissions({ - EDITPERMISSIONS: true, - SETDATA: true, -}), -``` - ---- - ## fetchData ```js diff --git a/src/index.ts b/src/index.ts index 6cf27f14..957d5c50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,6 +62,7 @@ import { getDataFromExternalSources } from './lib/getDataFromExternalSources'; import { DynamicKeyPart, DynamicKeyParts } from './types/dynamicKeys'; import { getData } from './lib/getData'; import { checkPermissions } from './lib/detector'; +import { decodeValueType, encodeValueType } from './lib/encoder'; import { decodeMappingKey } from './lib/decodeMappingKey'; export { @@ -174,6 +175,21 @@ export class ERC725 { throw new Error(`Incorrect or unsupported provider ${providerOrRpcUrl}`); } + + private getAddressAndProvider() { + if (!this.options.address || !isAddress(this.options.address)) { + throw new Error('Missing ERC725 contract address.'); + } + if (!this.options.provider) { + throw new Error('Missing provider.'); + } + + return { + address: this.options.address, + provider: this.options.provider, + }; + } + /** * Gets **decoded data** for one, many or all keys of the specified `ERC725` smart-contract. * When omitting the `keyOrKeys` parameter, it will get all the keys (as per {@link ERC725JSONSchema | ERC725JSONSchema} definition). @@ -410,20 +426,6 @@ export class ERC725 { ); } - private getAddressAndProvider() { - if (!this.options.address || !isAddress(this.options.address)) { - throw new Error('Missing ERC725 contract address.'); - } - if (!this.options.provider) { - throw new Error('Missing provider.'); - } - - return { - address: this.options.address, - provider: this.options.provider, - }; - } - /** * Encode permissions into a hexadecimal string as defined by the LSP6 KeyManager Standard. * @@ -607,6 +609,38 @@ export class ERC725 { ): boolean { return ERC725.checkPermissions(requiredPermissions, grantedPermissions); } + + /** + * @param type The valueType to encode the value as + * @param value The value to encode + * @returns The encoded value + */ + static encodeValueType( + type: string, + value: string | string[] | number | number[] | boolean | boolean[], + ): string { + return encodeValueType(type, value); + } + + encodeValueType( + type: string, + value: string | string[] | number | number[] | boolean | boolean[], + ): string { + return ERC725.encodeValueType(type, value); + } + + /** + * @param type The valueType to decode the value as + * @param data The data to decode + * @returns The decoded value + */ + static decodeValueType(type: string, data: string) { + return decodeValueType(type, data); + } + + decodeValueType(type: string, data: string) { + return ERC725.decodeValueType(type, data); + } } export default ERC725; diff --git a/src/lib/encoder.ts b/src/lib/encoder.ts index 6ff989b3..42f93890 100644 --- a/src/lib/encoder.ts +++ b/src/lib/encoder.ts @@ -740,18 +740,18 @@ export function encodeValueType( return valueTypeEncodingMap[type].encode(value); } -export function decodeValueType(type: string, value: string) { +export function decodeValueType(type: string, data: string) { if (!valueTypeEncodingMap[type]) { throw new Error('Could not decode valueType: "' + type + '".'); } - if (value === '0x') return null; + if (data === '0x') return null; - if (typeof value === 'undefined' || value === null) { - return value; + if (typeof data === 'undefined' || data === null) { + return data; } - return valueTypeEncodingMap[type].decode(value); + return valueTypeEncodingMap[type].decode(data); } export function encodeValueContent( From 3a6be551d889904b7d95e2630ab637f2a31feb50 Mon Sep 17 00:00:00 2001 From: Jean Cvllr <31145285+CJ42@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:04:09 +0000 Subject: [PATCH 2/2] feat: allow to encode LSP2 Array length only (#326) * feat(wip): allow to encode LSP2 Array length only * refactor: add typing `number` for `EncodeDataType` * docs: add docs for encode array length --------- Co-authored-by: Hugo Masclet --- docs/classes/ERC725.md | 24 +++++++++++++ src/index.ts | 2 +- src/lib/encoder.ts | 37 ++++++------------- src/lib/utils.test.ts | 63 ++++++++++++++++++++++++++------- src/lib/utils.ts | 5 +++ src/types/encodeData/JSONURL.ts | 7 +++- 6 files changed, 97 insertions(+), 41 deletions(-) diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index 269870ce..2fceb610 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -676,6 +676,30 @@ myErc725.encodeData([ +
+ Encode array length + +If the key is of type Array and you pass an integer as a value (for instance, the array length), it will be encoded accordingly. + +```javascript title="Encode the length of an array" +myErc725.encodeData([ + { + keyName: 'LSP3IssuedAssets[]', + value: 5, + }, +]); +/** +{ + keys: [ + '0x3a47ab5bd3a594c3a8995f8fa58d0876c96819ca4516bd76100c92462f2f9dc0', + ], + values: ['0x00000000000000000000000000000005'], +} +*/ +``` + +
+ --- ## encodePermissions diff --git a/src/index.ts b/src/index.ts index 957d5c50..7287a2e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,7 +125,7 @@ export class ERC725 { } /** - * To prevent weird behovior from the lib, we must make sure all the schemas are correct before loading them. + * To prevent weird behavior from the lib, we must make sure all the schemas are correct before loading them. * * @param schemas * @returns diff --git a/src/lib/encoder.ts b/src/lib/encoder.ts index 42f93890..0944edd6 100644 --- a/src/lib/encoder.ts +++ b/src/lib/encoder.ts @@ -332,30 +332,6 @@ const returnTypesOfUintNCompactBytesArray = () => { return types; }; -/** - * Encodes any set of strings to string[CompactBytesArray] - * - * @param values An array of non restricted strings - * @returns string[CompactBytesArray] - */ -const encodeStringCompactBytesArray = (values: string[]): string => { - const hexValues: string[] = values.map((element) => utf8ToHex(element)); - - return encodeCompactBytesArray(hexValues); -}; - -/** - * Decode a string[CompactBytesArray] to an array of strings - * @param compactBytesArray A string[CompactBytesArray] - * @returns An array of strings - */ -const decodeStringCompactBytesArray = (compactBytesArray: string): string[] => { - const hexValues: string[] = decodeCompactBytesArray(compactBytesArray); - const stringValues: string[] = hexValues.map((element) => hexToUtf8(element)); - - return stringValues; -}; - const valueTypeEncodingMap = { bool: { encode: (value: boolean) => (value ? '0x01' : '0x00'), @@ -494,8 +470,17 @@ const valueTypeEncodingMap = { decode: (value: string) => decodeCompactBytesArray(value), }, 'string[CompactBytesArray]': { - encode: (value: string[]) => encodeStringCompactBytesArray(value), - decode: (value: string) => decodeStringCompactBytesArray(value), + encode: (values: string[]) => { + const hexValues: string[] = values.map((element) => utf8ToHex(element)); + return encodeCompactBytesArray(hexValues); + }, + decode: (value: string) => { + const hexValues: string[] = decodeCompactBytesArray(value); + const stringValues: string[] = hexValues.map((element) => + hexToUtf8(element), + ); + return stringValues; + }, }, ...returnTypesOfBytesNCompactBytesArray(), ...returnTypesOfUintNCompactBytesArray(), diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 0ae455ad..57a5add0 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -29,7 +29,6 @@ import { SUPPORTED_VERIFICATION_METHOD_STRINGS } from '../constants/constants'; import { guessKeyTypeFromKeyName, isDataAuthentic, - encodeArrayKey, encodeKeyValue, decodeKeyValue, encodeKey, @@ -44,6 +43,7 @@ import { decodeKey } from './decodeData'; describe('utils', () => { describe('encodeKey/decodeKey', () => { const testCases = [ + // test encoding an array of address { schema: { name: 'LSP3IssuedAssets[]', @@ -226,12 +226,28 @@ describe('utils', () => { encodeKey(testCase.schema as ERC725JSONSchema, testCase.decodedValue), testCase.encodedValue, ); + assert.deepStrictEqual( decodeKey(testCase.schema as ERC725JSONSchema, testCase.encodedValue), testCase.decodedValue, ); }); }); + + it('should encode the array length only if passing a number', async () => { + const schema: ERC725JSONSchema = { + name: 'LSP3IssuedAssets[]', + key: '0x3a47ab5bd3a594c3a8995f8fa58d0876c96819ca4516bd76100c92462f2f9dc0', + keyType: 'Array', + valueContent: 'Address', + valueType: 'address', + }; + + const decodedValue = 3; + const encodedValue = '0x00000000000000000000000000000003'; + + assert.equal(encodeKey(schema, decodedValue), encodedValue); + }); }); describe('encodeKeyValue/decodeKeyValue', () => { @@ -391,19 +407,19 @@ describe('utils', () => { }); describe('encodeArrayKey', () => { - it('encodes array key correctly', () => { - const key = - '0x3a47ab5bd3a594c3a8995f8fa58d0876c96819ca4516bd76100c92462f2f9dc0'; - - const expectedValues = [ - '0x3a47ab5bd3a594c3a8995f8fa58d087600000000000000000000000000000000', - '0x3a47ab5bd3a594c3a8995f8fa58d087600000000000000000000000000000001', - '0x3a47ab5bd3a594c3a8995f8fa58d087600000000000000000000000000000002', - ]; + it('should encode the array length only if passing a number', async () => { + const schema: ERC725JSONSchema = { + name: 'LSP3IssuedAssets[]', + key: '0x3a47ab5bd3a594c3a8995f8fa58d0876c96819ca4516bd76100c92462f2f9dc0', + keyType: 'Array', + valueContent: 'Address', + valueType: 'address', + }; - expectedValues.forEach((expectedValue, index) => { - assert.strictEqual(encodeArrayKey(key, index), expectedValue); - }); + const decodedValue = 3; + const encodedValue = '0x00000000000000000000000000000003'; + + assert.equal(encodeKey(schema, decodedValue), encodedValue); }); }); @@ -567,6 +583,27 @@ describe('utils', () => { }); }); + it('encodes array length only if giving a number', () => { + const length = 5; + + const encodedArrayLengthKey = encodeData( + [ + { + keyName: 'LSP3IssuedAssets[]', + value: length, + }, + ], + schemas, + ); + + assert.deepStrictEqual(encodedArrayLengthKey, { + keys: [ + '0x3a47ab5bd3a594c3a8995f8fa58d0876c96819ca4516bd76100c92462f2f9dc0', + ], + values: ['0x00000000000000000000000000000005'], + }); + }); + it('encodes multiple keys', () => { const encodedMultipleKeys = encodeData( [ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a29a9a72..0e5555c9 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -249,6 +249,11 @@ export function encodeKey( switch (lowerCaseKeyType) { case 'array': { + // if we are encoding only the Array length + if (typeof value === 'number') { + return encodeValueType('uint128', value); + } + if (!Array.isArray(value)) { console.error("Can't encode a non array for key of type array"); return null; diff --git a/src/types/encodeData/JSONURL.ts b/src/types/encodeData/JSONURL.ts index 1c00ac90..4948cdc9 100644 --- a/src/types/encodeData/JSONURL.ts +++ b/src/types/encodeData/JSONURL.ts @@ -27,7 +27,12 @@ export interface URLDataWithJson extends URLData { export type JSONURLDataToEncode = URLDataWithHash | URLDataWithJson; -export type EncodeDataType = string | string[] | JSONURLDataToEncode | boolean; +export type EncodeDataType = + | string + | string[] + | JSONURLDataToEncode + | boolean + | number; export interface EncodeDataReturn { keys: string[];