Skip to content

Commit

Permalink
Merge pull request #427 from ERC725Alliance/fix/tuple-encoding-number
Browse files Browse the repository at this point in the history
feat: add support for encoding `bytes1` to `bytes32` + fix incorrect encoding `uintN`
  • Loading branch information
CJ42 authored Apr 28, 2024
2 parents 06d872b + b926597 commit a218ef3
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 55 deletions.
50 changes: 50 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,56 @@ describe('Running @erc725/erc725.js tests...', () => {
});
});

describe('Testing `encodeData`', () => {
describe('for `uintN` as `Number`', () => {
[
{ valueType: 'uint8', valueToEncode: 10, expectedEncodedValue: '0x0a' },
{
valueType: 'uint16',
valueToEncode: 10,
expectedEncodedValue: '0x000a',
},
{
valueType: 'uint24',
valueToEncode: 10,
expectedEncodedValue: '0x00000a',
},
{
valueType: 'uint32',
valueToEncode: 10,
expectedEncodedValue: '0x0000000a',
},
{
valueType: 'uint128',
valueToEncode: 10,
expectedEncodedValue: '0x0000000000000000000000000000000a',
},
{
valueType: 'uint256',
valueToEncode: 10,
expectedEncodedValue:
'0x000000000000000000000000000000000000000000000000000000000000000a',
},
].forEach((testCase) => {
it('should encode a valueType `uintN` as valueContent `Number` correctly with the right padding', () => {
const schema = {
name: 'ExampleUintN',
key: '0x512cddbe2654abd240fafbed308d91e82ff977301943f08ea825ba3e435bfa57',
keyType: 'Singleton',
valueType: testCase.valueType,
valueContent: 'Number',
};
const erc725js = new ERC725([schema]);
const result = erc725js.encodeData([
{ keyName: schema.name, value: testCase.valueToEncode },
]);

assert.equal(result.values[0], testCase.expectedEncodedValue);
});
});
});
});

describe('Testing utility encoding & decoding functions', () => {
const allGraphData = generateAllData(mockSchema) as any;
/* **************************************** */
Expand Down
51 changes: 44 additions & 7 deletions src/lib/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,35 @@ describe('encoder', () => {
encodedValue: '0x7765656b', // utf8-encoded characters
decodedValue: '0x7765656b',
},
];

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,
);
});
});

