diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index 85de1596..2fceb610 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 @@ -615,6 +676,178 @@ 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 + +```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 @@ -766,86 +999,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 @@ -1418,85 +1571,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 3eb9fc9a..a1246a9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,7 +62,8 @@ 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 { decodeValueType, encodeValueType } from './lib/encoder'; import { decodeMappingKey } from './lib/decodeMappingKey'; export { @@ -129,7 +130,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 @@ -179,6 +180,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). @@ -415,20 +431,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. * @@ -583,50 +585,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. * @@ -656,6 +614,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/decodeData.ts b/src/lib/decodeData.ts index 16daead9..9cbe885e 100644 --- a/src/lib/decodeData.ts +++ b/src/lib/decodeData.ts @@ -19,12 +19,15 @@ * @author Callum Grindle <@CallumGrindle> * @date 2023 */ - 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'; @@ -33,45 +36,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 tuplesValidValueTypes = [ - 'bytes2', - 'bytes4', - 'bytes8', - 'bytes16', - 'bytes32', - 'address', - ]; + const valueTypeParts = extractTupleElements(valueTypeToDecode); + const valueContentParts = extractTupleElements(valueContent); if (valueTypeParts.length !== valueContentParts.length) { throw new Error( @@ -80,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}`, ); } @@ -139,27 +136,19 @@ export const decodeTupleKeyValue = ( ): Array => { // We assume data has already been validated at this stage - let valueTypeToDecode = valueType; - - if (valueType.includes('[CompactBytesArray')) { - valueTypeToDecode = valueType.replace(COMPACT_BYTES_ARRAY_STRING, ''); - } + // Sanitize the string to keep only the tuple, if we are dealing with `CompactBytesArray` + const 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); }); 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. 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 6ff989b3..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]; @@ -356,156 +364,220 @@ const decodeStringCompactBytesArray = (compactBytesArray: string): string[] => { return stringValues; }; -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}`); - } +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(), + }; - 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 compactBytesArrayMap[type]; + } - if (value.length > 34) { - throw new Error( - `Can't convert hex value ${value} to uint128. Too many bytes. ${ - (value.length - 2) / 2 - } > 16`, - ); - } + 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 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 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); - const numberOfBytes = countNumberOfBytes(value); + // convert to an array of individual bytes + const bytesArray = hexToBytes(abiEncodedValue); - 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.`, - ); - } + // 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); - 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: (value: string[]) => encodeStringCompactBytesArray(value), - decode: (value: string) => decodeStringCompactBytesArray(value), - }, - ...returnTypesOfBytesNCompactBytesArray(), - ...returnTypesOfUintNCompactBytesArray(), + 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) { @@ -693,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`); } @@ -726,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, value: string) { - if (!valueTypeEncodingMap[type]) { - throw new Error('Could not decode valueType: "' + type + '".'); - } - - if (value === '0x') return null; +export function decodeValueType( + type: ERC725JSONSchemaValueType | string, // for tuples and CompactBytesArray + data: string, +) { + 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( diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index a609d10e..e583a000 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, @@ -46,6 +45,7 @@ import { decodeKey } from './decodeData'; describe('utils', () => { describe('encodeKey/decodeKey', () => { const testCases = [ + // test encoding an array of address { schema: { name: 'LSP3IssuedAssets[]', @@ -228,12 +228,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', () => { @@ -393,19 +409,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); }); }); @@ -569,6 +585,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 99c6680a..7024068f 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[] @@ -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; @@ -360,7 +365,7 @@ export function encodeKey( */ export function decodeKeyValue( valueContent: string, - valueType: ERC725JSONSchemaValueType, + valueType: ERC725JSONSchemaValueType | string, // string for tuples and CompactBytesArray value, name?: string, ) { @@ -745,3 +750,13 @@ export const duplicateMultiTypeERC725SchemaEntry = ( }) .flat(); }; + +/* + * `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/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[]; 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). } 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[];