diff --git a/src/lib/decodeData.ts b/src/lib/decodeData.ts index a3756df0..9cbe885e 100644 --- a/src/lib/decodeData.ts +++ b/src/lib/decodeData.ts @@ -50,9 +50,6 @@ const isValidTupleDefinition = (tupleContent: string): boolean => { return true; }; -const extractTupleElements = (tupleContent: string): string[] => - tupleContent.substring(1, tupleContent.length - 1).split(','); - const extractTupleElements = (tupleContent: string): string[] => tupleContent.substring(1, tupleContent.length - 1).split(','); diff --git a/src/lib/encoder.test.ts b/src/lib/encoder.test.ts index 7519bdb9..fd88e44c 100644 --- a/src/lib/encoder.test.ts +++ b/src/lib/encoder.test.ts @@ -327,8 +327,39 @@ describe('encoder', () => { }); }); - describe('`uint128` type', () => { + describe('`uintN` type', () => { const validTestCases = [ + { + valueType: 'uint256', + decodedValue: 1337, + encodedValue: + '0x0000000000000000000000000000000000000000000000000000000000000539', + }, + { + valueType: 'uint8', + decodedValue: 10, + encodedValue: '0x0a', + }, + { + valueType: 'uint16', + decodedValue: 10, + encodedValue: '0x000a', + }, + { + valueType: 'uint24', + decodedValue: 10, + encodedValue: '0x00000a', + }, + { + valueType: 'uint32', + decodedValue: 10, + encodedValue: '0x0000000a', + }, + { + valueType: 'uint64', + decodedValue: 25, + encodedValue: '0x0000000000000019', + }, { valueType: 'uint128', decodedValue: 11, @@ -350,30 +381,10 @@ describe('encoder', () => { ); }); }); - }); - - describe('`uint256` type', () => { - const validTestCases = [ - { - valueType: 'uint256', - decodedValue: 1337, - encodedValue: - '0x0000000000000000000000000000000000000000000000000000000000000539', - }, - ]; - validTestCases.forEach((testCase) => { - it(`encodes/decodes: ${testCase.decodedValue} as ${testCase.valueType}`, () => { - const encodedValue = encodeValueType( - testCase.valueType, - testCase.decodedValue, - ); - - assert.deepStrictEqual(encodedValue, testCase.encodedValue); - assert.deepStrictEqual( - decodeValueType(testCase.valueType, encodedValue), - testCase.decodedValue, - ); + ['uint1', 'uint129', 'uint257'].forEach((invalidUintType) => { + it(`should error with invalid valueType for type ${invalidUintType}`, async () => { + assert.throws(() => encodeValueType(invalidUintType, '12345')); }); }); }); diff --git a/src/lib/encoder.ts b/src/lib/encoder.ts index c941c355..a0a1bee3 100644 --- a/src/lib/encoder.ts +++ b/src/lib/encoder.ts @@ -56,7 +56,9 @@ import { ERC725JSONSchemaValueType } from '../types/ERC725JSONSchema'; const abiCoder = AbiCoder; -const bytesNRegex = /Bytes(\d+)/; +const uintNValueTypeRegex = /^uint(?:8|[1-9]\d|1[0-9]{2}|2[0-4]\d|25[0-6])$/; + +const BytesNValueContentRegex = /Bytes(\d+)/; const ALLOWED_BYTES_SIZES = [2, 4, 8, 16, 32, 64, 128, 256]; @@ -357,156 +359,354 @@ const decodeStringCompactBytesArray = (compactBytesArray: string): string[] => { return stringValues; }; -const valueTypeEncodingMap = { - bool: { - encode: (value: boolean) => (value ? '0x01' : '0x00'), - decode: (value: string) => value === '0x01', - }, - boolean: { - encode: (value: boolean) => (value ? '0x01' : '0x00'), - decode: (value: string) => value === '0x01', - }, - string: { - encode: (value: string | number) => { - // if we receive a number as input, - // convert each letter to its utf8 hex representation - if (typeof value === 'number') { - return utf8ToHex(`${value}`); - } +const valueTypeEncodingMap = ( + type: string, +): { + encode: (value: any) => string; + decode: (value: string) => any; +} => { + const uintNRegexMatch = type.match(uintNValueTypeRegex); + + const uintLength = uintNRegexMatch + ? parseInt(uintNRegexMatch[0].slice(4), 10) + : ''; + + if (type.includes('[CompactBytesArray]')) { + const compactBytesArrayMap = { + 'bytes[CompactBytesArray]': { + encode: (value: string[]) => encodeCompactBytesArray(value), + decode: (value: string) => decodeCompactBytesArray(value), + }, + 'string[CompactBytesArray]': { + encode: (value: string[]) => encodeStringCompactBytesArray(value), + decode: (value: string) => decodeStringCompactBytesArray(value), + }, + ...returnTypesOfBytesNCompactBytesArray(), + ...returnTypesOfUintNCompactBytesArray(), + }; - return utf8ToHex(value); - }, - decode: (value: string) => hexToUtf8(value), - }, - address: { - encode: (value: string) => { - // abi-encode pads to 32 x 00 bytes on the left, so we need to remove them - const abiEncodedValue = abiCoder.encodeParameter('address', value); - - // convert to an array of individual bytes - const bytesArray = hexToBytes(abiEncodedValue); - - // just keep the last 20 bytes, starting at index 12 - return bytesToHex(bytesArray.slice(12)); - }, - decode: (value: string) => toChecksumAddress(value), - }, - // NOTE: We could add conditional handling of numeric values here... - uint128: { - encode: (value: string | number) => { - const abiEncodedValue = abiCoder.encodeParameter('uint128', value); - const bytesArray = hexToBytes(abiEncodedValue); - return bytesToHex(bytesArray.slice(16)); - }, - decode: (value: string) => { - if (!isHex(value)) { - throw new Error(`Can't convert ${value} to uint128, value is not hex.`); - } + return compactBytesArrayMap[type]; + } - if (value.length > 34) { - throw new Error( - `Can't convert hex value ${value} to uint128. Too many bytes. ${ - (value.length - 2) / 2 - } > 16`, - ); - } + switch (type) { + case 'bool': + case 'boolean': + return { + encode: (value: boolean) => (value ? '0x01' : '0x00'), + decode: (value: string) => value === '0x01', + }; + case 'string': + return { + encode: (value: string | number) => { + // if we receive a number as input, convert each letter to its utf8 hex representation + if (typeof value === 'number') { + return utf8ToHex(`${value}`); + } - return toBN(value).toNumber(); - }, - }, - uint256: { - encode: (value: string | number) => { - return abiCoder.encodeParameter('uint256', value); - }, - decode: (value: string) => { - if (!isHex(value)) { - throw new Error(`Can't convert ${value} to uint256, value is not hex.`); - } + return utf8ToHex(value); + }, + decode: (value: string) => hexToUtf8(value), + }; + case 'address': + return { + encode: (value: string) => { + // abi-encode pads to 32 x 00 bytes on the left, so we need to remove them + const abiEncodedValue = abiCoder.encodeParameter('address', value); - const numberOfBytes = countNumberOfBytes(value); + // convert to an array of individual bytes + const bytesArray = hexToBytes(abiEncodedValue); - if (numberOfBytes > 32) { - throw new Error( - `Can't convert hex value ${value} to uint256. Too many bytes. ${numberOfBytes} is above the maximal number of bytes 32.`, - ); - } + // just keep the last 20 bytes, starting at index 12 + return bytesToHex(bytesArray.slice(12)); + }, + decode: (value: string) => toChecksumAddress(value), + }; + // NOTE: We could add conditional handling of numeric values here... + case `uint${uintLength}`: + return { + encode: (value: string | number) => { + const abiEncodedValue = abiCoder.encodeParameter(type, value); - return toBN(value).toNumber(); - }, - }, - bytes32: { - encode: (value: string | number) => encodeToBytesN('bytes32', value), - decode: (value: string) => abiCoder.decodeParameter('bytes32', value), - }, - bytes4: { - encode: (value: string | number) => encodeToBytesN('bytes4', value), - decode: (value: string) => { - // we need to abi-encode the value again to ensure that: - // - that data to decode does not go over 4 bytes. - // - if the data is less than 4 bytes, that it gets padded to 4 bytes long. - const reEncodedData = abiCoder.encodeParameter('bytes4', value); - return abiCoder.decodeParameter('bytes4', reEncodedData); - }, - }, - bytes: { - encode: (value: string) => toHex(value), - decode: (value: string) => value, - }, - 'bool[]': { - encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), - decode: (value: string) => abiCoder.decodeParameter('bool[]', value), - }, - 'boolean[]': { - encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), - decode: (value: string) => abiCoder.decodeParameter('bool[]', value), - }, - 'string[]': { - encode: (value: string[]) => abiCoder.encodeParameter('string[]', value), - decode: (value: string) => abiCoder.decodeParameter('string[]', value), - }, - 'address[]': { - encode: (value: string[]) => abiCoder.encodeParameter('address[]', value), - decode: (value: string) => abiCoder.decodeParameter('address[]', value), - }, - 'uint256[]': { - encode: (value: Array) => - abiCoder.encodeParameter('uint256[]', value), - decode: (value: string) => { - // we want to return an array of numbers as [1, 2, 3], not an array of strings as [ '1', '2', '3'] - return abiCoder - .decodeParameter('uint256[]', value) - .map((numberAsString) => parseInt(numberAsString, 10)); - }, - }, - 'bytes32[]': { - encode: (value: string[]) => abiCoder.encodeParameter('bytes32[]', value), - decode: (value: string) => abiCoder.decodeParameter('bytes32[]', value), - }, - 'bytes4[]': { - encode: (value: string[]) => abiCoder.encodeParameter('bytes4[]', value), - decode: (value: string) => abiCoder.decodeParameter('bytes4[]', value), - }, - 'bytes[]': { - encode: (value: string[]) => abiCoder.encodeParameter('bytes[]', value), - decode: (value: string) => abiCoder.decodeParameter('bytes[]', value), - }, - 'bytes[CompactBytesArray]': { - encode: (value: string[]) => encodeCompactBytesArray(value), - decode: (value: string) => decodeCompactBytesArray(value), - }, - 'string[CompactBytesArray]': { - encode: (value: string[]) => encodeStringCompactBytesArray(value), - decode: (value: string) => decodeStringCompactBytesArray(value), - }, - ...returnTypesOfBytesNCompactBytesArray(), - ...returnTypesOfUintNCompactBytesArray(), + const bytesArray = hexToBytes(abiEncodedValue); + const numberOfBytes = (uintLength as number) / 8; + + // abi-encoding always pad to 32 bytes. We need to keep the `n` rightmost bytes. + // where `n` = `numberOfBytes` + const startIndex = 32 - numberOfBytes; + + return bytesToHex(bytesArray.slice(startIndex)); + }, + decode: (value: string) => { + if (!isHex(value)) { + throw new Error( + `Can't convert ${value} to ${type}, value is not hex.`, + ); + } + + const numberOfBytes = countNumberOfBytes(value); + + if (numberOfBytes > (uintLength as number) / 8) { + throw new Error( + `Can't convert hex value ${value} to ${type}. Too many bytes. ${numberOfBytes} > 16`, + ); + } + + return toBN(value).toNumber(); + }, + }; + case 'bytes32': + return { + encode: (value: string | number) => encodeToBytesN('bytes32', value), + decode: (value: string) => abiCoder.decodeParameter('bytes32', value), + }; + case 'bytes4': + return { + encode: (value: string | number) => encodeToBytesN('bytes4', value), + decode: (value: string) => { + // we need to abi-encode the value again to ensure that: + // - that data to decode does not go over 4 bytes. + // - if the data is less than 4 bytes, that it gets padded to 4 bytes long. + const reEncodedData = abiCoder.encodeParameter('bytes4', value); + return abiCoder.decodeParameter('bytes4', reEncodedData); + }, + }; + case 'bytes': + return { + encode: (value: string) => toHex(value), + decode: (value: string) => value, + }; + case 'bool[]': + return { + encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), + decode: (value: string) => abiCoder.decodeParameter('bool[]', value), + }; + case 'boolean[]': + return { + encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), + decode: (value: string) => abiCoder.decodeParameter('bool[]', value), + }; + case 'string[]': + return { + encode: (value: string[]) => + abiCoder.encodeParameter('string[]', value), + decode: (value: string) => abiCoder.decodeParameter('string[]', value), + }; + case 'address[]': + return { + encode: (value: string[]) => + abiCoder.encodeParameter('address[]', value), + decode: (value: string) => abiCoder.decodeParameter('address[]', value), + }; + case 'uint256[]': + return { + encode: (value: Array) => + abiCoder.encodeParameter('uint256[]', value), + decode: (value: string) => { + // we want to return an array of numbers as [1, 2, 3], not an array of strings as [ '1', '2', '3'] + return abiCoder + .decodeParameter('uint256[]', value) + .map((numberAsString) => parseInt(numberAsString, 10)); + }, + }; + case 'bytes32[]': + return { + encode: (value: string[]) => + abiCoder.encodeParameter('bytes32[]', value), + decode: (value: string) => abiCoder.decodeParameter('bytes32[]', value), + }; + case 'bytes4[]': + return { + encode: (value: string[]) => + abiCoder.encodeParameter('bytes4[]', value), + decode: (value: string) => abiCoder.decodeParameter('bytes4[]', value), + }; + case 'bytes[]': + return { + encode: (value: string[]) => abiCoder.encodeParameter('bytes[]', value), + decode: (value: string) => abiCoder.decodeParameter('bytes[]', value), + }; + case 'bytes[CompactBytesArray]': + return { + encode: (value: string[]) => encodeCompactBytesArray(value), + decode: (value: string) => decodeCompactBytesArray(value), + }; + case 'string[CompactBytesArray]': + return { + encode: (value: string[]) => encodeStringCompactBytesArray(value), + decode: (value: string) => decodeStringCompactBytesArray(value), + }; + default: + return { + encode: (value: any) => { + throw new Error( + `Could not encode ${value}. Value type ${type} is unknown`, + ); + }, + decode: (value: any) => { + throw new Error( + `Could not decode ${value}. Value type ${type} is unknown`, + ); + }, + }; + } }; +// const valueTypeEncodingMap = { +// bool: { +// encode: (value: boolean) => (value ? '0x01' : '0x00'), +// decode: (value: string) => value === '0x01', +// }, +// boolean: { +// encode: (value: boolean) => (value ? '0x01' : '0x00'), +// decode: (value: string) => value === '0x01', +// }, +// string: { +// encode: (value: string | number) => { +// // if we receive a number as input, +// // convert each letter to its utf8 hex representation +// if (typeof value === 'number') { +// return utf8ToHex(`${value}`); +// } + +// return utf8ToHex(value); +// }, +// decode: (value: string) => hexToUtf8(value), +// }, +// address: { +// encode: (value: string) => { +// // abi-encode pads to 32 x 00 bytes on the left, so we need to remove them +// const abiEncodedValue = abiCoder.encodeParameter('address', value); + +// // convert to an array of individual bytes +// const bytesArray = hexToBytes(abiEncodedValue); + +// // just keep the last 20 bytes, starting at index 12 +// return bytesToHex(bytesArray.slice(12)); +// }, +// decode: (value: string) => toChecksumAddress(value), +// }, +// // NOTE: We could add conditional handling of numeric values here... +// uint128: { +// encode: (value: string | number) => { +// const abiEncodedValue = abiCoder.encodeParameter('uint128', value); +// const bytesArray = hexToBytes(abiEncodedValue); +// return bytesToHex(bytesArray.slice(16)); +// }, +// decode: (value: string) => { +// if (!isHex(value)) { +// throw new Error(`Can't convert ${value} to uint128, value is not hex.`); +// } + +// if (value.length > 34) { +// throw new Error( +// `Can't convert hex value ${value} to uint128. Too many bytes. ${ +// (value.length - 2) / 2 +// } > 16`, +// ); +// } + +// return toBN(value).toNumber(); +// }, +// }, +// uint256: { +// encode: (value: string | number) => { +// return abiCoder.encodeParameter('uint256', value); +// }, +// decode: (value: string) => { +// if (!isHex(value)) { +// throw new Error(`Can't convert ${value} to uint256, value is not hex.`); +// } + +// 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 toBN(value).toNumber(); +// }, +// }, +// bytes32: { +// encode: (value: string | number) => encodeToBytesN('bytes32', value), +// decode: (value: string) => abiCoder.decodeParameter('bytes32', value), +// }, +// bytes4: { +// encode: (value: string | number) => encodeToBytesN('bytes4', value), +// decode: (value: string) => { +// // we need to abi-encode the value again to ensure that: +// // - that data to decode does not go over 4 bytes. +// // - if the data is less than 4 bytes, that it gets padded to 4 bytes long. +// const reEncodedData = abiCoder.encodeParameter('bytes4', value); +// return abiCoder.decodeParameter('bytes4', reEncodedData); +// }, +// }, +// bytes: { +// encode: (value: string) => toHex(value), +// decode: (value: string) => value, +// }, +// 'bool[]': { +// encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), +// decode: (value: string) => abiCoder.decodeParameter('bool[]', value), +// }, +// 'boolean[]': { +// encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value), +// decode: (value: string) => abiCoder.decodeParameter('bool[]', value), +// }, +// 'string[]': { +// encode: (value: string[]) => abiCoder.encodeParameter('string[]', value), +// decode: (value: string) => abiCoder.decodeParameter('string[]', value), +// }, +// 'address[]': { +// encode: (value: string[]) => abiCoder.encodeParameter('address[]', value), +// decode: (value: string) => abiCoder.decodeParameter('address[]', value), +// }, +// 'uint256[]': { +// encode: (value: Array) => +// abiCoder.encodeParameter('uint256[]', value), +// decode: (value: string) => { +// // we want to return an array of numbers as [1, 2, 3], not an array of strings as [ '1', '2', '3'] +// return abiCoder +// .decodeParameter('uint256[]', value) +// .map((numberAsString) => parseInt(numberAsString, 10)); +// }, +// }, +// 'bytes32[]': { +// encode: (value: string[]) => abiCoder.encodeParameter('bytes32[]', value), +// decode: (value: string) => abiCoder.decodeParameter('bytes32[]', value), +// }, +// 'bytes4[]': { +// encode: (value: string[]) => abiCoder.encodeParameter('bytes4[]', value), +// decode: (value: string) => abiCoder.decodeParameter('bytes4[]', value), +// }, +// 'bytes[]': { +// encode: (value: string[]) => abiCoder.encodeParameter('bytes[]', value), +// decode: (value: string) => abiCoder.decodeParameter('bytes[]', value), +// }, +// 'bytes[CompactBytesArray]': { +// encode: (value: string[]) => encodeCompactBytesArray(value), +// decode: (value: string) => decodeCompactBytesArray(value), +// }, +// 'string[CompactBytesArray]': { +// encode: (value: string[]) => encodeStringCompactBytesArray(value), +// decode: (value: string) => decodeStringCompactBytesArray(value), +// }, +// ...returnTypesOfBytesNCompactBytesArray(), +// ...returnTypesOfUintNCompactBytesArray(), +// }; + // Use enum for type below // Is it this enum ERC725JSONSchemaValueType? (If so, custom is missing from enum) -export const valueContentEncodingMap = (valueContent: string) => { - const bytesNRegexMatch = valueContent.match(bytesNRegex); +export const valueContentEncodingMap = ( + valueContent: string, +): { + type: string; + encode: (value: any) => string; + decode: (value: string) => any; +} => { + const bytesNRegexMatch = valueContent.match(BytesNValueContentRegex); const bytesLength = bytesNRegexMatch ? parseInt(bytesNRegexMatch[1], 10) : ''; switch (valueContent) { @@ -694,12 +894,12 @@ export const valueContentEncodingMap = (valueContent: string) => { case 'Boolean': { return { type: 'bool', - encode: (value): string => { - return valueTypeEncodingMap.bool.encode(value); + encode: (value: boolean): string => { + return valueTypeEncodingMap('bool').encode(value); }, decode: (value: string): boolean => { try { - return valueTypeEncodingMap.bool.decode(value) as any as boolean; + return valueTypeEncodingMap('bool').decode(value) as any as boolean; } catch (error) { throw new Error(`Value ${value} is not a boolean`); } @@ -734,7 +934,7 @@ export function encodeValueType( return value; } - return valueTypeEncodingMap[type].encode(value); + return valueTypeEncodingMap(type).encode(value); } export function decodeValueType( @@ -747,7 +947,7 @@ export function decodeValueType( return value; } - return valueTypeEncodingMap[type].decode(value); + return valueTypeEncodingMap(type).decode(value); } export function encodeValueContent(