const leftPaddedTestCases = [
{
valueType: 'bytes4',
input: 1122334455,
encodedValue: '0x42e576f7', // number converted to hex + right padded
encodedValue: '0x42e576f7', // number converted to hex + left padded still
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 () => {
// numbers encoded as `bytesN` are left padded to allow symmetric encoding / decoding
leftPaddedTestCases.forEach((testCase) => {
it(`encodes + left pad numbers \`input\` = ${testCase.input} as ${testCase.valueType} padded on the left with \`00\`s`, async () => {
const encodedValue = encodeValueType(
testCase.valueType,
testCase.input,
Expand Down Expand Up @@ -273,18 +292,36 @@ describe('encoder', () => {
encodedValue:
'0x546869732073656e74656e6365206973203332206279746573206c6f6e672021',
},
];

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,
);
});
});

const leftPaddedTestCases = [
{
valueType: 'bytes32',
input: 12345,
decodedValue:
'0x3039000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000003039',
encodedValue:
'0x3039000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000003039',
},
];

oneWayEncodingTestCases.forEach((testCase) => {
it(`encodes one way \`input\` = ${testCase.input} as ${testCase.valueType}, but does not decode back as the same input`, async () => {
leftPaddedTestCases.forEach((testCase) => {
it(`encodes + left pad number \`input\` = ${testCase.input} as ${testCase.valueType} padded on the left with \`00\`s`, async () => {
const encodedValue = encodeValueType(
testCase.valueType,
testCase.input,
Expand Down
106 changes: 78 additions & 28 deletions src/lib/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,17 @@ import {
countNumberOfBytes,
isValidUintSize,
countSignificantBits,
isValidByteSize,
isValueContentLiteralHex,
} from './utils';
import { ERC725JSONSchemaValueType } from '../types/ERC725JSONSchema';

const abiCoder = AbiCoder;

const uintNValueTypeRegex = /^uint(\d+)$/;

const bytesNValueTypeRegex = /^bytes(\d+)$/;
const BytesNValueContentRegex = /Bytes(\d+)/;

const ALLOWED_BYTES_SIZES = [2, 4, 8, 16, 32, 64, 128, 256];

export const encodeDataSourceWithHash = (
verification: undefined | Verification,
dataSource: string,
Expand Down Expand Up @@ -171,23 +171,60 @@ export const decodeDataSourceWithHash = (value: string): URLDataWithHash => {
};
};

type BytesNValueTypes =
| 'bytes1'
| 'bytes2'
| 'bytes3'
| 'bytes4'
| 'bytes5'
| 'bytes6'
| 'bytes7'
| 'bytes8'
| 'bytes9'
| 'bytes10'
| 'bytes11'
| 'bytes12'
| 'bytes13'
| 'bytes14'
| 'bytes15'
| 'bytes16'
| 'bytes17'
| 'bytes18'
| 'bytes19'
| 'bytes20'
| 'bytes21'
| 'bytes22'
| 'bytes23'
| 'bytes24'
| 'bytes25'
| 'bytes26'
| 'bytes27'
| 'bytes28'
| 'bytes29'
| 'bytes30'
| 'bytes31'
| 'bytes32';

const encodeToBytesN = (
bytesN: 'bytes32' | 'bytes4',
bytesN: BytesNValueTypes,
value: string | number,
): string => {
const numberOfBytesInType = Number.parseInt(bytesN.split('bytes')[1], 10);

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);
// if we receive a number as input, convert it to hex,
// despite `bytesN` pads on the right, we pad number on the left side here
// to symmetrically encode / decode
valueToEncode = padLeft(numberToHex(value), numberOfBytesInType * 2);
} else {
valueToEncode = value;
}

const numberOfBytesInType = Number.parseInt(bytesN.split('bytes')[1], 10);
const numberOfBytesInValue = countNumberOfBytes(valueToEncode);

if (numberOfBytesInValue > numberOfBytesInType) {
Expand All @@ -204,7 +241,7 @@ const encodeToBytesN = (
}

const bytesArray = hexToBytes(abiEncodedValue);
return bytesToHex(bytesArray.slice(0, 4));
return bytesToHex(bytesArray.slice(0, numberOfBytesInType));
};

/**
Expand Down Expand Up @@ -441,6 +478,10 @@ const valueTypeEncodingMap = (
decode: (value: string) => any;
} => {
const uintNRegexMatch = type.match(uintNValueTypeRegex);
const bytesNRegexMatch = type.match(bytesNValueTypeRegex);
const bytesLength = bytesNRegexMatch
? Number.parseInt(bytesNRegexMatch[1], 10)
: '';

const uintLength = uintNRegexMatch
? Number.parseInt(uintNRegexMatch[0].slice(4), 10)
Expand All @@ -463,6 +504,13 @@ const valueTypeEncodingMap = (
return compactBytesArrayMap[type];
}

if (type === 'bytes') {
return {
encode: (value: string) => toHex(value),
decode: (value: string) => value,
};
}

switch (type) {
case 'bool':
case 'boolean':
Expand Down Expand Up @@ -552,27 +600,24 @@ const valueTypeEncodingMap = (
return toBN(value).toNumber();
},
};
case 'bytes32':
case `bytes${bytesLength}`:
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),
encode: (value: string | number) => {
if (!isValidByteSize(bytesLength as number)) {
throw new Error(
`Can't encode ${value} as ${type}. Invalid \`bytesN\` provided. Expected a \`N\` value for bytesN between 1 and 32.`,
);
}
return encodeToBytesN(type as BytesNValueTypes, 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);
// - that data to decode does not go over N bytes.
// - if the data is less than N bytes, that it gets padded to N bytes long.
const reEncodedData = abiCoder.encodeParameter(type, value);
return abiCoder.decodeParameter(type, reEncodedData);
},
};
case 'bytes':
return {
encode: (value: string) => toHex(value),
decode: (value: string) => value,
};
case 'bool[]':
return {
encode: (value: boolean) => abiCoder.encodeParameter('bool[]', value),
Expand Down Expand Up @@ -778,7 +823,7 @@ export const valueContentEncodingMap = (
throw new Error(`Value: ${value} is not hex.`);
}

if (bytesLength && !ALLOWED_BYTES_SIZES.includes(bytesLength)) {
if (bytesLength && !isValidByteSize(bytesLength)) {
throw new Error(
`Provided bytes length: ${bytesLength} for encoding valueContent: ${valueContent} is not valid.`,
);
Expand All @@ -800,7 +845,7 @@ export const valueContentEncodingMap = (
return null;
}

if (bytesLength && !ALLOWED_BYTES_SIZES.includes(bytesLength)) {
if (bytesLength && !isValidByteSize(bytesLength)) {
console.error(
`Provided bytes length: ${bytesLength} for encoding valueContent: ${valueContent} is not valid.`,
);
Expand Down Expand Up @@ -937,8 +982,13 @@ export function decodeValueContent(
valueContent: string,
value: string,
): string | URLDataWithHash | number | boolean | null {
if (valueContent.slice(0, 2) === '0x') {
return valueContent === value ? value : null;
if (isValueContentLiteralHex(valueContent)) {
if (valueContent.toLowerCase() !== value) {
throw new Error(
`Could not decode value content: the value ${value} does not match the Hex Literal ${valueContent} defined in the \`valueContent\` part of the schema`,
);
}
return valueContent;
}

if (value == null || value === '0x') {
Expand Down
8 changes: 7 additions & 1 deletion src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ describe('utils', () => {
valueContent: '0xc9aaAE3201F40fd0fF04D9c885769d8256A456ab',
valueType: 'bytes',
decodedValue: '0xc9aaAE3201F40fd0fF04D9c885769d8256A456ab',
encodedValue: '0xc9aaAE3201F40fd0fF04D9c885769d8256A456ab',
encodedValue: '0xc9aaae3201f40fd0ff04d9c885769d8256a456ab', // encoded hex is always lower case
},
];

Expand Down Expand Up @@ -414,6 +414,12 @@ describe('utils', () => {
encodedValue: '0xdeadbeaf0000000000000010',
decodedValue: ['0xdeadbeaf', 16],
},
{
valueContent: '(Bytes4,Number)',
valueType: '(bytes4,uint128)',
encodedValue: '0xdeadbeaf00000000000000000000000000000020',
decodedValue: ['0xdeadbeaf', 32],
},
]; // we may need to add more test cases! Address, etc.

testCases.forEach((testCase) => {
Expand Down
Loading

0 comments on commit a218ef3

Please sign in to comment.