diff --git a/docs/classes/ERC725.md b/docs/classes/ERC725.md index 79383cd7..06ca8d4d 100644 --- a/docs/classes/ERC725.md +++ b/docs/classes/ERC725.md @@ -275,6 +275,23 @@ When encoding JSON, it is possible to pass in the JSON object and the URL where ::: +:::info + +When encoding some values using specific `string` or `bytesN` as `valueType`, if the data passed is a non-hex value, _erc725.js_ will convert the value +to its utf8-hex representation for you. For instance: + +- If `valueType` is `string` and you provide a `number` as input. + +_Example: input `42` --> will encode as `0x3432` (utf-8 hex code for `4` = `0x34`, for `2` = `0x32`)._ + +- If `valueType` is `bytes32` or `bytes4`, it will convert as follow: + +_Example 1: input `week` encoded as `bytes4` --> will encode as `0x7765656b`._ + +_Example 2: input `1122334455` encoded as `bytes4` --> will encode as `0x42e576f7`._ + +::: + #### Parameters ##### 1. `data` - Array of Objects diff --git a/src/lib/decodeMappingKey.ts b/src/lib/decodeMappingKey.ts index 24161a12..4071e2c9 100644 --- a/src/lib/decodeMappingKey.ts +++ b/src/lib/decodeMappingKey.ts @@ -23,10 +23,6 @@ import { decodeValueType } from './encoder'; import { ERC725JSONSchema } from '../types/ERC725JSONSchema'; import { DynamicKeyPart } from '../types/dynamicKeys'; -function make32BytesLong(s: string): string { - return padLeft(s, 64); -} - function isDynamicKeyPart(keyPartName: string): boolean { return ( keyPartName.slice(0, 1) === '<' && @@ -59,7 +55,14 @@ function decodeKeyPart( ? 0 : encodedKeyPart.length - bytesLength; decodedKey = encodedKeyPart.slice(sliceFrom); - } else decodedKey = decodeValueType(type, make32BytesLong(encodedKeyPart)); + } else if (type === 'address') { + // this is required if the 2nd word is an address in a MappingWithGrouping + const leftPaddedAddress = padLeft('0x' + encodedKeyPart, 40); + + decodedKey = decodeValueType(type, leftPaddedAddress); + } else { + decodedKey = decodeValueType(type, encodedKeyPart); + } return { type, value: decodedKey }; } diff --git a/src/lib/encoder.test.ts b/src/lib/encoder.test.ts index 51b8072d..fb70250a 100644 --- a/src/lib/encoder.test.ts +++ b/src/lib/encoder.test.ts @@ -17,7 +17,7 @@ import { expect } from 'chai'; import assert from 'assert'; -import { keccak256, utf8ToHex, stripHexPrefix } from 'web3-utils'; +import { keccak256, utf8ToHex, stripHexPrefix, toBN, toHex } from 'web3-utils'; import { valueContentEncodingMap, encodeValueType, @@ -33,329 +33,783 @@ import { JSONURLDataToEncode, URLDataWithHash } from '../types'; describe('encoder', () => { describe('valueType', () => { - const testCases = [ - { - valueType: 'string', - decodedValue: 'Hello I am a string', - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001348656c6c6f204920616d206120737472696e6700000000000000000000000000', - }, - { - valueType: 'address', - decodedValue: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - encodedValue: - '0x000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7', - }, - { - valueType: 'uint128', - decodedValue: 11, - encodedValue: '0x0000000000000000000000000000000b', - }, - { - valueType: 'uint256', - decodedValue: '1337', - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000539', - }, - { - valueType: 'bytes32', - decodedValue: - '0x1337000000000000000000000000000000000000000000000000000000000000', - encodedValue: - '0x1337000000000000000000000000000000000000000000000000000000000000', - }, - { - valueType: 'bytes4', - decodedValue: '0x13370000', - encodedValue: - '0x1337000000000000000000000000000000000000000000000000000000000000', - }, - { - valueType: 'bytes', - decodedValue: - '0x0000000000000000000000000000000000000000000000000000000000001337', - encodedValue: - '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000001337', - }, - { - valueType: 'string[]', - decodedValue: ['a', 'b'], - encodedValue: - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016200000000000000000000000000000000000000000000000000000000000000', - }, - { - valueType: 'address[]', - decodedValue: [ - '0x68114e23B500Cdb63A5B6c9006f3acB0325AD0CC', - '0x7466e40FEF4978394A07C9124ad4aD1A374b9465', - ], - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000068114e23b500cdb63a5b6c9006f3acb0325ad0cc0000000000000000000000007466e40fef4978394a07c9124ad4ad1a374b9465', - }, - { - valueType: 'uint256[]', - decodedValue: ['1', '99'], - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000063', - }, - { - valueType: 'bytes32[]', - decodedValue: [ - '0x1337000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000061626364', - ], - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000213370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000061626364', - }, - { - valueType: 'bytes4[]', - decodedValue: ['0x12345678', '0x87654321'], - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000212345678000000000000000000000000000000000000000000000000000000008765432100000000000000000000000000000000000000000000000000000000', - }, - { - valueType: 'bytes[]', - decodedValue: [ - '0x0000000000000000000000000000000000000000000000000000000000001337', - '0x00000000000000000000000000000000000000000000000000000000000054ef', - ], - encodedValue: - '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000001337000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000054ef', - }, - { - valueType: 'bytes[CompactBytesArray]', - decodedValue: [ - '0xaabb', - '0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', - '0xbeefbeefbeefbeefbeef', - ], - encodedValue: - '0x0002aabb0020cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe000abeefbeefbeefbeefbeef', - }, - { - valueType: 'bytes[CompactBytesArray]', - decodedValue: [`0x${'cafe'.repeat(256)}`, `0x${'beef'.repeat(250)}`], - encodedValue: `0x0200${'cafe'.repeat(256)}01f4${'beef'.repeat(250)}`, - }, - { - valueType: 'string[CompactBytesArray]', - decodedValue: [ - 'one random string', - 'bring back my coke', - 'Diagon Alley', - ], - encodedValue: `0x0011${stripHexPrefix( - utf8ToHex('one random string'), - )}0012${stripHexPrefix( - utf8ToHex('bring back my coke'), - )}000c${stripHexPrefix(utf8ToHex('Diagon Alley'))}`, - }, - { - valueType: 'uint8[CompactBytesArray]', - decodedValue: [1, 43, 73, 255], - encodedValue: '0x00010100012b0001490001ff', - }, - { - valueType: 'bytes4[CompactBytesArray]', - decodedValue: ['0xe6520726', '0x272696e6', '0x72062616', '0xab7f11e3'], - encodedValue: '0x0004e65207260004272696e60004720626160004ab7f11e3', - }, - { - valueType: 'bool', - decodedValue: true, - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000001', - }, - { - valueType: 'bool', - decodedValue: false, - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000000', - }, - { - valueType: 'boolean', // allow to specify "boolean" - decodedValue: true, - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000001', - }, - { - valueType: 'boolean', // allow to specify "boolean" - decodedValue: false, - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000000', - }, - { - valueType: 'bool[]', - decodedValue: [true, false, true], - encodedValue: - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', - }, - { - valueType: 'bool[]', - decodedValue: [false, false, true, false, false], - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - }, - { - valueType: 'boolean[]', // allow to specify "boolean" - decodedValue: [true], - encodedValue: - '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', - }, - { - valueType: 'boolean[]', // allow to specify "boolean" - decodedValue: [false, false], - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - }, - ]; + describe('`bool`/`boolean` type', () => { + const validTestCases = [ + { + valueType: 'bool', + decodedValue: true, + encodedValue: '0x01', + }, + { + valueType: 'bool', + decodedValue: false, + encodedValue: '0x00', + }, + { + valueType: 'boolean', // allow to specify "boolean" + decodedValue: true, + encodedValue: '0x01', + }, + { + valueType: 'boolean', // allow to specify "boolean" + decodedValue: false, + encodedValue: '0x00', + }, + ]; - testCases.forEach((testCase) => { - it(`encodes/decodes: ${testCase.valueType}`, () => { - const encodedValue = encodeValueType( - testCase.valueType, - testCase.decodedValue, - ); + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); - assert.deepStrictEqual(encodedValue, testCase.encodedValue); - assert.deepStrictEqual( - decodeValueType(testCase.valueType, encodedValue), - testCase.decodedValue, - ); + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); }); }); - it('throws when trying to encode a string as `uint128`', () => { - assert.throws(() => encodeValueType('uint128', 'helloWorld')); + describe('`bytes4` type', () => { + const validTestCases = [ + { + valueType: 'bytes4', + decodedValue: '0x13370000', + encodedValue: '0x13370000', + }, + { + valueType: 'bytes4', + decodedValue: '0xcafecafe', + encodedValue: '0xcafecafe', + }, + ]; + + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); + + const errorEncodingTestCases = [ + { + valueType: 'bytes4', + input: '0x000000000001337', // more than 4 bytes + }, + { + valueType: 'bytes4', + input: '0xcafecafecafecafe', // more than 4 bytes + }, + { + valueType: 'bytes4', + input: 'hello there', // string input that converts to more than 4 bytes in hex + }, + { + valueType: 'bytes4', + input: 2 ** (8 * 4), // max number (`uint32`), does not fit in 4 bytes (= 0x0100000000) + }, + ]; + + errorEncodingTestCases.forEach((testCase) => { + it(`should throw when trying to encode ${testCase.input} as ${testCase.valueType}`, async () => { + assert.throws(() => + encodeValueType(testCase.valueType, testCase.input), + ); + }); + }); + + // these cases are not symetric. The input is converted + encoded. + // When decoding, we do not get the same input back, but its bytes4 hex representation + const oneWayEncodingTestCases = [ + { + valueType: 'bytes4', + input: 'week', // 4 letter word (= 4 bytes), + encodedValue: '0x7765656b', // utf8-encoded characters + decodedValue: '0x7765656b', + }, + { + valueType: 'bytes4', + input: 1122334455, + encodedValue: '0x42e576f7', // number converted to hex + right padded + decodedValue: '0x42e576f7', + }, + ]; + + oneWayEncodingTestCases.forEach((testCase) => { + it(`encodes one way \`input\` = ${testCase.input} as ${testCase.valueType}, but does not decode back as the same input`, async () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.input, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); + + // these cases are not symetric and right pad the value + const rightPaddedTestCases = [ + { + valueType: 'bytes4', + input: '0xf00d', + encodedValue: '0xf00d0000', // pad on the right with 2x 0x00 bytes + decodedValue: '0xf00d0000', + }, + { + valueType: 'bytes4', + input: 'yes', // convert to utf8 hex + pad on the right with 1x 0x00 byte + encodedValue: '0x79657300', + decodedValue: '0x79657300', + }, + ]; + + rightPaddedTestCases.forEach((testCase) => { + it(`encodes + right pad \`input\` = ${testCase.input} as ${testCase.valueType} padded on the right with \`00\`s`, async () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.input, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); }); - it('throws when trying to encode a bytes17 as `uint128`', () => { - assert.throws(() => - encodeValueType('uint128', '340282366920938463463374607431768211456'), - ); - assert.throws(() => - encodeValueType('uint128', '0x0100000000000000000000000000000000'), - ); + describe('`bytes32` type', () => { + const validTestCases = [ + { + valueType: 'bytes32', + decodedValue: + '0x1337000000000000000000000000000000000000000000000000000000000000', + encodedValue: + '0x1337000000000000000000000000000000000000000000000000000000000000', + }, + { + valueType: 'bytes32', + decodedValue: + '0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + encodedValue: + '0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + }, + ]; + + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); + + const errorEncodingTestCases = [ + { + valueType: 'bytes32', + // too many bytes (= 40 bytes) + input: + '0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + }, + { + valueType: 'bytes32', + // more than 32 characters, does not fit + input: 'This is a very long sentence that is more than 32 bytes.', + }, + { + valueType: 'bytes32', + // over the max uint256 allowed, does not fit in 32 bytes + input: toHex( + toBN( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + + 1, + ), + ), + }, + ]; + + errorEncodingTestCases.forEach((testCase) => { + it(`should throw when trying to encode ${testCase.input} as ${testCase.valueType}`, async () => { + assert.throws(() => + encodeValueType(testCase.valueType, testCase.input), + ); + }); + }); + + const oneWayEncodingTestCases = [ + { + valueType: 'bytes32', + input: 'This sentence is 32 bytes long !', + decodedValue: + '0x546869732073656e74656e6365206973203332206279746573206c6f6e672021', + encodedValue: + '0x546869732073656e74656e6365206973203332206279746573206c6f6e672021', + }, + { + valueType: 'bytes32', + input: 12345, + decodedValue: + '0x3039000000000000000000000000000000000000000000000000000000000000', + encodedValue: + '0x3039000000000000000000000000000000000000000000000000000000000000', + }, + ]; + + oneWayEncodingTestCases.forEach((testCase) => { + it(`encodes one way \`input\` = ${testCase.input} as ${testCase.valueType}, but does not decode back as the same input`, async () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.input, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); + + // these cases are not symetric and right pad the value + const rightPaddedTestCases = [ + { + valueType: 'bytes32', + input: '0xcafecafe', + decodedValue: + '0xcafecafe00000000000000000000000000000000000000000000000000000000', + encodedValue: + '0xcafecafe00000000000000000000000000000000000000000000000000000000', + }, + { + valueType: 'bytes32', + input: 'hello world!', + decodedValue: + '0x68656c6c6f20776f726c64210000000000000000000000000000000000000000', + encodedValue: + '0x68656c6c6f20776f726c64210000000000000000000000000000000000000000', + }, + ]; + + rightPaddedTestCases.forEach((testCase) => { + it(`encodes + right pad \`input\` = ${testCase.input} as ${testCase.valueType} padded on the right with \`00\`s`, async () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.input, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); }); - it('throws when trying to decode a bytes17 as `uint128`', () => { - expect(() => - decodeValueType('uint128', '0x000000000000000000000000000000ffff'), - ).to.throw( - "Can't convert hex value 0x000000000000000000000000000000ffff to uint128. Too many bytes. 17 > 16", - ); + describe('`uint128` type', () => { + const validTestCases = [ + { + valueType: 'uint128', + decodedValue: 11, + encodedValue: '0x0000000000000000000000000000000b', + }, + ]; + + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); }); - describe('when encoding bytes[CompactBytesArray]', () => { - it('should encode `0x` elements as `0x0000`', async () => { - const testCase = { - valueType: 'bytes[CompactBytesArray]', - decodedValue: ['0xaabb', '0x', '0x', '0xbeefbeefbeefbeefbeef'], - encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef', - }; + describe('`uint256` type', () => { + const validTestCases = [ + { + valueType: 'uint256', + decodedValue: 1337, + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000539', + }, + ]; - const encodedValue = encodeValueType( - testCase.valueType, - testCase.decodedValue, - ); - assert.deepStrictEqual(encodedValue, testCase.encodedValue); + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); }); + }); + + describe('`string` type', () => { + const validTestCases = [ + { + valueType: 'string', + decodedValue: 'Hello I am a string', + encodedValue: '0x48656c6c6f204920616d206120737472696e67', + }, + ]; + + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); - it("should encode '' (empty strings) elements as `0x0000`", async () => { + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); + + it('should encode each letter in a number as a utf8 character, and decode it back as a string', () => { const testCase = { - valueType: 'bytes[CompactBytesArray]', - decodedValue: ['0xaabb', '', '', '0xbeefbeefbeefbeefbeef'], - encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef', + valueType: 'string', + decodedValue: 12345, // encode each letter as a utf8 hex + encodedValue: '0x3132333435', }; const encodedValue = encodeValueType( testCase.valueType, testCase.decodedValue, ); + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + `${testCase.decodedValue}`, + ); }); + }); - it('should throw when trying to encode a array that contains non hex string as `bytes[CompactBytesArray]`', async () => { - expect(() => { - encodeValueType('bytes[CompactBytesArray]', [ - 'some random string', - 'another random strings', - '0xaabbccdd', - ]); - }).to.throw( - "Couldn't encode bytes[CompactBytesArray], value at index 0 is not hex", - ); + describe('`address` type', () => { + const validTestCases = [ + { + valueType: 'address', + // should decode as a checksummed address + decodedValue: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + encodedValue: '0xdac17f958d2ee523a2206206994597c13d831ec7', + }, + ]; + + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); }); - it('should throw when trying to encode a `bytes[CompactBytesArray]` with a bytes length bigger than 65_535', async () => { - expect(() => { - encodeValueType('bytes[CompactBytesArray]', [ - '0x' + 'ab'.repeat(66_0000), - ]); - }).to.throw( - "Couldn't encode bytes[CompactBytesArray], value at index 0 exceeds 65_535 bytes", - ); + const errorEncodingTestCases = [ + { + valueType: 'address', + input: '0x388C818CA8B9251b3931', // less than 20 bytes + }, + { + valueType: 'address', + input: '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326818CA8B92', // more than 20 bytes + }, + ]; + + errorEncodingTestCases.forEach((testCase) => { + it(`should throw when trying to encode ${testCase.input} as ${testCase.valueType}`, async () => { + assert.throws(() => + encodeValueType(testCase.valueType, testCase.input), + ); + }); }); }); - describe('when encoding uintN[CompactBytesArray]', () => { - it('should throw if trying to encode a value that exceeds the maximal lenght of bytes for this type', async () => { - expect(() => { - encodeValueType('uint8[CompactBytesArray]', [15, 178, 266]); - }).to.throw('Hex uint8 value at index 2 does not fit in 1 bytes'); + describe('`bytes` type', () => { + const validTestCases = [ + { + valueType: 'bytes', + decodedValue: + '0x0000000000000000000000000000000000000000000000000000000000001337', + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000001337', + }, + { + valueType: 'bytes', + decodedValue: '0xaabbccddeeff1122334455', + encodedValue: '0xaabbccddeeff1122334455', + }, + { + valueType: 'bytes', + decodedValue: '0x1337', + encodedValue: '0x1337', + }, + ]; + + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); }); + }); - it('should throw if trying to decode a value that exceeds the maximal lenght of bytes for this type', async () => { - expect(() => { - decodeValueType( - 'uint8[CompactBytesArray]', - '0x00010100012b00014900020100', + describe('arrays `[]` of static types', () => { + const validTestCases = [ + { + valueType: 'string[]', + decodedValue: ['a', 'b'], + encodedValue: + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016200000000000000000000000000000000000000000000000000000000000000', + }, + { + valueType: 'address[]', + decodedValue: [ + '0x68114e23B500Cdb63A5B6c9006f3acB0325AD0CC', + '0x7466e40FEF4978394A07C9124ad4aD1A374b9465', + ], + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000068114e23b500cdb63a5b6c9006f3acb0325ad0cc0000000000000000000000007466e40fef4978394a07c9124ad4ad1a374b9465', + }, + { + valueType: 'uint256[]', + decodedValue: ['1', '99'], // TODO: return them as an array of `number` type, not an array of `string` + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000063', + }, + { + valueType: 'bytes32[]', + decodedValue: [ + '0x1337000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000061626364', + ], + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000213370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000061626364', + }, + { + valueType: 'bytes4[]', + decodedValue: ['0x12345678', '0x87654321'], + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000212345678000000000000000000000000000000000000000000000000000000008765432100000000000000000000000000000000000000000000000000000000', + }, + { + valueType: 'bytes[]', + decodedValue: [ + '0x0000000000000000000000000000000000000000000000000000000000001337', + '0x00000000000000000000000000000000000000000000000000000000000054ef', + ], + encodedValue: + '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000001337000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000054ef', + }, + { + valueType: 'bool[]', + decodedValue: [true, false, true], + encodedValue: + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + }, + { + valueType: 'bool[]', + decodedValue: [false, false, true, false, false], + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + }, + { + valueType: 'boolean[]', // allow to specify "boolean" + decodedValue: [true], + encodedValue: + '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', + }, + { + valueType: 'boolean[]', // allow to specify "boolean" + decodedValue: [false, false], + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + }, + ]; + + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); + }); + }); + + describe('when encoding a value that exceeds the maximal lenght of bytes than its type', () => { + const validTestCases = [ + { + valueType: 'bytes32', + decodedValue: + '0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + encodedValue: + '0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + }, + ]; + + validTestCases.forEach((testCase) => { + it('should throw', async () => { + assert.throws(() => + encodeValueType(testCase.valueType, testCase.decodedValue), ); - }).to.throw('Hex uint8 value at index 3 does not fit in 1 bytes'); + }); + }); + }); + + describe('when encoding/decoding a value that is not a number as a `uint128`', () => { + it('throws when trying to encode a string as `uint128`', () => { + assert.throws(() => encodeValueType('uint128', 'helloWorld')); + }); + + it('throws when trying to encode a bytes17 as `uint128`', () => { + assert.throws(() => + encodeValueType('uint128', '340282366920938463463374607431768211456'), + ); + assert.throws(() => + encodeValueType('uint128', '0x0100000000000000000000000000000000'), + ); + }); + + it('throws when trying to decode a bytes17 as `uint128`', () => { + expect(() => + decodeValueType('uint128', '0x000000000000000000000000000000ffff'), + ).to.throw( + "Can't convert hex value 0x000000000000000000000000000000ffff to uint128. Too many bytes. 17 > 16", + ); }); }); - describe('when encoding bytesN[CompactBytesArray]', () => { - it('should throw if trying to encode a value that exceeds the maximal lenght of bytes for this type', async () => { - expect(() => { - encodeValueType('bytes4[CompactBytesArray]', [ + describe('`type[CompactBytesArray]` (of static types)', () => { + const validTestCases = [ + { + valueType: 'bytes[CompactBytesArray]', + decodedValue: [ + '0xaabb', + '0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + '0xbeefbeefbeefbeefbeef', + ], + encodedValue: + '0x0002aabb0020cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe000abeefbeefbeefbeefbeef', + }, + { + valueType: 'bytes[CompactBytesArray]', + decodedValue: [`0x${'cafe'.repeat(256)}`, `0x${'beef'.repeat(250)}`], + encodedValue: `0x0200${'cafe'.repeat(256)}01f4${'beef'.repeat(250)}`, + }, + { + valueType: 'string[CompactBytesArray]', + decodedValue: [ + 'one random string', + 'bring back my coke', + 'Diagon Alley', + ], + encodedValue: `0x0011${stripHexPrefix( + utf8ToHex('one random string'), + )}0012${stripHexPrefix( + utf8ToHex('bring back my coke'), + )}000c${stripHexPrefix(utf8ToHex('Diagon Alley'))}`, + }, + { + valueType: 'uint8[CompactBytesArray]', + decodedValue: [1, 43, 73, 255], + encodedValue: '0x00010100012b0001490001ff', + }, + { + valueType: 'bytes4[CompactBytesArray]', + decodedValue: [ '0xe6520726', '0x272696e6', '0x72062616', - '0xab7f11e3aabbcc', - ]); - }).to.throw('Hex bytes4 value at index 3 does not fit in 4 bytes'); + '0xab7f11e3', + ], + encodedValue: '0x0004e65207260004272696e60004720626160004ab7f11e3', + }, + ]; + + validTestCases.forEach((testCase) => { + it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + assert.deepStrictEqual( + decodeValueType(testCase.valueType, encodedValue), + testCase.decodedValue, + ); + }); }); - it('should throw if trying to decode a value that exceeds the maximal lenght of bytes for this type', async () => { - expect(() => { - decodeValueType( - 'bytes4[CompactBytesArray]', - '0x0004e65207260004272696e60004720626160007ab7f11e3aabbcc', + describe('when encoding bytes[CompactBytesArray]', () => { + it('should encode `0x` elements as `0x0000`', async () => { + const testCase = { + valueType: 'bytes[CompactBytesArray]', + decodedValue: ['0xaabb', '0x', '0x', '0xbeefbeefbeefbeefbeef'], + encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef', + }; + + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + }); + + it("should encode '' (empty strings) elements as `0x0000`", async () => { + const testCase = { + valueType: 'bytes[CompactBytesArray]', + decodedValue: ['0xaabb', '', '', '0xbeefbeefbeefbeefbeef'], + encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef', + }; + + const encodedValue = encodeValueType( + testCase.valueType, + testCase.decodedValue, + ); + assert.deepStrictEqual(encodedValue, testCase.encodedValue); + }); + + it('should throw when trying to encode a array that contains non hex string as `bytes[CompactBytesArray]`', async () => { + expect(() => { + encodeValueType('bytes[CompactBytesArray]', [ + 'some random string', + 'another random strings', + '0xaabbccdd', + ]); + }).to.throw( + "Couldn't encode bytes[CompactBytesArray], value at index 0 is not hex", + ); + }); + + it('should throw when trying to encode a `bytes[CompactBytesArray]` with a bytes length bigger than 65_535', async () => { + expect(() => { + encodeValueType('bytes[CompactBytesArray]', [ + '0x' + 'ab'.repeat(66_0000), + ]); + }).to.throw( + "Couldn't encode bytes[CompactBytesArray], value at index 0 exceeds 65_535 bytes", ); - }).to.throw('Hex bytes4 value at index 3 does not fit in 4 bytes'); + }); }); - }); - describe('when decoding a bytes[CompactBytesArray] that contains `0000` entries', () => { - it("should decode as '' (empty string) in the decoded array", async () => { - const testCase = { - valueType: 'bytes[CompactBytesArray]', - decodedValue: ['0xaabb', '', '', '0xbeefbeefbeefbeefbeef'], - encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef', - }; + describe('when encoding uintN[CompactBytesArray]', () => { + it('should throw if trying to encode a value that exceeds the maximal lenght of bytes for this type', async () => { + expect(() => { + encodeValueType('uint8[CompactBytesArray]', [15, 178, 266]); + }).to.throw('Hex uint8 value at index 2 does not fit in 1 bytes'); + }); + + it('should throw if trying to decode a value that exceeds the maximal lenght of bytes for this type', async () => { + expect(() => { + decodeValueType( + 'uint8[CompactBytesArray]', + '0x00010100012b00014900020100', + ); + }).to.throw('Hex uint8 value at index 3 does not fit in 1 bytes'); + }); + }); - const decodedValue = decodeValueType( - testCase.valueType, - testCase.encodedValue, - ); - assert.deepStrictEqual(decodedValue, testCase.decodedValue); + describe('when encoding bytesN[CompactBytesArray]', () => { + it('should throw if trying to encode a value that exceeds the maximal lenght of bytes for this type', async () => { + expect(() => { + encodeValueType('bytes4[CompactBytesArray]', [ + '0xe6520726', + '0x272696e6', + '0x72062616', + '0xab7f11e3aabbcc', + ]); + }).to.throw('Hex bytes4 value at index 3 does not fit in 4 bytes'); + }); + + it('should throw if trying to decode a value that exceeds the maximal lenght of bytes for this type', async () => { + expect(() => { + decodeValueType( + 'bytes4[CompactBytesArray]', + '0x0004e65207260004272696e60004720626160007ab7f11e3aabbcc', + ); + }).to.throw('Hex bytes4 value at index 3 does not fit in 4 bytes'); + }); }); - it('should throw when trying to decode a `bytes[CompactBytesArray]` with an invalid length byte', async () => { - expect(() => { - decodeValueType('bytes[CompactBytesArray]', '0x0005cafe'); - }).to.throw("Couldn't decode bytes[CompactBytesArray]"); + describe('when decoding a bytes[CompactBytesArray] that contains `0000` entries', () => { + it("should decode as '' (empty string) in the decoded array", async () => { + const testCase = { + valueType: 'bytes[CompactBytesArray]', + decodedValue: ['0xaabb', '', '', '0xbeefbeefbeefbeefbeef'], + encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef', + }; + + const decodedValue = decodeValueType( + testCase.valueType, + testCase.encodedValue, + ); + assert.deepStrictEqual(decodedValue, testCase.decodedValue); + }); + + it('should throw when trying to decode a `bytes[CompactBytesArray]` with an invalid length byte', async () => { + expect(() => { + decodeValueType('bytes[CompactBytesArray]', '0x0005cafe'); + }).to.throw("Couldn't decode bytes[CompactBytesArray]"); + }); }); }); @@ -429,14 +883,12 @@ describe('encoder', () => { }, { valueContent: 'Boolean', - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000000', + encodedValue: '0x00', decodedValue: false, }, { valueContent: 'Boolean', - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000001', + encodedValue: '0x01', decodedValue: true, }, ]; diff --git a/src/lib/encoder.ts b/src/lib/encoder.ts index 91a5b070..7d3fb289 100644 --- a/src/lib/encoder.ts +++ b/src/lib/encoder.ts @@ -17,6 +17,7 @@ * @author Fabian Vogelsteller * @author Hugo Masclet <@Hugoo> * @author Callum Grindle <@CallumGrindle> + * @author Jean Cavallera <@CJ42> * @date 2020 */ @@ -39,6 +40,7 @@ import { stripHexPrefix, hexToBytes, bytesToHex, + toHex, } from 'web3-utils'; import BigNumber from 'bignumber.js'; @@ -50,7 +52,9 @@ import { SUPPORTED_HASH_FUNCTIONS, SUPPORTED_HASH_FUNCTION_STRINGS, } from '../constants/constants'; -import { getHashFunction, hashData } from './utils'; +import { getHashFunction, hashData, countNumberOfBytes } from './utils'; + +const abiCoder = AbiCoder; const bytesNRegex = /Bytes(\d+)/; @@ -70,8 +74,6 @@ const encodeDataSourceWithHash = ( ); }; -const abiCoder = AbiCoder; - const decodeDataSourceWithHash = (value: string): URLDataWithHash => { const hashFunctionSig = value.slice(0, 10); const hashFunction = getHashFunction(hashFunctionSig); @@ -83,6 +85,42 @@ const decodeDataSourceWithHash = (value: string): URLDataWithHash => { return { hashFunction: hashFunction.name, hash: dataHash, url: dataSource }; }; +const encodeToBytesN = ( + bytesN: 'bytes32' | 'bytes4', + value: string | number, +): string => { + let valueToEncode: string; + + if (typeof value === 'string' && !isHex(value)) { + // if we receive a plain string (e.g: "hey!"), convert it to utf8-hex data + valueToEncode = toHex(value); + } else if (typeof value === 'number') { + // if we receive a number as input, convert it to hex + valueToEncode = numberToHex(value); + } else { + valueToEncode = value; + } + + const numberOfBytesInType = parseInt(bytesN.slice(5), 10); + const numberOfBytesInValue = countNumberOfBytes(valueToEncode); + + if (numberOfBytesInValue > numberOfBytesInType) { + throw new Error( + `Can't convert ${value} to ${bytesN}. Too many bytes, expected at most ${numberOfBytesInType} bytes, received ${numberOfBytesInValue}.`, + ); + } + + const abiEncodedValue = abiCoder.encodeParameter(bytesN, valueToEncode); + + // abi-encoding right pads to 32 bytes, if we need less, we need to remove the padding + if (numberOfBytesInType === 32) { + return abiEncodedValue; + } + + const bytesArray = hexToBytes(abiEncodedValue); + return bytesToHex(bytesArray.slice(0, 4)); +}; + /** * Encodes bytes to CompactBytesArray * @@ -313,20 +351,37 @@ const decodeStringCompactBytesArray = (compactBytesArray: string): string[] => { const valueTypeEncodingMap = { bool: { - encode: (value: boolean) => abiCoder.encodeParameter('bool', value), - decode: (value: string) => abiCoder.decodeParameter('bool', value), + encode: (value: boolean) => (value ? '0x01' : '0x00'), + decode: (value: string) => value === '0x01', }, boolean: { - encode: (value: boolean) => abiCoder.encodeParameter('bool', value), - decode: (value: string) => abiCoder.decodeParameter('bool', value), + encode: (value: boolean) => (value ? '0x01' : '0x00'), + decode: (value: string) => value === '0x01', }, string: { - encode: (value: string) => abiCoder.encodeParameter('string', value), - decode: (value: string) => abiCoder.decodeParameter('string', value), + encode: (value: string | number) => { + // if we receive a number as input, + // convert each letter to its utf8 hex representation + if (typeof value === 'number') { + return utf8ToHex(`${value}`); + } + + return utf8ToHex(value); + }, + decode: (value: string) => hexToUtf8(value), }, address: { - encode: (value: string) => abiCoder.encodeParameter('address', value), - decode: (value: string) => abiCoder.decodeParameter('address', value), + 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: { @@ -352,21 +407,42 @@ const valueTypeEncodingMap = { }, }, uint256: { - encode: (value: string | number) => - abiCoder.encodeParameter('uint256', value), - decode: (value: string) => abiCoder.decodeParameter('uint256', value), + 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.`); + } + + const numberOfBytes = countNumberOfBytes(value); + + if (numberOfBytes > 32) { + throw new Error( + `Can't convert hex value ${value} to uint256. Too many bytes. ${numberOfBytes} is above the maximal number of bytes 32.`, + ); + } + + return BigNumber(value).toNumber(); + }, }, bytes32: { - encode: (value) => abiCoder.encodeParameter('bytes32', value), + encode: (value: string | number) => encodeToBytesN('bytes32', value), decode: (value: string) => abiCoder.decodeParameter('bytes32', value), }, bytes4: { - encode: (value) => abiCoder.encodeParameter('bytes4', value), - decode: (value: string) => abiCoder.decodeParameter('bytes4', value), + 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) => abiCoder.encodeParameter('bytes', value), - decode: (value: string) => abiCoder.decodeParameter('bytes', value), + encode: (value: string) => toHex(value), + decode: (value: string) => value, }, 'bool[]': { encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bcd607e8..fe384e99 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -24,6 +24,7 @@ import { isAddress, numberToHex, padLeft, + stripHexPrefix, } from 'web3-utils'; import { arrToBufArr } from 'ethereumjs-util'; @@ -573,3 +574,7 @@ export function patchIPFSUrlsIfApplicable( return receivedData; } + +export function countNumberOfBytes(data: string) { + return stripHexPrefix(data).length / 2; +} diff --git a/test/mockSchema.ts b/test/mockSchema.ts index 97b41d2b..53496407 100644 --- a/test/mockSchema.ts +++ b/test/mockSchema.ts @@ -331,7 +331,7 @@ export const mockSchema: (ERC725JSONSchema & { ]), returnGraphData: '0x0000000000000000000000000000000000000000000000000000000000000063', - expectedResult: '99', // TODO: BUG: This should not need to be string to work? + expectedResult: 99, }, // Case 13