diff --git a/.github/workflows/lint-test-build.yml b/.github/workflows/lint-test-build.yml index 585c2266..8ff146d9 100644 --- a/.github/workflows/lint-test-build.yml +++ b/.github/workflows/lint-test-build.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x] + node-version: [18.x, 20.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/README.md b/README.md index 790dd393..f4f7c7e6 100644 --- a/README.md +++ b/README.md @@ -41,44 +41,85 @@ If you install it on the backend side, you may need to also install [`isomorphic ## Instantiation -You need to initialise it with a [schema](https://docs.lukso.tech/tools/erc725js/schemas), a contract address and an RPC URL. +You need to initialise the ERC725 object with a [schema](https://docs.lukso.tech/tools/erc725js/schemas), a contract address, and an RPC URL. ```js -import { ERC725 } from '@erc725/erc725.js'; +const address = '0x0Dc07C77985fE31996Ed612F568eb441afe5768D'; +const RPC_URL = 'https://rpc.testnet.lukso.network'; +const config = { + ipfsGateway: 'https://YOUR-IPFS-GATEWAY/ipfs/', + gas: 20_000_000, // optional, default is 1_000_000 +}; +``` + +### TypeScript -// Part of LSP3-Profile Schema -// https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-3-Profile-Metadata.md -const schema = [ +> If you are using ES6 `import` statements in Node.js, make sure your file has a `.mjs` extension, or that your project is set up to support ES6 modules. + +```ts +import { ERC725, ERC725JSONSchema } from '@erc725/erc725.js'; + +// Part of LSP3-UniversalProfile Schema +// https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-3-UniversalProfile.md +const schemas: ERC725JSONSchema[] = [ { name: 'SupportedStandards:LSP3Profile', key: '0xeafec4d89fa9619884b600005ef83ad9559033e6e941db7d7c495acdce616347', keyType: 'Mapping', - valueContent: '0x5ef83ad9', valueType: 'bytes', + valueContent: '0x5ef83ad9', }, { name: 'LSP3Profile', key: '0x5ef83ad9559033e6e941db7d7c495acdce616347d28e90c7ce47cbfcfcad3bc5', keyType: 'Singleton', - valueContent: 'VerifiableURI', valueType: 'bytes', + valueContent: 'VerifiableURI', }, { name: 'LSP1UniversalReceiverDelegate', key: '0x0cfc51aec37c55a4d0b1a65c6255c4bf2fbdf6277f3cc0730c45b828b6db8b47', keyType: 'Singleton', - valueContent: 'Address', valueType: 'address', + valueContent: 'Address', }, ]; -const address = '0x3000783905Cc7170cCCe49a4112Deda952DDBe24'; -const RPC_URL = 'https://rpc.testnet.lukso.network'; -const config = { - ipfsGateway: 'https://2eff.lukso.dev/ipfs/', -}; +const erc725 = new ERC725(schemas, address, RPC_URL, config); +``` + +### JavaScript + +```js +import { ERC725 } require('@erc725/erc725.js'); + +// Part of LSP3-UniversalProfile Schema +// https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-3-UniversalProfile.md +const schemas = [ + { + name: 'SupportedStandards:LSP3Profile', + key: '0xeafec4d89fa9619884b600005ef83ad9559033e6e941db7d7c495acdce616347', + keyType: 'Mapping', + valueType: 'bytes', + valueContent: '0x5ef83ad9', + }, + { + name: 'LSP3Profile', + key: '0x5ef83ad9559033e6e941db7d7c495acdce616347d28e90c7ce47cbfcfcad3bc5', + keyType: 'Singleton', + valueType: 'bytes', + valueContent: 'VerifiableURI', + }, + { + name: 'LSP1UniversalReceiverDelegate', + key: '0x0cfc51aec37c55a4d0b1a65c6255c4bf2fbdf6277f3cc0730c45b828b6db8b47', + keyType: 'Singleton', + valueType: 'address', + valueContent: 'Address', + }, +]; -const myErc725 = new ERC725(schema, address, RPC_URL, config); +const erc725 = new ERC725(schemas, address, RPC_URL, config); ``` ## Usage diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index a5ecd2ae..d26b5c78 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -754,7 +754,8 @@ ERC725.encodePermissions({ ENCRYPT: false, DECRYPT: false, SIGN: false, - EXECUTE_RELAY_CALL: false + EXECUTE_RELAY_CALL: false, + ERC4337_PERMISSION: false }), // '0x0000000000000000000000000000000000000000000000000000000000000110' diff --git a/docs/getting-started.md b/docs/getting-started.md index 896338bd..5b97cf89 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -27,12 +27,27 @@ If you install it on the backend side, you may need to also install [`isomorphic ## Instantiation +You need to initialise the ERC725 object with a [schema](https://docs.lukso.tech/tools/erc725js/schemas), a contract address, and an RPC URL. + ```js -import { ERC725 } from '@erc725/erc725.js'; +const address = '0x0Dc07C77985fE31996Ed612F568eb441afe5768D'; +const RPC_URL = 'https://rpc.testnet.lukso.network'; +const config = { + ipfsGateway: 'https://YOUR-IPFS-GATEWAY/ipfs/', + gas: 20_000_000, // optional, default is 1_000_000 +}; +``` + +### TypeScript + +> If you are using ES6 `import` statements in Node.js, make sure your file has a `.mjs` extension, or that your project is set up to support ES6 modules. + +```ts +import { ERC725, ERC725JSONSchema } from '@erc725/erc725.js'; // Part of LSP3-UniversalProfile Schema // https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-3-UniversalProfile.md -const schemas = [ +const schemas: ERC725JSONSchema[] = [ { name: 'SupportedStandards:LSP3Profile', key: '0xeafec4d89fa9619884b600005ef83ad9559033e6e941db7d7c495acdce616347', @@ -56,12 +71,39 @@ const schemas = [ }, ]; -const address = '0x0Dc07C77985fE31996Ed612F568eb441afe5768D'; -const RPC_URL = 'https://rpc.testnet.lukso.network'; -const config = { - ipfsGateway: 'https://YOUR-IPFS-GATEWAY/ipfs/', - gas: 20_000_000, // optional, default is 1_000_000 -}; +const erc725 = new ERC725(schemas, address, RPC_URL, config); +``` + +### JavaScript + +```js +import { ERC725 } require('@erc725/erc725.js'); + +// Part of LSP3-UniversalProfile Schema +// https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-3-UniversalProfile.md +const schemas = [ + { + name: 'SupportedStandards:LSP3Profile', + key: '0xeafec4d89fa9619884b600005ef83ad9559033e6e941db7d7c495acdce616347', + keyType: 'Mapping', + valueType: 'bytes', + valueContent: '0x5ef83ad9', + }, + { + name: 'LSP3Profile', + key: '0x5ef83ad9559033e6e941db7d7c495acdce616347d28e90c7ce47cbfcfcad3bc5', + keyType: 'Singleton', + valueType: 'bytes', + valueContent: 'VerifiableURI', + }, + { + name: 'LSP1UniversalReceiverDelegate', + key: '0x0cfc51aec37c55a4d0b1a65c6255c4bf2fbdf6277f3cc0730c45b828b6db8b47', + keyType: 'Singleton', + valueType: 'address', + valueContent: 'Address', + }, +]; const erc725 = new ERC725(schemas, address, RPC_URL, config); ``` diff --git a/docs/schemas.md b/docs/schemas.md index 42ecde81..2f7f0b21 100644 --- a/docs/schemas.md +++ b/docs/schemas.md @@ -20,9 +20,29 @@ _A quick reference for keys used in schema definitions can be seen below_ ## Standard LSP Schemas -The most common schemas of [LUKSO Standard Proposals](https://github.com/lukso-network/LIPs/tree/main/LSPs) are available under the [`schemas/`](https://github.com/ERC725Alliance/erc725.js/tree/develop/schemas) folder. +The most common schemas of [LUKSO Standard Proposals](https://github.com/lukso-network/LIPs/tree/main/LSPs) are available to import. These are typed automatically with the Typescript type `ERC725JSONSchema[]` for when instantiating `new ERC725(...)` from Typescript projects. -Current provided LSPs are: +```ts +import { + LSP1Schema, + LSP3Schema, + LSP4Schema, + LSP4LegacySchema, + LSP5Schema, + LSP6Schema, + LSP8Schema, + LSP9Schema, + LSP10Schema, + LSP12Schema, + LSP17Schema, +} from '@erc725/erc725.js/schemas'; + +const erc725js = new ERC725(LSP12Schema); +``` + +The raw JSON schemas are also available for import from the [`schemas/`](https://github.com/ERC725Alliance/erc725.js/tree/develop/schemas) folder. + +Current provided LSPs JSON schemas are: ``` LSP1UniversalReceiverDelegate.json @@ -38,7 +58,7 @@ LSP12IssuedAssets.json LSP17ContractExtension.json ``` -You can import them from: +You can import the raw JSON as follow: ```js import LSP3 from '@erc725/erc725.js/schemas/LSP3ProfileMetadata.json'; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 3673cc85..85e19aea 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -14,6 +14,7 @@ /* eslint-disable @typescript-eslint/ban-types */ import { numberToHex, keccak256 } from 'web3-utils'; +import { arrToBufArr, bufferToHex } from 'ethereumjs-util'; import { MethodData, Encoding, Method } from '../types/Method'; @@ -92,7 +93,7 @@ export const METHODS: Record = { }, }; -export const UNKNOWN_VERIFICATION_METHOD = 'unknown'; +export const NONE_VERIFICATION_METHOD = '0x00000000'; export enum SUPPORTED_VERIFICATION_METHOD_STRINGS { KECCAK256_UTF8 = 'keccak256(utf8)', @@ -112,25 +113,36 @@ export const SUPPORTED_VERIFICATION_METHODS_LIST = Object.values( SUPPORTED_VERIFICATION_METHOD_STRINGS, ); -function keccak256Utf8(data) { - return keccak256(JSON.stringify(data)); +function keccak256Method(data: object | string | Uint8Array | null) { + if (data === null) { + return keccak256(''); + } + if (data instanceof Uint8Array) { + const buffer = bufferToHex(arrToBufArr(data)); + return keccak256(buffer); + } + if (typeof data === 'object') { + const buffer = JSON.stringify(data); + return keccak256(buffer); + } + return keccak256(data); } const KECCAK256_UTF8 = { - method: keccak256Utf8, + method: keccak256Method, name: SUPPORTED_VERIFICATION_METHOD_STRINGS.KECCAK256_UTF8, sig: SUPPORTED_VERIFICATION_METHOD_HASHES.HASH_KECCAK256_UTF8, }; const KECCAK256_BYTES = { - method: keccak256, + method: keccak256Method, name: SUPPORTED_VERIFICATION_METHOD_STRINGS.KECCAK256_BYTES, sig: SUPPORTED_VERIFICATION_METHOD_HASHES.HASH_KECCAK256_BYTES, }; export const HASH_METHODS: { [key: string]: { - method: Function; + method: (data: object | string | Uint8Array | null) => string; name: SUPPORTED_VERIFICATION_METHOD_STRINGS; sig: SUPPORTED_VERIFICATION_METHODS; }; @@ -167,6 +179,7 @@ export const LSP6_DEFAULT_PERMISSIONS = { DECRYPT : "0x0000000000000000000000000000000000000000000000000000000000100000", SIGN : "0x0000000000000000000000000000000000000000000000000000000000200000", EXECUTE_RELAY_CALL : "0x0000000000000000000000000000000000000000000000000000000000400000", + ERC4337_PERMISSION : "0x0000000000000000000000000000000000000000000000000000000000800000" }; export const LSP6_ALL_PERMISSIONS = diff --git a/src/index.test.ts b/src/index.test.ts index 6e444748..c498e50f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -24,6 +24,9 @@ import Web3 from 'web3'; import * as sinon from 'sinon'; import { hexToNumber, leftPad, numberToHex } from 'web3-utils'; +// examples of schemas to load (for testing) +import { LSP1Schema, LSP12Schema, LSP3Schema, LSP6Schema } from './schemas'; + import ERC725 from '.'; import { decodeKeyValue, @@ -68,6 +71,18 @@ describe('Running @erc725/erc725.js tests...', () => { }, /Incorrect or unsupported provider/); }); + it('should allow importing the schemas and instantiating with them', async () => { + const schemasToLoad = [ + ...LSP1Schema, + ...LSP12Schema, + ...LSP3Schema, + ...LSP6Schema, + ]; + const erc725 = new ERC725(schemasToLoad); + + assert.deepStrictEqual(erc725.options.schemas, schemasToLoad); + }); + it('should throw when calling getData without address & provider options set', async () => { const erc725 = new ERC725(mockSchema); try { @@ -341,6 +356,93 @@ describe('Running @erc725/erc725.js tests...', () => { }); }); + describe('By HttpProvider to retrieve single dynamic key with getDataBatch', () => { + const provider = new HttpProvider( + { + returnData: [ + { + key: '0x4b80742de2bf82acb36300009139def55c73c12bcda9c44f12326686e3948634', + value: + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002', + }, + ], + }, + [ERC725Y_INTERFACE_IDS['5.0']], + ); + + it('should return data even with a single BitArray key', async () => { + const erc725 = new ERC725( + [ + { + name: 'AddressPermissions:Permissions:
', + key: '0x4b80742de2bf82acb3630000
', + keyType: 'MappingWithGrouping', + valueType: 'bytes32', + valueContent: 'BitArray', + }, + ], + '0x24464DbA7e7781a21eD86133Ebe88Eb9C0762620', + provider, + ); + + const data = await erc725.getData([ + { + keyName: 'AddressPermissions:Permissions:
', + dynamicKeyParts: '0x9139def55c73c12bcda9c44f12326686e3948634', + }, + ]); + assert.deepStrictEqual(data[0], { + key: '0x4b80742de2bf82acb36300009139def55c73c12bcda9c44f12326686e3948634', + name: 'AddressPermissions:Permissions:9139def55c73c12bcda9c44f12326686e3948634', + value: + '0x0000000000000000000000000000000000000000000000000000000000000002', + }); + }); + }); + + describe('By HttpProvider to retrieve single dynamic key with getDataBatch', () => { + const provider = new HttpProvider( + { + returnData: [ + { + key: '0x6de85eaf5d982b4e5da000009139def55c73c12bcda9c44f12326686e3948634', + value: + '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001424871b3d00000000000000000000000000000000000000000000000000000000', + }, + ], + }, + [ERC725Y_INTERFACE_IDS['5.0']], + ); + + it('should return data even with a single BitArray key', async () => { + const erc725 = new ERC725( + [ + { + name: 'LSP4CreatorsMap:
', + key: '0x6de85eaf5d982b4e5da00000
', + keyType: 'Mapping', + valueType: '(bytes4,uint128)', + valueContent: '(Bytes4,Number)', + }, + ], + '0x24464DbA7e7781a21eD86133Ebe88Eb9C0762620', + provider, + ); + + const data = await erc725.getData([ + { + keyName: 'LSP4CreatorsMap:
', + dynamicKeyParts: '0x9139def55c73c12bcda9c44f12326686e3948634', + }, + ]); + assert.deepStrictEqual(data[0], { + key: '0x6de85eaf5d982b4e5da000009139def55c73c12bcda9c44f12326686e3948634', + name: 'LSP4CreatorsMap:9139def55c73c12bcda9c44f12326686e3948634', + value: ['0x24871b3d', 0], + }); + }); + }); + describe('By provider [e2e] - luksoTestnet', () => { const e2eSchema: ERC725JSONSchema[] = [ { @@ -561,15 +663,23 @@ describe('Running @erc725/erc725.js tests...', () => { const jsonString = `{"LSP3Profile":{"profileImage":"ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf","backgroundImage":"ipfs://QmZF5pxDJcB8eVvCd74rsXBFXhWL3S1XR5tty2cy1a58Ew","description":"Beautiful clothing that doesn't cost the Earth. A sustainable designer based in London Patrick works with brand partners to refocus on systemic change centred around creative education. "}}`; const fetchStub = sinon.stub(global, 'fetch'); - fetchStub.onCall(0).returns(Promise.resolve(new Response(jsonString))); - const result = await erc725.fetchData('TestJSONURL'); - fetchStub.restore(); - - assert.deepStrictEqual(result, { - key: testJSONURLSchema.key, - name: testJSONURLSchema.name, - value: JSON.parse(jsonString), - }); + try { + fetchStub.onCall(0).returns( + Promise.resolve( + new Response(jsonString, { + headers: { 'content-type': 'application/json' }, + }), + ), + ); + const result = await erc725.fetchData('TestJSONURL'); + assert.deepStrictEqual(result, { + key: testJSONURLSchema.key, + name: testJSONURLSchema.name, + value: JSON.parse(jsonString), + }); + } finally { + fetchStub.restore(); + } }); it('fetchData JSONURL with custom config.ipfsGateway', async () => { @@ -584,7 +694,7 @@ describe('Running @erc725/erc725.js tests...', () => { [contractVersion.interface], ); - const ipfsGateway = 'https://2eff.lukso.dev'; + const ipfsGateway = 'https://api.universalprofile.cloud'; const erc725 = new ERC725([testJSONURLSchema], address, provider, { ipfsGateway, @@ -593,20 +703,29 @@ describe('Running @erc725/erc725.js tests...', () => { const jsonString = `{"LSP3Profile":{"profileImage":"ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf","backgroundImage":"ipfs://QmZF5pxDJcB8eVvCd74rsXBFXhWL3S1XR5tty2cy1a58Ew","description":"Beautiful clothing that doesn't cost the Earth. A sustainable designer based in London Patrick works with brand partners to refocus on systemic change centred around creative education. "}}`; const fetchStub = sinon.stub(global, 'fetch'); - fetchStub.onCall(0).returns(Promise.resolve(new Response(jsonString))); - const result = await erc725.fetchData('TestJSONURL'); - assert.deepStrictEqual(result, { - key: testJSONURLSchema.key, - name: testJSONURLSchema.name, - value: JSON.parse(jsonString), - }); - fetchStub.restore(); + try { + fetchStub.onCall(0).returns( + Promise.resolve( + new Response(jsonString, { + headers: { 'content-type': 'application/json' }, + }), + ), + ); + const result = await erc725.fetchData('TestJSONURL'); + assert.deepStrictEqual(result, { + key: testJSONURLSchema.key, + name: testJSONURLSchema.name, + value: JSON.parse(jsonString), + }); - assert.ok( - fetchStub.calledWith( - `${ipfsGateway}/ipfs/QmbErKh3FjsAR6YjsTjHZNm6McDp6aRt82Ftcv9AJJvZbd`, // this value comes from the mockSchema - ), - ); + assert.ok( + fetchStub.calledWith( + `${ipfsGateway}/ipfs/QmbErKh3FjsAR6YjsTjHZNm6McDp6aRt82Ftcv9AJJvZbd`, // this value comes from the mockSchema + ), + ); + } finally { + fetchStub.restore(); + } }); if (contractVersion.interface === ERC725Y_INTERFACE_IDS['3.0']) { @@ -640,72 +759,77 @@ describe('Running @erc725/erc725.js tests...', () => { const jsonString = `{"LSP3Profile":{"profileImage":"ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf","backgroundImage":"ipfs://QmZF5pxDJcB8eVvCd74rsXBFXhWL3S1XR5tty2cy1a58Ew","description":"Beautiful clothing that doesn't cost the Earth. A sustainable designer based in London Patrick works with brand partners to refocus on systemic change centred around creative education. "}}`; const fetchStub = sinon.stub(global, 'fetch'); - fetchStub - .onCall(0) - .returns(Promise.resolve(new Response(jsonString))); - const result = await erc725.fetchData({ - keyName: 'JSONForAddress:
', - dynamicKeyParts: '0xcafecafecafecafecafecafecafecafecafecafe', - }); - fetchStub.restore(); - assert.deepStrictEqual(result, { - name: 'JSONForAddress:cafecafecafecafecafecafecafecafecafecafe', - key: '0x84b02f6e50a0a0819a4f0000cafecafecafecafecafecafecafecafecafecafe', - value: JSON.parse(jsonString), - }); + try { + fetchStub + .onCall(0) + .returns(Promise.resolve(new Response(jsonString))); + const result = await erc725.fetchData({ + keyName: 'JSONForAddress:
', + dynamicKeyParts: '0xcafecafecafecafecafecafecafecafecafecafe', + }); + + assert.deepStrictEqual(result, { + name: 'JSONForAddress:cafecafecafecafecafecafecafecafecafecafe', + key: '0x84b02f6e50a0a0819a4f0000cafecafecafecafecafecafecafecafecafecafe', + value: JSON.parse(jsonString), + }); + } finally { + fetchStub.restore(); + } }); } if (contractVersion.interface === ERC725Y_INTERFACE_IDS.legacy) { it('fetchData AssetURL', async () => { const fetchStub = sinon.stub(global, 'fetch'); - fetchStub - .onCall(0) - .returns(Promise.resolve(new Response(new Uint8Array(5)))); + try { + fetchStub + .onCall(0) + .returns(Promise.resolve(new Response('{"hello": "world"}'))); - const provider = new HttpProvider( - { - returnData: [ + const provider = new HttpProvider( + { + returnData: [ + { + key: '0xf18290c9b373d751e12c5ec807278267a807c35c3806255168bc48a85757ceee', + value: + '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000598019f9b1586e9b1e1681ba3ebad5ff5e6f673d3e3aa129fcdb76f92083dbc386cdde4312697066733a2f2f516d596f387967347a7a6d647532364e537674736f4b6555356f56523668326f686d6f61324378356939316d506600000000000000', + }, + + // Encoded value of: + // { + // verification: { + // method: 'keccak256(bytes)', // 0x8019f9b1 + // data: '0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec', + // }, + // url: 'ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf', + // }, + ], + }, + [contractVersion.interface], + ); + + const erc725 = new ERC725( + [ { + name: 'TestAssetURL', key: '0xf18290c9b373d751e12c5ec807278267a807c35c3806255168bc48a85757ceee', - value: - '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000598019f9b1c41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec697066733a2f2f516d596f387967347a7a6d647532364e537674736f4b6555356f56523668326f686d6f61324378356939316d506600000000000000', + keyType: 'Singleton', + valueContent: 'AssetURL', + valueType: 'bytes', }, - - // Encoded value of: - // { - // verification: { - // method: 'keccak256(bytes)', // 0x8019f9b1 - // data: '0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec', - // }, - // url: 'ipfs://QmYo8yg4zzmdu26NSvtsoKeU5oVR6h2ohmoa2Cx5i91mPf', - // }, ], - }, - [contractVersion.interface], - ); - - const erc725 = new ERC725( - [ - { - name: 'TestAssetURL', - key: '0xf18290c9b373d751e12c5ec807278267a807c35c3806255168bc48a85757ceee', - keyType: 'Singleton', - valueContent: 'AssetURL', - valueType: 'bytes', - }, - ], - address, - provider, - ); - const result = await erc725.fetchData('TestAssetURL'); - - fetchStub.restore(); - - assert.strictEqual( - Object.prototype.toString.call(result.value), - '[object Uint8Array]', - ); + address, + provider, + ); + const result = await erc725.fetchData('TestAssetURL'); + + assert.deepStrictEqual(result.value, { + hello: 'world', + }); + } finally { + fetchStub.restore(); + } }); } }); @@ -1067,6 +1191,7 @@ describe('Running @erc725/erc725.js tests...', () => { DECRYPT: true, SIGN: true, EXECUTE_RELAY_CALL: false, + ERC4337_PERMISSION: false, }, hex: '0x00000000000000000000000000000000000000000000000000000000003f3f7f', }, @@ -1095,6 +1220,7 @@ describe('Running @erc725/erc725.js tests...', () => { DECRYPT: false, SIGN: false, EXECUTE_RELAY_CALL: false, + ERC4337_PERMISSION: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000000000', }, @@ -1123,6 +1249,7 @@ describe('Running @erc725/erc725.js tests...', () => { DECRYPT: false, SIGN: true, EXECUTE_RELAY_CALL: false, + ERC4337_PERMISSION: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000200a00', }, @@ -1151,6 +1278,7 @@ describe('Running @erc725/erc725.js tests...', () => { DECRYPT: false, SIGN: false, EXECUTE_RELAY_CALL: false, + ERC4337_PERMISSION: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000040800', }, @@ -1179,6 +1307,7 @@ describe('Running @erc725/erc725.js tests...', () => { DECRYPT: false, SIGN: false, EXECUTE_RELAY_CALL: false, + ERC4337_PERMISSION: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000040004', }, @@ -1207,6 +1336,7 @@ describe('Running @erc725/erc725.js tests...', () => { DECRYPT: false, SIGN: false, EXECUTE_RELAY_CALL: false, + ERC4337_PERMISSION: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000000a00', }, @@ -1288,6 +1418,7 @@ describe('Running @erc725/erc725.js tests...', () => { DECRYPT: true, SIGN: true, EXECUTE_RELAY_CALL: true, + ERC4337_PERMISSION: true, }, ); assert.deepStrictEqual( @@ -1318,6 +1449,7 @@ describe('Running @erc725/erc725.js tests...', () => { DECRYPT: true, SIGN: true, EXECUTE_RELAY_CALL: true, + ERC4337_PERMISSION: true, }, ); }); diff --git a/src/index.ts b/src/index.ts index e7924b75..d797aa50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -252,7 +252,7 @@ export class ERC725 { keyOrKeys?: GetDataInput, ): Promise { let keyNames: Array; - + let throwException = false; if (Array.isArray(keyOrKeys)) { keyNames = keyOrKeys; } else if (!keyOrKeys) { @@ -260,6 +260,7 @@ export class ERC725 { .map((element) => element.name) .filter((key) => !isDynamicKeyName(key)); } else { + throwException = true; // If it's explicitely a single key, then we allow throwing an exception keyNames = [keyOrKeys]; } @@ -276,6 +277,7 @@ export class ERC725 { schemas, dataFromChain, this.options.ipfsGateway, + throwException, ); if ( @@ -500,6 +502,7 @@ export class ERC725 { DECRYPT: false, SIGN: false, EXECUTE_RELAY_CALL: false, + ERC4337_PERMISSION: false, }; const permissionsToTest = Object.keys(LSP6_DEFAULT_PERMISSIONS); diff --git a/src/lib/decodeData.test.ts b/src/lib/decodeData.test.ts index 22ea7ea2..65845c33 100644 --- a/src/lib/decodeData.test.ts +++ b/src/lib/decodeData.test.ts @@ -91,6 +91,11 @@ describe('decodeData', () => { value: '0x6f357c6a820464ddfac1bec070cc14a8daf04129871d458f2ca94368aae8391311af6361696670733a2f2f516d597231564a4c776572673670456f73636468564775676f3339706136727963455a4c6a7452504466573834554178', }, + { + keyName: 'JSONURLCase', + value: + '0x8019f9b1820464ddfac1bec070cc14a8daf04129871d458f2ca94368aae8391311af6361696670733a2f2f516d597231564a4c776572673670456f73636468564775676f3339706136727963455a4c6a7452504466573834554178', + }, { keyName: 'AssetURLCase', value: @@ -105,6 +110,13 @@ describe('decodeData', () => { valueType: 'bytes', valueContent: 'JSONURL', // Deprecated - We keep it for backward compatibility between v0.21.3 and v0.22.0 }, + { + name: 'JSONURLCase2', + key: '0x9136feeb09af67b63993b586ce46a43bd3456990d3fdb39d07beab9dee8d5910', + keyType: 'Singleton', + valueType: 'bytes', + valueContent: 'JSONURL', // Deprecated - We keep it for backward compatibility between v0.21.3 and v0.22.0 + }, { name: 'AssetURLCase', key: '0xbda5878fa57d8da097bf7cfd78c28e75f2c2c7b028e4e056d16d7e4b83f98081', @@ -123,6 +135,13 @@ describe('decodeData', () => { }, url: 'ifps://QmYr1VJLwerg6pEoscdhVGugo39pa6rycEZLjtRPDfW84UAx', }, + { + verification: { + method: 'keccak256(bytes)', // 0x8019f9b1 + data: '0x820464ddfac1bec070cc14a8daf04129871d458f2ca94368aae8391311af6361', + }, + url: 'ifps://QmYr1VJLwerg6pEoscdhVGugo39pa6rycEZLjtRPDfW84UAx', + }, { verification: { method: 'keccak256(bytes)', // 0x8019f9b1 @@ -206,6 +225,44 @@ describe('decodeData', () => { ]); }); + it('parses type Array correctly (even with uint256)', () => { + const decodedData = decodeData( + { + keyName: 'LSP12IssuedAssets[]', + value: [ + { + key: '0x7c8c3416d6cda87cd42c71ea1843df28ac4850354f988d55ee2eaa47b6dc05cd', + value: + '0x0000000000000000000000000000000000000000000000000000000000000002', + }, + { + key: '0x7c8c3416d6cda87cd42c71ea1843df2800000000000000000000000000000000', + value: '0xd94353d9b005b3c0a9da169b768a31c57844e490', + }, + { + key: '0x7c8c3416d6cda87cd42c71ea1843df2800000000000000000000000000000001', + value: '0xdaea594e385fc724449e3118b2db7e86dfba1826', + }, + ], + }, + [ + { + name: 'LSP12IssuedAssets[]', + key: '0x7c8c3416d6cda87cd42c71ea1843df28ac4850354f988d55ee2eaa47b6dc05cd', + keyType: 'Array', + valueContent: 'Address', + valueType: 'address', + }, + ], + ); + + expect(decodedData.name).to.eql('LSP12IssuedAssets[]'); + expect(decodedData.value).to.eql([ + '0xD94353D9B005B3c0A9Da169b768a31C57844e490', + '0xDaea594E385Fc724449E3118B2Db7E86dFBa1826', + ]); + }); + it('decodes dynamic keys', () => { const decodedData = decodeData( [ diff --git a/src/lib/decodeData.ts b/src/lib/decodeData.ts index 9cbe885e..360e35c1 100644 --- a/src/lib/decodeData.ts +++ b/src/lib/decodeData.ts @@ -150,6 +150,10 @@ export const decodeTupleKeyValue = ( // if we are dealing with `bytesN` if (regexMatch) bytesLengths.push(parseInt(regexMatch[1], 10)); + const numericMatch = valueTypePart.match(/u?int(\d+)/); + + if (numericMatch) bytesLengths.push(parseInt(numericMatch[1], 10) / 8); + if (valueTypePart === 'address') bytesLengths.push(20); }); @@ -312,37 +316,33 @@ export function decodeData( data: DecodeDataInput | DecodeDataInput[], schema: ERC725JSONSchema[], ): DecodeDataOutput | DecodeDataOutput[] { - const processDataInput = ({ - keyName, - dynamicKeyParts, - value, - }: DecodeDataInput) => { + const processDataInput = ( + { keyName, dynamicKeyParts, value }: DecodeDataInput, + throwException = true, + ) => { const isDynamic = isDynamicKeyName(keyName); - let schemaElement: ERC725JSONSchema; - if (isDynamic) { - schemaElement = getSchemaElement(schema, keyName, dynamicKeyParts); - - // NOTE: it might be confusing to use as the output will contain other keys as the ones used - // for the input - return { - key: schemaElement.key, - name: schemaElement.name, - value: decodeKey(schemaElement, value), - }; + const schemaElement: ERC725JSONSchema = isDynamic + ? getSchemaElement(schema, keyName, dynamicKeyParts) + : getSchemaElement(schema, keyName); + let decodedValue: any = null; + try { + decodedValue = decodeKey(schemaElement, value); + } catch (error) { + if (throwException) { + throw error; + } + console.error(error); } - - schemaElement = getSchemaElement(schema, keyName); - return { key: schemaElement.key, name: schemaElement.name, - value: decodeKey(schemaElement, value), + value: decodedValue, }; }; if (Array.isArray(data)) { - return data.map((dataInput) => processDataInput(dataInput)); + return data.map((dataInput) => processDataInput(dataInput, false)); } return processDataInput(data); diff --git a/src/lib/encoder.test.ts b/src/lib/encoder.test.ts index d2b5cd5b..07e40637 100644 --- a/src/lib/encoder.test.ts +++ b/src/lib/encoder.test.ts @@ -32,6 +32,7 @@ import { decodeValueContent, } from './encoder'; import { + NONE_VERIFICATION_METHOD, SUPPORTED_VERIFICATION_METHOD_HASHES, SUPPORTED_VERIFICATION_METHOD_STRINGS, } from '../constants/constants'; @@ -666,11 +667,16 @@ describe('encoder', () => { ); }); + // We do not want to throw an Exception during decode. During encode we enforce the correct sizes. + // If there is a value size exception then we should throw. + it('throws when trying to decode a bytes17 as `uint128`', () => { - expect(() => + assert.equal( decodeValueType('uint128', '0x000000000000000000000000000000ffff'), - ).to.throw( - "Can't convert hex value 0x000000000000000000000000000000ffff to uint128. Too many bytes. 17 > 16", + '0xffff', + ); + assert.throws(() => + decodeValueType('uint128', '0x0100000000000000000000000000000000'), ); }); }); @@ -924,6 +930,20 @@ describe('encoder', () => { encodedValue: '0x00006f357c6a0020027547537d35728a741470df1ccf65de10b454ca0def7c5c20b257b7b8d16168687474703a2f2f746573742e636f6d2f61737365742e676c62', }, + { + valueContent: 'VerifiableURI', // Actual content is (bytes4,URI) + decodedValue: { + verification: { + method: NONE_VERIFICATION_METHOD, + data: '0x', + }, + url: 'https://name.universal.page/', + }, + encodedValue: + '0x6f357c6a68747470733a2f2f6e616d652e756e6976657273616c2e706167652f', + reencodedValue: + '0x000000000000000068747470733a2f2f6e616d652e756e6976657273616c2e706167652f', + }, { valueContent: 'BitArray', encodedValue: @@ -952,7 +972,10 @@ describe('encoder', () => { encodeValueContent(testCase.valueContent, testCase.decodedValue); - assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + encodedValue, + testCase.reencodedValue || testCase.encodedValue, + ); assert.deepStrictEqual( encodedValue !== false ? decodeValueContent(testCase.valueContent, encodedValue) diff --git a/src/lib/encoder.ts b/src/lib/encoder.ts index 3838fda3..7092f3b6 100644 --- a/src/lib/encoder.ts +++ b/src/lib/encoder.ts @@ -49,13 +49,14 @@ import { AssetURLEncode } from '../types/encodeData'; import { SUPPORTED_VERIFICATION_METHOD_STRINGS, - UNKNOWN_VERIFICATION_METHOD, + NONE_VERIFICATION_METHOD, } from '../constants/constants'; import { getVerificationMethod, hashData, countNumberOfBytes, isValidUintSize, + countSignificantBits, } from './utils'; import { ERC725JSONSchemaValueType } from '../types/ERC725JSONSchema'; @@ -72,7 +73,7 @@ const encodeDataSourceWithHash = ( dataSource: string, ): string => { const verificationMethod = getVerificationMethod( - verification?.method || UNKNOWN_VERIFICATION_METHOD, + verification?.method || NONE_VERIFICATION_METHOD, ); return [ '0x0000', @@ -95,6 +96,9 @@ const encodeDataSourceWithHash = ( const decodeDataSourceWithHash = (value: string): URLDataWithHash => { if (value.slice(0, 6) === '0x0000') { + // DEAL with VerifiableURI + // NOTE: A JSONURL with a 0x00000000 verification method is invalid. + /* 0 1 2 3 4 5 6 7 8 12345678901234567890123456789012345678901234567890123456789012345678901234567890 @@ -119,22 +123,48 @@ const decodeDataSourceWithHash = (value: string): URLDataWithHash => { return { verification: { - method: verificationMethod?.name || UNKNOWN_VERIFICATION_METHOD, + method: verificationMethod?.name || verificationMethodSignature, data: dataHash, }, url: dataSource, }; } + // @Deprecated code here: + + // Eventually we should no longer have JSONURL, AssetURL or (bytes4,URI) + + // DEAL with JSONURL + const verificationMethodSignature = value.slice(0, 10); const verificationMethod = getVerificationMethod(verificationMethodSignature); const encodedData = value.slice(10); // Rest of data string after function hash + + try { + // Special case where JSONURL is really (bytes4,URI) as specified + // by the old version of LSP8TokenMetadataBaseURI + // Catch error in case the buffor is not convertable to utf8. + const dataSource = hexToUtf8('0x' + encodedData); // Get as URI + if (encodedData.length < 64 || /^[a-z]{2,}:[/\S]/.test(dataSource)) { + // If the verification data starts with a utf8 sequence that looks like https:/ or data: or ar:/ for example. + return { + verification: { + method: NONE_VERIFICATION_METHOD, + data: '0x', + }, + url: dataSource, + }; + } + } catch { + // ignore + } + const dataHash = '0x' + encodedData.slice(0, 64); // Get jsonHash 32 bytes const dataSource = hexToUtf8('0x' + encodedData.slice(64)); // Get remainder as URI return { verification: { - method: verificationMethod?.name || UNKNOWN_VERIFICATION_METHOD, + method: verificationMethod?.name || verificationMethodSignature, data: dataHash, }, url: dataSource, @@ -478,13 +508,19 @@ const valueTypeEncodingMap = ( } const abiEncodedValue = abiCoder.encodeParameter(type, value); + const numberOfBits = countSignificantBits(abiEncodedValue); + if (numberOfBits > (uintLength as number)) { + throw new Error( + `Can't represent value ${value} as ${type}. To many bits required ${numberOfBits} > ${uintLength}`, + ); + } + 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) => { @@ -500,11 +536,17 @@ const valueTypeEncodingMap = ( ); } - const numberOfBytes = countNumberOfBytes(value); + const numberOfBits = countSignificantBits(value); + if (numberOfBits > (uintLength as number)) { + throw new Error( + `Can't represent value ${value} as ${type}. To many bits required ${numberOfBits} > ${uintLength}`, + ); + } + 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`, + console.debug( + `Value ${value} for ${type} is too long but value contains only ${numberOfBits}. Too many bytes. ${numberOfBytes} > 16`, ); } @@ -718,7 +760,7 @@ export const valueContentEncodingMap = ( method: (method as SUPPORTED_VERIFICATION_METHOD_STRINGS) || SUPPORTED_VERIFICATION_METHOD_STRINGS.KECCAK256_UTF8, - data: hashedJson, + data: hashedJson || '0x', }, url, ); @@ -753,7 +795,7 @@ export const valueContentEncodingMap = ( }, decode: (value: string) => { if (typeof value !== 'string' || !isHex(value)) { - console.log(`Value: ${value} is not hex.`); + console.error(`Value: ${value} is not hex.`); return null; } @@ -898,7 +940,8 @@ export function decodeValueContent( return valueContent === value ? value : null; } - if (!value || value === '0x') { + if (value == null || value === '0x') { + // !value allows 0 values to become null. return null; } diff --git a/src/lib/getData.ts b/src/lib/getData.ts index 5d7cbc1e..acff7746 100644 --- a/src/lib/getData.ts +++ b/src/lib/getData.ts @@ -106,24 +106,28 @@ const getDataMultiple = async ( // Looks like it gets array even if not requested as it gets the arrays from the this.options.schemas? // eslint-disable-next-line no-restricted-syntax for (const keySchema of arraySchemas) { - const dataKeyValue = { - [keySchema.key]: { - key: keySchema.key, - value: keyValueMap[keySchema.key], - }, - }; - const arrayValues = await getArrayValues( - erc725Options, - keySchema, - dataKeyValue, - ); - - if (arrayValues && arrayValues.length > 0) { - arrayValues.push(dataKeyValue[keySchema.key]); // add the raw data array length - - schemasWithValue[ - schemasWithValue.findIndex((schema) => schema.key === keySchema.key) - ] = { ...keySchema, value: arrayValues }; + try { + const dataKeyValue = { + [keySchema.key]: { + key: keySchema.key, + value: keyValueMap[keySchema.key], + }, + }; + const arrayValues = await getArrayValues( + erc725Options, + keySchema, + dataKeyValue, + ); + + if (arrayValues && arrayValues.length > 0) { + arrayValues.push(dataKeyValue[keySchema.key]); // add the raw data array length + + schemasWithValue[ + schemasWithValue.findIndex((schema) => schema.key === keySchema.key) + ] = { ...keySchema, value: arrayValues }; + } + } catch (error) { + console.error(error); } } // ------- END ARRAY HANDLER ------- diff --git a/src/lib/getDataFromExternalSources.ts b/src/lib/getDataFromExternalSources.ts index dd0a96ec..809d58c6 100644 --- a/src/lib/getDataFromExternalSources.ts +++ b/src/lib/getDataFromExternalSources.ts @@ -19,18 +19,21 @@ * @date 2021 */ +import { arrToBufArr } from 'ethereumjs-util'; + import { DecodeDataOutput, GetDataExternalSourcesOutput, } from '../types/decodeData'; import { ERC725JSONSchema } from '../types/ERC725JSONSchema'; -import { SUPPORTED_VERIFICATION_METHOD_STRINGS } from '../constants/constants'; import { isDataAuthentic, patchIPFSUrlsIfApplicable } from './utils'; +import { URLDataWithHash } from '../types'; export const getDataFromExternalSources = ( schemas: ERC725JSONSchema[], dataFromChain: DecodeDataOutput[], ipfsGateway: string, + throwException = true, ): Promise => { const promises = dataFromChain.map(async (dataEntry) => { const schemaElement = schemas.find( @@ -51,51 +54,95 @@ export const getDataFromExternalSources = ( return dataEntry; } - // At this stage, value should be of type jsonurl, verifiableuri or asseturl - if (typeof dataEntry.value === 'string') { - console.error( - `Value of key: ${dataEntry.name} (${dataEntry.value}) is string but valueContent is: ${schemaElement.valueContent}. Expected type should be object with url key.`, - ); - return dataEntry; - } - - if (!dataEntry.value) { - return dataEntry; - } + try { + // At this stage, value should be of type jsonurl, verifiableuri or asseturl + if (typeof dataEntry.value === 'string') { + throw new Error( + `Value of key: ${dataEntry.name} (${dataEntry.value}) is string but valueContent is: ${schemaElement.valueContent}. Expected type should be object with url key.`, + ); + } - if (Array.isArray(dataEntry.value)) { - console.error( - `Value of key: ${dataEntry.name} (${dataEntry.value}) is string[] but valueContent is: ${schemaElement.valueContent}. Expected type should be object with url key.`, - ); - return dataEntry; - } + if (!dataEntry.value) { + throw new Error(`Value of key: ${dataEntry.name} is empty`); + } - const urlDataWithHash = dataEntry.value; // Type URLDataWithHash + if (Array.isArray(dataEntry.value)) { + throw new Error( + `Value of key: ${dataEntry.name} (${dataEntry.value}) is string[] but valueContent is: ${schemaElement.valueContent}. Expected type should be object with url key.`, + ); + } - let receivedData; - try { - const { url } = patchIPFSUrlsIfApplicable(urlDataWithHash, ipfsGateway); + const urlDataWithHash: URLDataWithHash = + dataEntry.value as URLDataWithHash; // Type URLDataWithHash - receivedData = await fetch(url).then(async (response) => { - if ( - urlDataWithHash.verification?.method === - SUPPORTED_VERIFICATION_METHOD_STRINGS.KECCAK256_BYTES - ) { + const { url } = patchIPFSUrlsIfApplicable( + urlDataWithHash as URLDataWithHash, + ipfsGateway, + ); + try { + if (/[=?/]$/.test(url)) { + // this URL is not verifiable and the URL ends with a / or ? or = meaning it's not a file + // and more likely to be some kind of directory or query BaseURI + return dataEntry; + } + const receivedData = await fetch(url).then(async (response) => { + if (!response.ok) { + throw new Error(response.statusText); + } return response .arrayBuffer() .then((buffer) => new Uint8Array(buffer)); - } + }); + if (receivedData.length >= 2) { + // JSON data cannot be less than 2 characters long. + try { + // - Build a string containing the first and last byte of the received data + // and try to convert it to utf8. If that succeeds then + // - check whether those could represent valid JSON data. + // - then validate the data as JSON + // - then verfiy the data against the verification method + const key = String.fromCharCode( + receivedData[0], + receivedData[receivedData.length - 1], + ); + // Currently not supported even though they could be added and can represent valid JSON. + // " " => JSON.stringify("") NOT SUPPORTED as valid JSON + // t or f and e => JSON.stringify(true) or JSON.stringify(false) NOT SUPPORTED as valid JSON + // 0-9 => JSON.stringify(0) integer or float (note .5 is not legitimate JSON) NOT SUPPORTED as valid JSON + // if (/^(\[\]|\{\}|(tf)e|\d\d)$/.test(key)) { - return response.json(); - }); - } catch (error) { - console.error(error, `GET request to ${urlDataWithHash.url} failed`); - throw error; + // Check if the beginning or end are + // { and } => JSON.stringify({...}) => pretty much 100% of our JSON will be this. + // [ and ] => JSON.stringify([...]) + if (/^(\[\]|\{\})$/.test(key)) { + const json = arrToBufArr(receivedData).toString(); + const value = JSON.parse(json); + if (isDataAuthentic(value, urlDataWithHash.verification)) { + return { ...dataEntry, value }; + } + throw new Error('result did not correctly validate'); + } + } catch { + // ignore + } + } + if (isDataAuthentic(receivedData, urlDataWithHash.verification)) { + return { ...dataEntry, value: receivedData }; + } + throw new Error('result did not correctly validate'); + } catch (error: any) { + error.message = `GET request to ${urlDataWithHash.url} (resolved as ${url}) failed: ${error.message}`; + throw error; + } + } catch (error: any) { + error.message = `Value of key: ${dataEntry.name} has an error: ${error.message}`; + if (throwException) { + throw error; + } + console.error(error); } - - return isDataAuthentic(receivedData, urlDataWithHash.verification) - ? { ...dataEntry, value: receivedData } - : { ...dataEntry, value: null }; + // Invalid data + return { ...dataEntry, value: null }; }); return Promise.all(promises); diff --git a/src/lib/provider-wrapper-utils.ts b/src/lib/provider-wrapper-utils.ts index 0a0d52a0..9b56cce3 100644 --- a/src/lib/provider-wrapper-utils.ts +++ b/src/lib/provider-wrapper-utils.ts @@ -50,7 +50,7 @@ export function decodeResult(method: Method, hexString: string) { const constructJSONRPCParams = ( address: string, method: Method, - gasInfo: number, + gasInfo?: number, methodParam?: string, ): JsonRpcEthereumProviderParamsWithLatest => { const data = methodParam @@ -61,7 +61,7 @@ const constructJSONRPCParams = ( { to: address, value: METHODS[method].value, - gas: numberToHex(gasInfo), + ...(gasInfo ? { gas: numberToHex(gasInfo) } : {}), data, }, 'latest', @@ -71,7 +71,7 @@ const constructJSONRPCParams = ( export function constructJSONRPC( address: string, method: Method, - gasInfo: number, + gasInfo?: number, methodParam?: string, ): JsonRpc { idCount += 1; diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 89b6056b..8a8ee3f2 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -38,6 +38,7 @@ import { encodeTupleKeyValue, duplicateMultiTypeERC725SchemaEntry, splitMultiDynamicKeyNamePart, + countSignificantBits, } from './utils'; import { isDynamicKeyName } from './encodeKeyName'; import { decodeKey } from './decodeData'; @@ -252,6 +253,26 @@ describe('utils', () => { }); }); + describe('count bits', () => { + const testCases = [ + { + value: '0x00', + result: 0, + }, + { + value: '0x01', + result: 1, + }, + { value: '0x1000', result: 13 }, + { value: '0x000f', result: 4 }, + ]; + testCases.forEach(({ value, result }) => { + it(`should count the number of bits in ${value}`, () => { + assert.equal(countSignificantBits(value), result); + }); + }); + }); + describe('encodeKeyValue/decodeKeyValue', () => { const testCases = [ { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d3230b13..132a793f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -21,13 +21,13 @@ import { checkAddressChecksum, + hexToBytes, isAddress, leftPad, numberToHex, padLeft, stripHexPrefix, } from 'web3-utils'; -import { arrToBufArr } from 'ethereumjs-util'; import { URLDataToEncode, @@ -504,18 +504,11 @@ export function isDataAuthentic( data: string | Uint8Array, options: Verification, ): boolean { - let dataHash: string; - if (!options || !options.method) { return true; } - if (data instanceof Uint8Array) { - dataHash = hashData(arrToBufArr(data), options.method); - } else { - dataHash = hashData(data, options.method); - } - + const dataHash = hashData(data, options.method); if (dataHash !== options.data) { console.error( `Hash mismatch, returned JSON hash ("${dataHash}") is different from expected hash: "${options.method}"`, @@ -588,6 +581,22 @@ export function countNumberOfBytes(data: string) { return stripHexPrefix(data).length / 2; } +export function countSignificantBits(data: string) { + const bytes = hexToBytes(data); + for (let i = 0; i < bytes.length; i++) { + if (bytes[i] !== 0) { + return ( + (bytes.length - i - 1) * 8 + // The number of bits with non-zero values from the right. + (32 - Math.clz32(bytes[i])) // The number of bits from the right of the current byte. + // The docs for Math.clz32 are: + // Returns the number of leading zero bits in the 32-bit binary representation of a number. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 + ); + } + } + return 0; +} + /** * Given an input string which can define dynamic types, will return an array with all types * In: diff --git a/src/provider/providerWrapper.ts b/src/provider/providerWrapper.ts index a3f235d1..9ec748e4 100644 --- a/src/provider/providerWrapper.ts +++ b/src/provider/providerWrapper.ts @@ -158,7 +158,7 @@ export class ProviderWrapper { constructJSONRPC( address, Method.IS_VALID_SIGNATURE, - this.gas, + undefined, // this.gas, encodedParams, ), ); @@ -183,7 +183,7 @@ export class ProviderWrapper { constructJSONRPC( address, Method.IS_VALID_SIGNATURE, - this.gas, + undefined, // this.gas, encodedParams, ), ]); @@ -245,7 +245,7 @@ export class ProviderWrapper { constructJSONRPC( address, method, - this.gas, + undefined, // this.gas, abiCoder.encodeParameter('bytes32[]', keyHashes), ), ); @@ -262,7 +262,7 @@ export class ProviderWrapper { constructJSONRPC( address, method, - this.gas, + undefined, // this.gas, abiCoder.encodeParameter('bytes32[]', keyHashes), ), ]; diff --git a/src/schemas/index.ts b/src/schemas/index.ts index eb9e5154..0011630c 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,26 +1,42 @@ import { ERC725JSONSchema } from '../types/ERC725JSONSchema'; -import LSP1UniversalReceiverDelegate from '../../schemas/LSP1UniversalReceiverDelegate.json'; -import LSP3Profile from '../../schemas/LSP3ProfileMetadata.json'; -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'; +import LSP1JSONSchema from '../../schemas/LSP1UniversalReceiverDelegate.json'; +import LSP3JSONSchema from '../../schemas/LSP3ProfileMetadata.json'; +import LSP4JSONSchema from '../../schemas/LSP4DigitalAsset.json'; +import LSP4LegacyJSONSchema from '../../schemas/LSP4DigitalAssetLegacy.json'; +import LSP5JSONSchema from '../../schemas/LSP5ReceivedAssets.json'; +import LSP6JSONSchema from '../../schemas/LSP6KeyManager.json'; +import LSP8JSONSchema from '../../schemas/LSP8IdentifiableDigitalAsset.json'; +import LSP9JSONSchema from '../../schemas/LSP9Vault.json'; +import LSP10JSONSchema from '../../schemas/LSP10ReceivedVaults.json'; +import LSP12JSONSchema from '../../schemas/LSP12IssuedAssets.json'; +import LSP17JSONSchema from '../../schemas/LSP17ContractExtension.json'; -export default LSP1UniversalReceiverDelegate.concat( - LSP3Profile, - LSP4DigitalAssetLegacy, - LSP4DigitalAsset, - LSP5ReceivedAssets, - LSP6KeyManager, - LSP8IdentifiableDigitalAsset, - LSP9Vault, - LSP10ReceivedVaults, - LSP12IssuedAssets, - LSP17ContractExtension, -) as ERC725JSONSchema[]; +type schemaType = ERC725JSONSchema[]; + +export const LSP1Schema: schemaType = LSP1JSONSchema as schemaType; +export const LSP3Schema: schemaType = LSP3JSONSchema as schemaType; +export const LSP4Schema: schemaType = LSP4JSONSchema as schemaType; +export const LSP4LegacySchema: schemaType = LSP4LegacyJSONSchema as schemaType; +export const LSP5Schema: schemaType = LSP5JSONSchema as schemaType; +export const LSP6Schema: schemaType = LSP6JSONSchema as schemaType; +export const LSP8Schema: schemaType = LSP8JSONSchema as schemaType; +export const LSP9Schema: schemaType = LSP9JSONSchema as schemaType; +export const LSP10Schema: schemaType = LSP10JSONSchema as schemaType; +export const LSP12Schema: schemaType = LSP12JSONSchema as schemaType; +export const LSP17Schema: schemaType = LSP17JSONSchema as schemaType; + +const AllSchemas = LSP1Schema.concat( + LSP3Schema, + LSP4Schema, + LSP4LegacySchema, + LSP5Schema, + LSP6Schema, + LSP8Schema, + LSP9Schema, + LSP10Schema, + LSP12Schema, + LSP17Schema, +); + +export default AllSchemas; diff --git a/src/types/ERC725JSONSchema.ts b/src/types/ERC725JSONSchema.ts index e1b6b661..5eb0eced 100644 --- a/src/types/ERC725JSONSchema.ts +++ b/src/types/ERC725JSONSchema.ts @@ -159,7 +159,7 @@ export function isValidValueType( export interface ERC725JSONSchema { name: string; // Describes the name of the key, SHOULD compromise of the Standards name + sub type. e.g: LSP2Name 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. + keyType: ERC725JSONSchemaKeyType | string; // 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 | 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/JsonRpc.ts b/src/types/JsonRpc.ts index 13d9a801..96f9c6af 100644 --- a/src/types/JsonRpc.ts +++ b/src/types/JsonRpc.ts @@ -1,6 +1,6 @@ interface JsonRpcEthereumProviderParams { to: string; - gas: string; + gas?: string; value: string; data; } diff --git a/src/types/Method.ts b/src/types/Method.ts index 1c794a67..681aee85 100644 --- a/src/types/Method.ts +++ b/src/types/Method.ts @@ -47,4 +47,5 @@ export interface Permissions { DECRYPT?: boolean; SIGN?: boolean; EXECUTE_RELAY_CALL?: boolean; + ERC4337_PERMISSION?: boolean; } diff --git a/src/types/decodeData.ts b/src/types/decodeData.ts index e66e5ae6..b0e32342 100644 --- a/src/types/decodeData.ts +++ b/src/types/decodeData.ts @@ -14,8 +14,10 @@ export interface DecodeDataInput extends DataInput { value: string | { key: string; value: string | null }[]; } +export type Data = string | number | boolean | null; + export interface DecodeDataOutput { - value: string | string[] | URLDataWithHash | null; + value: Data | Data[] | URLDataWithHash | null; name: string; key: string; }