Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for encoding bytes1 to bytes32 + fix incorrect encoding uintN #427

Merged
merged 5 commits into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading