Skip to content

Commit

Permalink
feat: add support for bytesN valueType from 1 to 32
Browse files Browse the repository at this point in the history
  • Loading branch information
CJ42 committed Apr 25, 2024
1 parent f8dc5ae commit 1db6f2f
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 35 deletions.
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 \`input\` = ${testCase.input} as ${testCase.valueType} padded on the right with \`00\`s`, async () => {
const encodedValue = encodeValueType(
testCase.valueType,
testCase.input,
Expand Down
96 changes: 70 additions & 26 deletions src/lib/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,18 @@ import {
countNumberOfBytes,
isValidUintSize,
countSignificantBits,
isValidBytesSize,
} 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 +172,58 @@ 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 = 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, left padded
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 +240,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 +477,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 +503,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 +599,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 (!isValidBytesSize(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 +822,7 @@ export const valueContentEncodingMap = (
throw new Error(`Value: ${value} is not hex.`);
}

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

if (bytesLength && !ALLOWED_BYTES_SIZES.includes(bytesLength)) {
if (bytesLength && !isValidBytesSize(bytesLength)) {
console.error(
`Provided bytes length: ${bytesLength} for encoding valueContent: ${valueContent} is not valid.`,
);
Expand Down
10 changes: 10 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,3 +816,13 @@ export const duplicateMultiTypeERC725SchemaEntry = (
export function isValidUintSize(bitSize: number) {
return bitSize >= 8 && bitSize <= 256 && bitSize % 8 === 0;
}

/*
* `bytesN` must be a valid number of bytes between 1 and 32
* e.g: bytes1, bytes2, bytes3, bytes4, ..., bytes32
*
* @param bytesSize the size of the fixed size bytes value
*/
export function isValidBytesSize(bytesSize: number) {
return bytesSize >= 1 && bytesSize <= 32;
}
3 changes: 1 addition & 2 deletions test/mockSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,7 @@ export const mockSchema: (ERC725JSONSchema & {
returnRawDataArray: abiCoder.encodeParameter('bytes[]', [
'0x0000000000000000000000000000000000000000000000000000000000000063',
]),
returnGraphData:
'0x0000000000000000000000000000000000000000000000000000000000000063',
returnGraphData: '0x63',
expectedResult: 99,
},

Expand Down

0 comments on commit 1db6f2f

Please sign in to comment.