From ab83f0839457addfb3a03f0925b2f24a67743f79 Mon Sep 17 00:00:00 2001 From: Felix Hildebrandt <61689369+fhildeb@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:02:09 +0100 Subject: [PATCH 1/6] refactor!: remove supportsInterface --- docs/classes/ERC725.md | 82 ------------------------------------- src/constants/interfaces.ts | 39 ------------------ src/index.test.ts | 39 ------------------ src/index.ts | 46 +-------------------- src/lib/detector.test.ts | 44 +------------------- src/lib/detector.ts | 36 ---------------- 6 files changed, 2 insertions(+), 284 deletions(-) delete mode 100644 src/constants/interfaces.ts diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index 85de1596..d50f7c76 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -1418,85 +1418,3 @@ await myErc725.isValidSignature( [lsp6 keymanager permissions]: ../../../../../standards/universal-profile/lsp6-key-manager#permissions [lsp6 keymanager standard]: https://docs.lukso.tech/standards/universal-profile/lsp6-key-manager [lsp-2 erc725yjsonschema]: https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md - -## supportsInterface - -```js -myERC725.supportsInterface(interfaceIdOrName); -``` - -```js -ERC725.supportsInterface(interfaceIdOrName, options); -``` - -You can use this function if you need to check if the ERC725 object or a smart contract supports a specific interface (by ID or name). When you use the function on your instantiated ERC725 class, it will use the contract address and provider provided at instantiation. On non instantiated class, you need to specify them in the `options` parameter. - -:::caution -The `interfaceId` is not the most secure way to check for a standard, as they could be set manually. -::: - -#### Parameters - -##### 1. `interfaceIdOrName` - String - -Either a string of the hexadecimal `interfaceID` as defined by [ERC165](https://eips.ethereum.org/EIPS/eip-165) or one of the predefined interface names: - -| interfaceName | Standard | -| :------------------------------ | :------------------------------------------------------------------------------------------------------------------------- | -| `ERC1271` | [EIP-1271: Standard Signature Validation Method for Contracts](https://eips.ethereum.org/EIPS/eip-1271) | -| `ERC725X` | [EIP-725: General execution standard](https://eips.ethereum.org/EIPS/eip-725) | -| `ERC725Y` | [EIP-725: General key-value store](https://eips.ethereum.org/EIPS/eip-725) | -| `LSP0ERC725Account` | [LSP-0: ERC725 Account](https://docs.lukso.tech/standards/universal-profile/lsp0-erc725account) | -| `LSP1UniversalReceiver` | [LSP-1: Universal Receiver](https://docs.lukso.tech/standards/generic-standards/lsp1-universal-receiver) | -| `LSP1UniversalReceiverDelegate` | [LSP-1: Universal Receiver Delegate](https://docs.lukso.tech/standards/universal-profile/lsp1-universal-receiver-delegate) | -| `LSP6KeyManager` | [LSP-6: Key Manager](https://docs.lukso.tech/standards/universal-profile/lsp6-key-manager) | -| `LSP7DigitalAsset` | [LSP-7: Digital Asset](https://docs.lukso.tech/standards/nft-2.0/LSP7-Digital-Asset) | -| `LSP8IdentifiableDigitalAsset` | [LSP-8: Identifiable Digital Asset](https://docs.lukso.tech/standards/nft-2.0/LSP8-Identifiable-Digital-Asset) | -| `LSP9Vault` | [LSP-9: Vault](https://docs.lukso.tech/standards/universal-profile/lsp9-vault) | - -:::info - -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) - -On non instantiated class, you should provide an `options` object. - -| Name | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------- | -| `address` | string | Address of the smart contract to check against a certain interface. | -| `rpcUrl` | string | RPC URL to connect to the network the smart contract is deployed to. | -| `gas` | number | Optional: gas parameter to use. Default: 1_000_000. | - -#### Returns - -| Type | Description | -| :----------------- | :------------------------------------------------------------ | -| `Promise` | Returns `true` if the interface was found, otherwise `false`. | - -#### Examples - -```javascript title="By using the interface ID" -myErc725.supportsInterface('0xfd4d5c50'); -// true - -ERC725.supportsInterface('0xfd4d5c50', { - address: '0xe408BDDbBAB1985006A2c481700DD473F932e5cB', - rpcUrl: 'https://rpc.testnet.lukso.network', -}); -// false -``` - -```javascript title="By using interface name" -myErc725.supportsInterface('LSP0ERC725Account'); -// false - -ERC725.supportsInterface('LSP0ERC725Account', { - address: '0x0Dc07C77985fE31996Ed612F568eb441afe5768D', - rpcUrl: 'https://rpc.testnet.lukso.network', - gas: 20_000_000, -}); -// true -``` diff --git a/src/constants/interfaces.ts b/src/constants/interfaces.ts deleted file mode 100644 index 88f5338d..00000000 --- a/src/constants/interfaces.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - This file is part of @erc725/erc725.js. - @erc725/erc725.js is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - @erc725/erc725.js is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Lesser General Public License for more details. - You should have received a copy of the GNU Lesser General Public License - along with @erc725/erc725.js. If not, see . -*/ - -// from @lukso/lsp-smart-contracts v0.12.0, erc725.js should stay independent -export const INTERFACE_IDS_0_12_0 = { - ERC1271: '0x1626ba7e', - ERC725X: '0x7545acac', - ERC725Y: '0x629aa694', - LSP0ERC725Account: '0x24871b3d', - LSP1UniversalReceiver: '0x6bb56a14', - LSP1UniversalReceiverDelegate: '0xa245bbda', - LSP6KeyManager: '0x23f34c62', - LSP7DigitalAsset: '0xdaa746b7', - LSP8IdentifiableDigitalAsset: '0x30dc5278', - LSP9Vault: '0x28af17e6', - LSP11BasicSocialRecovery: '0x049a28f1', - LSP14Ownable2Step: '0x94be5999', - LSP17Extendable: '0xa918fa6b', - LSP17Extension: '0xcee78b40', - LSP20CallVerification: '0x1a0eb6a5', - LSP20CallVerifier: '0x0d6ecac7', - LSP25ExecuteRelayCall: '0x5ac79908', -}; - -export interface AddressProviderOptions { - address: string; - provider: any; -} diff --git a/src/index.test.ts b/src/index.test.ts index 6e444748..e241db2e 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -47,7 +47,6 @@ import { SUPPORTED_VERIFICATION_METHOD_STRINGS, } from './constants/constants'; import { decodeKey } from './lib/decodeData'; -import { INTERFACE_IDS_0_12_0 } from './constants/interfaces'; const address = '0x0c03fba782b07bcf810deb3b7f0595024a444f4e'; @@ -1390,44 +1389,6 @@ describe('encodeKeyName', () => { }); }); -describe('supportsInterface', () => { - const erc725Instance = new ERC725([]); - - it('is available on instance and class', () => { - assert.typeOf(ERC725.supportsInterface, 'function'); - assert.typeOf(erc725Instance.supportsInterface, 'function'); - }); - - const interfaceId = INTERFACE_IDS_0_12_0.LSP1UniversalReceiver; - const rpcUrl = 'https://my.test.provider'; - const contractAddress = '0xcafecafecafecafecafecafecafecafecafecafe'; - - it('should throw when provided address is not an address', async () => { - try { - await ERC725.supportsInterface(interfaceId, { - address: 'notAnAddress', - rpcUrl, - }); - } catch (error: any) { - assert.deepStrictEqual(error.message, 'Invalid address'); - } - }); - - it('should throw when rpcUrl is not provided on non instantiated class', async () => { - try { - await ERC725.supportsInterface(interfaceId, { - address: contractAddress, - // @ts-ignore - rpcUrl: undefined, - }); - } catch (error: any) { - assert.deepStrictEqual(error.message, 'Missing RPC URL'); - } - }); - - // TODO: add test to test the actual behavior of the function. -}); - describe('checkPermissions', () => { const erc725Instance = new ERC725([]); diff --git a/src/index.ts b/src/index.ts index 4d37d9be..6cf27f14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,7 +61,7 @@ import { decodeData } from './lib/decodeData'; import { getDataFromExternalSources } from './lib/getDataFromExternalSources'; import { DynamicKeyPart, DynamicKeyParts } from './types/dynamicKeys'; import { getData } from './lib/getData'; -import { supportsInterface, checkPermissions } from './lib/detector'; +import { checkPermissions } from './lib/detector'; import { decodeMappingKey } from './lib/decodeMappingKey'; export { @@ -578,50 +578,6 @@ export class ERC725 { return decodeMappingKey(keyHash, keyNameOrSchema); } - /** - * Check if the ERC725 object supports - * a certain interface. - * - * @param interfaceIdOrName Interface ID or supported interface name. - * @returns {Promise} if interface is supported. - */ - async supportsInterface(interfaceIdOrName: string): Promise { - const { address, provider } = this.getAddressAndProvider(); - - return supportsInterface(interfaceIdOrName, { - address, - provider, - }); - } - - /** - * Check if a smart contract address - * supports a certain interface. - * - * @param {string} interfaceIdOrName Interface ID or supported interface name. - * @param options Object of address, RPC URL and optional gas. - * @returns {Promise} if interface is supported. - */ - static async supportsInterface( - interfaceIdOrName: string, - options: { address: string; rpcUrl: string; gas?: number }, - ): Promise { - if (!isAddress(options.address)) { - throw new Error('Invalid address'); - } - if (!options.rpcUrl) { - throw new Error('Missing RPC URL'); - } - - return supportsInterface(interfaceIdOrName, { - address: options.address, - provider: this.initializeProvider( - options.rpcUrl, - options?.gas ? options?.gas : DEFAULT_GAS_VALUE, - ), - }); - } - /** * Check if the required permissions are included in the granted permissions as defined by the LSP6 KeyManager Standard. * diff --git a/src/lib/detector.test.ts b/src/lib/detector.test.ts index bfde55c0..3cb5a6fc 100644 --- a/src/lib/detector.test.ts +++ b/src/lib/detector.test.ts @@ -13,56 +13,14 @@ */ /** * @file lib/detector.test.ts - * @author Hugo Masclet <@Hugoo> - * @author Felix Hildebrandt <@fhildeb> * @date 2022 */ /* eslint-disable no-unused-expressions */ import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { INTERFACE_IDS_0_12_0 } from '../constants/interfaces'; -import { supportsInterface, checkPermissions } from './detector'; - -describe('supportsInterface', () => { - it('it should return true if the contract supports the interface with name', async () => { - const contractAddress = '0xcafecafecafecafecafecafecafecafecafecafe'; - const interfaceName = 'LSP0ERC725Account'; - - const providerStub = { supportsInterface: sinon.stub() }; - - providerStub.supportsInterface - .withArgs(contractAddress, INTERFACE_IDS_0_12_0[interfaceName]) - .returns(Promise.resolve(true)); - - const doesSupportInterface = await supportsInterface(interfaceName, { - address: contractAddress, - provider: providerStub, - }); - - expect(doesSupportInterface).to.be.true; - }); - - it('it should return true if the contract supports the interface with interfaceId', async () => { - const contractAddress = '0xcafecafecafecafecafecafecafecafecafecafe'; - const interfaceId = INTERFACE_IDS_0_12_0.LSP1UniversalReceiver; - - const providerStub = { supportsInterface: sinon.stub() }; - - providerStub.supportsInterface - .withArgs(contractAddress, interfaceId) - .returns(Promise.resolve(true)); - - const doesSupportInterface = await supportsInterface(interfaceId, { - address: contractAddress, - provider: providerStub, - }); - - expect(doesSupportInterface).to.be.true; - }); -}); +import { checkPermissions } from './detector'; describe('checkPermissions', () => { describe('test with single permission', () => { diff --git a/src/lib/detector.ts b/src/lib/detector.ts index b60fb638..c543897d 100644 --- a/src/lib/detector.ts +++ b/src/lib/detector.ts @@ -16,47 +16,11 @@ /** * @file detector.ts - * @author Hugo Masclet <@Hugoo> - * @author Felix Hildebrandt <@fhildeb> * @date 2022 */ import { LSP6_DEFAULT_PERMISSIONS } from '../constants/constants'; -import { - AddressProviderOptions, - INTERFACE_IDS_0_12_0, -} from '../constants/interfaces'; - -/** - * Check if a smart contract address - * supports a certain interface. - * - * @param {string} interfaceId Interface ID or supported interface name. - * @param options Object with address and RPC URL. - * @returns {Promise} if interface is supported. - */ -export const supportsInterface = async ( - interfaceIdOrName: string, - options: AddressProviderOptions, -): Promise => { - let plainInterfaceId: string; - if (INTERFACE_IDS_0_12_0[interfaceIdOrName]) { - plainInterfaceId = INTERFACE_IDS_0_12_0[interfaceIdOrName]; - } else { - plainInterfaceId = interfaceIdOrName; - } - - try { - return await options.provider.supportsInterface( - options.address, - plainInterfaceId, - ); - } catch (error) { - throw new Error(`Error checking the interface: ${error}`); - } -}; - /** * @notice Check if the given string is a valid 32-byte hex string. * @param str The string to be checked. From b8823796c5f99d89d56954c894dfb6964adc552a Mon Sep 17 00:00:00 2001 From: Jean Cvllr <31145285+CJ42@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:10:33 +0000 Subject: [PATCH 2/6] feat: add more schemas available to parse via `getSchema` (#351) --- src/schemas/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/schemas/index.ts b/src/schemas/index.ts index c533aa01..eb9e5154 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -6,6 +6,11 @@ import LSP4DigitalAssetLegacy from '../../schemas/LSP4DigitalAssetLegacy.json'; import LSP4DigitalAsset from '../../schemas/LSP4DigitalAsset.json'; import LSP5ReceivedAssets from '../../schemas/LSP5ReceivedAssets.json'; import LSP6KeyManager from '../../schemas/LSP6KeyManager.json'; +import LSP8IdentifiableDigitalAsset from '../../schemas/LSP8IdentifiableDigitalAsset.json'; +import LSP9Vault from '../../schemas/LSP9Vault.json'; +import LSP10ReceivedVaults from '../../schemas/LSP10ReceivedVaults.json'; +import LSP12IssuedAssets from '../../schemas/LSP12IssuedAssets.json'; +import LSP17ContractExtension from '../../schemas/LSP17ContractExtension.json'; export default LSP1UniversalReceiverDelegate.concat( LSP3Profile, @@ -13,4 +18,9 @@ export default LSP1UniversalReceiverDelegate.concat( LSP4DigitalAsset, LSP5ReceivedAssets, LSP6KeyManager, + LSP8IdentifiableDigitalAsset, + LSP9Vault, + LSP10ReceivedVaults, + LSP12IssuedAssets, + LSP17ContractExtension, ) as ERC725JSONSchema[]; From 77919777da6e102e2db0ded061d6ab9ed4114e9a Mon Sep 17 00:00:00 2001 From: Jean Cvllr <31145285+CJ42@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:16:36 +0000 Subject: [PATCH 3/6] refactor: improve code for tuple checks (#353) * refactor: extract to internal methods * refactor: improve method to extract tuple elements --- src/lib/decodeData.ts | 60 +++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/lib/decodeData.ts b/src/lib/decodeData.ts index 16daead9..d0913f17 100644 --- a/src/lib/decodeData.ts +++ b/src/lib/decodeData.ts @@ -19,7 +19,6 @@ * @author Callum Grindle <@CallumGrindle> * @date 2023 */ - import { isHexStrict } from 'web3-utils'; import { COMPACT_BYTES_ARRAY_STRING } from '../constants/constants'; @@ -33,36 +32,39 @@ import { decodeKeyValue, encodeArrayKey } from './utils'; const tupleValueTypesRegex = /bytes(\d+)/; const valueContentsBytesRegex = /Bytes(\d+)/; -export const isValidTuple = (valueType: string, valueContent: string) => { - if (valueType.length <= 2 && valueContent.length <= 2) { +const isValidTupleDefinition = (tupleContent: string): boolean => { + if (tupleContent.length <= 2) { return false; } - if ( - valueType[0] !== '(' && - valueType[valueType.length - 1] !== ')' && - valueContent[0] !== '(' && - valueContent[valueContent.length - 1] !== ')' + tupleContent[0] !== '(' && + tupleContent[tupleContent.length - 1] !== ')' ) { return false; } - // At this stage, we can assume the user is trying to use a tuple, let's throw errors instead of returning - // false + return true; +}; - let valueTypeToDecode = valueType; +const extractTupleElements = (tupleContent: string): string[] => + tupleContent.substring(1, tupleContent.length - 1).split(','); - if (valueType.includes(COMPACT_BYTES_ARRAY_STRING)) { - valueTypeToDecode = valueType.replace(COMPACT_BYTES_ARRAY_STRING, ''); +export const isValidTuple = (valueType: string, valueContent: string) => { + if ( + !isValidTupleDefinition(valueType) || + !isValidTupleDefinition(valueContent) + ) { + return false; } - const valueTypeParts = valueTypeToDecode - .substring(1, valueTypeToDecode.length - 1) - .split(','); + // At this stage, we can assume the user is trying to use a tuple, + // let's throw errors instead of returning false - const valueContentParts = valueContent - .substring(1, valueContent.length - 1) - .split(','); + // Sanitize the string to keep only the tuple, if we are dealing with `CompactBytesArray` + const valueTypeToDecode = valueType.replace(COMPACT_BYTES_ARRAY_STRING, ''); + + const valueTypeParts = extractTupleElements(valueTypeToDecode); + const valueContentParts = extractTupleElements(valueContent); const tuplesValidValueTypes = [ 'bytes2', @@ -139,27 +141,19 @@ export const decodeTupleKeyValue = ( ): Array => { // We assume data has already been validated at this stage - let valueTypeToDecode = valueType; + // Sanitize the string to keep only the tuple, if we are dealing with `CompactBytesArray` + const valueTypeToDecode = valueType.replace(COMPACT_BYTES_ARRAY_STRING, ''); - if (valueType.includes('[CompactBytesArray')) { - valueTypeToDecode = valueType.replace(COMPACT_BYTES_ARRAY_STRING, ''); - } - - const valueTypeParts = valueTypeToDecode - .substring(1, valueTypeToDecode.length - 1) - .split(','); - const valueContentParts = valueContent - .substring(1, valueContent.length - 1) - .split(','); + const valueTypeParts = extractTupleElements(valueTypeToDecode); + const valueContentParts = extractTupleElements(valueContent); const bytesLengths: number[] = []; + valueTypeParts.forEach((valueTypePart) => { const regexMatch = valueTypePart.match(tupleValueTypesRegex); // if we are dealing with `bytesN` - if (regexMatch) { - bytesLengths.push(parseInt(regexMatch[1], 10)); - } + if (regexMatch) bytesLengths.push(parseInt(regexMatch[1], 10)); if (valueTypePart === 'address') bytesLengths.push(20); }); 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 4/6] 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 5/6] 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[]; From 417a4e8ff2c74f3f9e35d0018a4973c97c6ac997 Mon Sep 17 00:00:00 2001 From: Jean Cvllr <31145285+CJ42@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:36:28 +0000 Subject: [PATCH 6/6] feat: add support to encode / decode any `uint8` to `uint256` types (#355) * feat: add support for more value types * refactor: extract to internal methods * refactor: improve method to extract tuple elements * feat: support encoding for unsigned integers from `uint8` to `uint256` * refactor: replace complex regex with modulo --- src/lib/decodeData.ts | 19 +- src/lib/encoder.test.ts | 80 +++++-- src/lib/encoder.ts | 412 ++++++++++++++++++++-------------- src/lib/utils.ts | 14 +- src/types/ERC725JSONSchema.ts | 145 ++++++++++-- 5 files changed, 448 insertions(+), 222 deletions(-) diff --git a/src/lib/decodeData.ts b/src/lib/decodeData.ts index d0913f17..9cbe885e 100644 --- a/src/lib/decodeData.ts +++ b/src/lib/decodeData.ts @@ -23,7 +23,11 @@ import { isHexStrict } from 'web3-utils'; import { COMPACT_BYTES_ARRAY_STRING } from '../constants/constants'; import { DecodeDataInput, DecodeDataOutput } from '../types/decodeData'; -import { ERC725JSONSchema } from '../types/ERC725JSONSchema'; +import { + ALL_VALUE_TYPES, + ERC725JSONSchema, + isValidValueType, +} from '../types/ERC725JSONSchema'; import { isDynamicKeyName } from './encodeKeyName'; import { valueContentEncodingMap, decodeValueType } from './encoder'; import { getSchemaElement } from './getSchemaElement'; @@ -66,15 +70,6 @@ export const isValidTuple = (valueType: string, valueContent: string) => { const valueTypeParts = extractTupleElements(valueTypeToDecode); const valueContentParts = extractTupleElements(valueContent); - const tuplesValidValueTypes = [ - 'bytes2', - 'bytes4', - 'bytes8', - 'bytes16', - 'bytes32', - 'address', - ]; - if (valueTypeParts.length !== valueContentParts.length) { throw new Error( `Invalid tuple for valueType: ${valueType} / valueContent: ${valueContent}. They should have the same number of elements. Got: ${valueTypeParts.length} and ${valueContentParts.length}`, @@ -82,9 +77,9 @@ export const isValidTuple = (valueType: string, valueContent: string) => { } for (let i = 0; i < valueTypeParts.length; i++) { - if (!tuplesValidValueTypes.includes(valueTypeParts[i])) { + if (!isValidValueType(valueTypeParts[i])) { throw new Error( - `Invalid tuple for valueType: ${valueType} / valueContent: ${valueContent}. Type: ${valueTypeParts[i]} is not valid. Valid types are: ${tuplesValidValueTypes}`, + `Invalid tuple for valueType: ${valueType} / valueContent: ${valueContent}. Type: ${valueTypeParts[i]} is not valid. Valid types are: ${ALL_VALUE_TYPES}`, ); } diff --git a/src/lib/encoder.test.ts b/src/lib/encoder.test.ts index 7519bdb9..4389821e 100644 --- a/src/lib/encoder.test.ts +++ b/src/lib/encoder.test.ts @@ -327,8 +327,39 @@ describe('encoder', () => { }); }); - describe('`uint128` type', () => { + describe('`uintN` type', () => { const validTestCases = [ + { + valueType: 'uint256', + decodedValue: 1337, + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000539', + }, + { + valueType: 'uint8', + decodedValue: 10, + encodedValue: '0x0a', + }, + { + valueType: 'uint16', + decodedValue: 10, + encodedValue: '0x000a', + }, + { + valueType: 'uint24', + decodedValue: 10, + encodedValue: '0x00000a', + }, + { + valueType: 'uint32', + decodedValue: 10, + encodedValue: '0x0000000a', + }, + { + valueType: 'uint64', + decodedValue: 25, + encodedValue: '0x0000000000000019', + }, { valueType: 'uint128', decodedValue: 11, @@ -350,31 +381,32 @@ describe('encoder', () => { ); }); }); - }); - - describe('`uint256` type', () => { - const validTestCases = [ - { - valueType: 'uint256', - decodedValue: 1337, - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000539', - }, - ]; - validTestCases.forEach((testCase) => { - it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { - const encodedValue = encodeValueType( - testCase.valueType, - testCase.decodedValue, - ); + it('should throw an error when trying to encode/decode with an invalid `uintN` type', async () => { + for (let ii = 1; ii <= 256; ii++) { + // only test for uintN where N is a multiple of 8 + if (ii % 8 !== 0) { + assert.throws(() => encodeValueType(`uint${ii}`, 12345)); + assert.throws(() => decodeValueType(`uint${ii}`, '0x00000001')); + + // test that `uintN` are encoded / decode correct when N is not a multiple of 8 + } else { + const expectedEncodedValue = `0x${'00'.repeat(ii / 8 - 1)}0a`; + const expectedDecodedValue = 10; + + const encodedValue = encodeValueType( + `uint${ii}`, + expectedDecodedValue, + ); + assert.equal(encodedValue, expectedEncodedValue); - assert.deepStrictEqual(encodedValue, testCase.encodedValue); - assert.deepStrictEqual( - decodeValueType(testCase.valueType, encodedValue), - testCase.decodedValue, - ); - }); + const decodedValue = decodeValueType( + `uint${ii}`, + expectedEncodedValue, + ); + assert.equal(decodedValue, expectedDecodedValue); + } + } }); }); diff --git a/src/lib/encoder.ts b/src/lib/encoder.ts index 0944edd6..6696cef9 100644 --- a/src/lib/encoder.ts +++ b/src/lib/encoder.ts @@ -51,11 +51,19 @@ import { SUPPORTED_VERIFICATION_METHOD_STRINGS, UNKNOWN_VERIFICATION_METHOD, } from '../constants/constants'; -import { getVerificationMethod, hashData, countNumberOfBytes } from './utils'; +import { + getVerificationMethod, + hashData, + countNumberOfBytes, + isValidUintSize, +} from './utils'; +import { ERC725JSONSchemaValueType } from '../types/ERC725JSONSchema'; const abiCoder = AbiCoder; -const bytesNRegex = /Bytes(\d+)/; +const uintNValueTypeRegex = /^uint(\d+)$/; + +const BytesNValueContentRegex = /Bytes(\d+)/; const ALLOWED_BYTES_SIZES = [2, 4, 8, 16, 32, 64, 128, 256]; @@ -332,165 +340,244 @@ const returnTypesOfUintNCompactBytesArray = () => { return types; }; -const valueTypeEncodingMap = { - bool: { - encode: (value: boolean) => (value ? '0x01' : '0x00'), - decode: (value: string) => value === '0x01', - }, - boolean: { - encode: (value: boolean) => (value ? '0x01' : '0x00'), - decode: (value: string) => value === '0x01', - }, - string: { - encode: (value: string | number) => { - // if we receive a number as input, - // convert each letter to its utf8 hex representation - if (typeof value === 'number') { - return utf8ToHex(`${value}`); - } +/** + * 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 utf8ToHex(value); - }, - decode: (value: string) => hexToUtf8(value), - }, - address: { - encode: (value: string) => { - // abi-encode pads to 32 x 00 bytes on the left, so we need to remove them - const abiEncodedValue = abiCoder.encodeParameter('address', value); - - // convert to an array of individual bytes - const bytesArray = hexToBytes(abiEncodedValue); - - // just keep the last 20 bytes, starting at index 12 - return bytesToHex(bytesArray.slice(12)); - }, - decode: (value: string) => toChecksumAddress(value), - }, - // NOTE: We could add conditional handling of numeric values here... - uint128: { - encode: (value: string | number) => { - const abiEncodedValue = abiCoder.encodeParameter('uint128', value); - const bytesArray = hexToBytes(abiEncodedValue); - return bytesToHex(bytesArray.slice(16)); - }, - decode: (value: string) => { - if (!isHex(value)) { - throw new Error(`Can't convert ${value} to uint128, value is not hex.`); - } + return encodeCompactBytesArray(hexValues); +}; - if (value.length > 34) { - throw new Error( - `Can't convert hex value ${value} to uint128. Too many bytes. ${ - (value.length - 2) / 2 - } > 16`, - ); - } +/** + * 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 toBN(value).toNumber(); - }, - }, - uint256: { - encode: (value: string | number) => { - return abiCoder.encodeParameter('uint256', value); - }, - decode: (value: string) => { - if (!isHex(value)) { - throw new Error(`Can't convert ${value} to uint256, value is not hex.`); - } + return stringValues; +}; - const numberOfBytes = countNumberOfBytes(value); +const valueTypeEncodingMap = ( + type: string, +): { + encode: (value: any) => string; + decode: (value: string) => any; +} => { + const uintNRegexMatch = type.match(uintNValueTypeRegex); + + const uintLength = uintNRegexMatch + ? parseInt(uintNRegexMatch[0].slice(4), 10) + : ''; + + if (type.includes('[CompactBytesArray]')) { + const compactBytesArrayMap = { + 'bytes[CompactBytesArray]': { + encode: (value: string[]) => encodeCompactBytesArray(value), + decode: (value: string) => decodeCompactBytesArray(value), + }, + 'string[CompactBytesArray]': { + encode: (value: string[]) => encodeStringCompactBytesArray(value), + decode: (value: string) => decodeStringCompactBytesArray(value), + }, + ...returnTypesOfBytesNCompactBytesArray(), + ...returnTypesOfUintNCompactBytesArray(), + }; - if (numberOfBytes > 32) { - throw new Error( - `Can't convert hex value ${value} to uint256. Too many bytes. ${numberOfBytes} is above the maximal number of bytes 32.`, - ); - } + return compactBytesArrayMap[type]; + } - return toBN(value).toNumber(); - }, - }, - bytes32: { - encode: (value: string | number) => encodeToBytesN('bytes32', value), - decode: (value: string) => abiCoder.decodeParameter('bytes32', value), - }, - bytes4: { - encode: (value: string | number) => encodeToBytesN('bytes4', value), - decode: (value: string) => { - // we need to abi-encode the value again to ensure that: - // - that data to decode does not go over 4 bytes. - // - if the data is less than 4 bytes, that it gets padded to 4 bytes long. - const reEncodedData = abiCoder.encodeParameter('bytes4', value); - return abiCoder.decodeParameter('bytes4', reEncodedData); - }, - }, - bytes: { - encode: (value: string) => toHex(value), - decode: (value: string) => value, - }, - 'bool[]': { - encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), - decode: (value: string) => abiCoder.decodeParameter('bool[]', value), - }, - 'boolean[]': { - encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), - decode: (value: string) => abiCoder.decodeParameter('bool[]', value), - }, - 'string[]': { - encode: (value: string[]) => abiCoder.encodeParameter('string[]', value), - decode: (value: string) => abiCoder.decodeParameter('string[]', value), - }, - 'address[]': { - encode: (value: string[]) => abiCoder.encodeParameter('address[]', value), - decode: (value: string) => abiCoder.decodeParameter('address[]', value), - }, - 'uint256[]': { - encode: (value: Array) => - abiCoder.encodeParameter('uint256[]', value), - decode: (value: string) => { - // we want to return an array of numbers as [1, 2, 3], not an array of strings as [ '1', '2', '3'] - return abiCoder - .decodeParameter('uint256[]', value) - .map((numberAsString) => parseInt(numberAsString, 10)); - }, - }, - 'bytes32[]': { - encode: (value: string[]) => abiCoder.encodeParameter('bytes32[]', value), - decode: (value: string) => abiCoder.decodeParameter('bytes32[]', value), - }, - 'bytes4[]': { - encode: (value: string[]) => abiCoder.encodeParameter('bytes4[]', value), - decode: (value: string) => abiCoder.decodeParameter('bytes4[]', value), - }, - 'bytes[]': { - encode: (value: string[]) => abiCoder.encodeParameter('bytes[]', value), - decode: (value: string) => abiCoder.decodeParameter('bytes[]', value), - }, - 'bytes[CompactBytesArray]': { - encode: (value: string[]) => encodeCompactBytesArray(value), - decode: (value: string) => decodeCompactBytesArray(value), - }, - 'string[CompactBytesArray]': { - 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(), + switch (type) { + case 'bool': + case 'boolean': + return { + encode: (value: boolean) => (value ? '0x01' : '0x00'), + decode: (value: string) => value === '0x01', + }; + case 'string': + return { + encode: (value: string | number) => { + // if we receive a number as input, convert each letter to its utf8 hex representation + if (typeof value === 'number') { + return utf8ToHex(`${value}`); + } + + return utf8ToHex(value); + }, + decode: (value: string) => hexToUtf8(value), + }; + case 'address': + return { + encode: (value: string) => { + // abi-encode pads to 32 x 00 bytes on the left, so we need to remove them + const abiEncodedValue = abiCoder.encodeParameter('address', value); + + // convert to an array of individual bytes + const bytesArray = hexToBytes(abiEncodedValue); + + // just keep the last 20 bytes, starting at index 12 + return bytesToHex(bytesArray.slice(12)); + }, + decode: (value: string) => toChecksumAddress(value), + }; + // NOTE: We could add conditional handling of numeric values here... + case `uint${uintLength}`: + return { + encode: (value: string | number) => { + if (!isValidUintSize(uintLength as number)) { + throw new Error( + `Can't encode ${value} as ${type}. Invalid \`uintN\` provided. Expected a multiple of 8 bits between 8 and 256.`, + ); + } + const abiEncodedValue = abiCoder.encodeParameter(type, value); + + const bytesArray = hexToBytes(abiEncodedValue); + const numberOfBytes = (uintLength as number) / 8; + + // abi-encoding always pad to 32 bytes. We need to keep the `n` rightmost bytes. + // where `n` = `numberOfBytes` + const startIndex = 32 - numberOfBytes; + + return bytesToHex(bytesArray.slice(startIndex)); + }, + decode: (value: string) => { + if (!isHex(value)) { + throw new Error( + `Can't convert ${value} to ${type}, value is not hex.`, + ); + } + + if (!isValidUintSize(uintLength as number)) { + throw new Error( + `Can't decode ${value} as ${type}. Invalid \`uintN\` provided. Expected a multiple of 8 bits between 8 and 256.`, + ); + } + + const numberOfBytes = countNumberOfBytes(value); + + if (numberOfBytes > (uintLength as number) / 8) { + throw new Error( + `Can't convert hex value ${value} to ${type}. Too many bytes. ${numberOfBytes} > 16`, + ); + } + + return toBN(value).toNumber(); + }, + }; + case 'bytes32': + return { + encode: (value: string | number) => encodeToBytesN('bytes32', value), + decode: (value: string) => abiCoder.decodeParameter('bytes32', value), + }; + case 'bytes4': + return { + encode: (value: string | number) => encodeToBytesN('bytes4', value), + decode: (value: string) => { + // we need to abi-encode the value again to ensure that: + // - that data to decode does not go over 4 bytes. + // - if the data is less than 4 bytes, that it gets padded to 4 bytes long. + const reEncodedData = abiCoder.encodeParameter('bytes4', value); + return abiCoder.decodeParameter('bytes4', reEncodedData); + }, + }; + case 'bytes': + return { + encode: (value: string) => toHex(value), + decode: (value: string) => value, + }; + case 'bool[]': + return { + encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), + decode: (value: string) => abiCoder.decodeParameter('bool[]', value), + }; + case 'boolean[]': + return { + encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), + decode: (value: string) => abiCoder.decodeParameter('bool[]', value), + }; + case 'string[]': + return { + encode: (value: string[]) => + abiCoder.encodeParameter('string[]', value), + decode: (value: string) => abiCoder.decodeParameter('string[]', value), + }; + case 'address[]': + return { + encode: (value: string[]) => + abiCoder.encodeParameter('address[]', value), + decode: (value: string) => abiCoder.decodeParameter('address[]', value), + }; + case 'uint256[]': + return { + encode: (value: Array) => + abiCoder.encodeParameter('uint256[]', value), + decode: (value: string) => { + // we want to return an array of numbers as [1, 2, 3], not an array of strings as [ '1', '2', '3'] + return abiCoder + .decodeParameter('uint256[]', value) + .map((numberAsString) => parseInt(numberAsString, 10)); + }, + }; + case 'bytes32[]': + return { + encode: (value: string[]) => + abiCoder.encodeParameter('bytes32[]', value), + decode: (value: string) => abiCoder.decodeParameter('bytes32[]', value), + }; + case 'bytes4[]': + return { + encode: (value: string[]) => + abiCoder.encodeParameter('bytes4[]', value), + decode: (value: string) => abiCoder.decodeParameter('bytes4[]', value), + }; + case 'bytes[]': + return { + encode: (value: string[]) => abiCoder.encodeParameter('bytes[]', value), + decode: (value: string) => abiCoder.decodeParameter('bytes[]', value), + }; + case 'bytes[CompactBytesArray]': + return { + encode: (value: string[]) => encodeCompactBytesArray(value), + decode: (value: string) => decodeCompactBytesArray(value), + }; + case 'string[CompactBytesArray]': + return { + encode: (value: string[]) => encodeStringCompactBytesArray(value), + decode: (value: string) => decodeStringCompactBytesArray(value), + }; + default: + return { + encode: (value: any) => { + throw new Error( + `Could not encode ${value}. Value type ${type} is unknown`, + ); + }, + decode: (value: any) => { + throw new Error( + `Could not decode ${value}. Value type ${type} is unknown`, + ); + }, + }; + } }; // Use enum for type below // Is it this enum ERC725JSONSchemaValueType? (If so, custom is missing from enum) -export const valueContentEncodingMap = (valueContent: string) => { - const bytesNRegexMatch = valueContent.match(bytesNRegex); +export const valueContentEncodingMap = ( + valueContent: string, +): { + type: string; + encode: (value: any) => string; + decode: (value: string) => any; +} => { + const bytesNRegexMatch = valueContent.match(BytesNValueContentRegex); const bytesLength = bytesNRegexMatch ? parseInt(bytesNRegexMatch[1], 10) : ''; switch (valueContent) { @@ -678,12 +765,12 @@ export const valueContentEncodingMap = (valueContent: string) => { case 'Boolean': { return { type: 'bool', - encode: (value): string => { - return valueTypeEncodingMap.bool.encode(value); + encode: (value: boolean): string => { + return valueTypeEncodingMap('bool').encode(value); }, decode: (value: string): boolean => { try { - return valueTypeEncodingMap.bool.decode(value) as any as boolean; + return valueTypeEncodingMap('bool').decode(value) as any as boolean; } catch (error) { throw new Error(`Value ${value} is not a boolean`); } @@ -711,32 +798,27 @@ export const valueContentEncodingMap = (valueContent: string) => { }; export function encodeValueType( - type: string, + type: ERC725JSONSchemaValueType | string, // for tuples and CompactBytesArray, value: string | string[] | number | number[] | boolean | boolean[], ): string { - if (!valueTypeEncodingMap[type]) { - throw new Error('Could not encode valueType: "' + type + '".'); - } - if (typeof value === 'undefined' || value === null) { return value; } - return valueTypeEncodingMap[type].encode(value); + return valueTypeEncodingMap(type).encode(value); } -export function decodeValueType(type: string, data: string) { - if (!valueTypeEncodingMap[type]) { - throw new Error('Could not decode valueType: "' + type + '".'); - } - +export function decodeValueType( + type: ERC725JSONSchemaValueType | string, // for tuples and CompactBytesArray + data: string, +) { if (data === '0x') return null; if (typeof data === 'undefined' || data === null) { return data; } - return valueTypeEncodingMap[type].decode(data); + return valueTypeEncodingMap(type).decode(data); } export function encodeValueContent( diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0e5555c9..77953c8d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -71,7 +71,7 @@ import { isValidTuple } from './decodeData'; */ export function encodeKeyValue( valueContent: string, - valueType: ERC725JSONSchemaValueType, + valueType: ERC725JSONSchemaValueType | string, decodedValue: | string | string[] @@ -365,7 +365,7 @@ export function encodeKey( */ export function decodeKeyValue( valueContent: string, - valueType: ERC725JSONSchemaValueType, + valueType: ERC725JSONSchemaValueType | string, // string for tuples and CompactBytesArray value, name?: string, ) { @@ -586,3 +586,13 @@ export function patchIPFSUrlsIfApplicable( export function countNumberOfBytes(data: string) { return stripHexPrefix(data).length / 2; } + +/** + * `uintN` must be a valid number of bits between 8 and 256, in multiple of 8 + * e.g: uint8, uint16, uint24, uint32, ..., uint256 + * + * @param bitSize the size of the uint in bits + */ +export function isValidUintSize(bitSize: number) { + return bitSize >= 8 && bitSize <= 256 && bitSize % 8 === 0; +} diff --git a/src/types/ERC725JSONSchema.ts b/src/types/ERC725JSONSchema.ts index 817c2e1e..e1b6b661 100644 --- a/src/types/ERC725JSONSchema.ts +++ b/src/types/ERC725JSONSchema.ts @@ -18,24 +18,131 @@ export type ERC725JSONSchemaValueContent = | 'Boolean' | string; // for tuples -export type ERC725JSONSchemaValueType = - | 'bool' - | 'boolean' - | 'string' - | 'address' - | 'uint256' - | 'bytes32' - | 'bytes' - | 'bytes4' - | 'string[]' - | 'address[]' - | 'uint256[]' - | 'bytes32[]' - | 'bytes4[]' - | 'bytes[]' - | 'bool[]' - | 'boolean[]' - | string; // for tuples; +export const ALL_VALUE_TYPES = [ + // unsigned integers + 'uint8', + 'uint16', + 'uint24', + 'uint32', + 'uint40', + 'uint48', + 'uint56', + 'uint64', + 'uint72', + 'uint80', + 'uint88', + 'uint96', + 'uint104', + 'uint112', + 'uint120', + 'uint128', + 'uint136', + 'uint144', + 'uint152', + 'uint160', + 'uint168', + 'uint176', + 'uint184', + 'uint192', + 'uint200', + 'uint208', + 'uint216', + 'uint224', + 'uint232', + 'uint240', + 'uint248', + 'uint256', + // signed integers + 'int8', + 'int16', + 'int24', + 'int32', + 'int40', + 'int48', + 'int56', + 'int64', + 'int72', + 'int80', + 'int88', + 'int96', + 'int104', + 'int112', + 'int120', + 'int128', + 'int136', + 'int144', + 'int152', + 'int160', + 'int168', + 'int176', + 'int184', + 'int192', + 'int200', + 'int208', + 'int216', + 'int224', + 'int232', + 'int240', + 'int248', + 'int256', + // bytesN + 'bytes1', + 'bytes2', + 'bytes3', + 'bytes4', + 'bytes5', + 'bytes6', + 'bytes7', + 'bytes8', + 'bytes9', + 'bytes10', + 'bytes11', + 'bytes12', + 'bytes13', + 'bytes14', + 'bytes15', + 'bytes16', + 'bytes17', + 'bytes18', + 'bytes19', + 'bytes20', + 'bytes21', + 'bytes22', + 'bytes23', + 'bytes24', + 'bytes25', + 'bytes26', + 'bytes27', + 'bytes28', + 'bytes29', + 'bytes30', + 'bytes31', + 'bytes32', + // others static types + 'bool', + 'boolean', + 'address', + // array and dynamic types + 'string', + 'bytes', + // arrays + 'string[]', + 'address[]', + 'uint256[]', + 'bytes32[]', + 'bytes4[]', + 'bytes[]', + 'bool[]', + 'boolean[]', +] as const; + +export type ERC725JSONSchemaValueType = (typeof ALL_VALUE_TYPES)[number]; + +export function isValidValueType( + value: string, +): value is ERC725JSONSchemaValueType { + return ALL_VALUE_TYPES.includes(value as ERC725JSONSchemaValueType); +} /** * ```javascript title=Example @@ -54,5 +161,5 @@ export interface ERC725JSONSchema { key: string; // The keccak256 hash of the name. This is the actual key that MUST be retrievable via ERC725Y.getData(bytes32 key) keyType: ERC725JSONSchemaKeyType; // Types that determine how the values should be interpreted. valueContent: ERC725JSONSchemaValueContent | string; // string holds '0x1345ABCD...' If the value content are specific bytes, than the returned value is expected to equal those bytes. - valueType: ERC725JSONSchemaValueType; + valueType: ERC725JSONSchemaValueType | string; // The type of the value. This is used to determine how the value should be encoded / decode (`string` for tuples and CompactBytesArray). }