Skip to content

Commit

Permalink
feat: add encoding/decoding for other types of compactBytesArray (#262)
Browse files Browse the repository at this point in the history
* feat: add encoding/decoding for other types of compactBytesArray

Co-authored-by: Jean Cvllr <[email protected]>

* test: add new test cases foor the new types

* chore: fix typos and improve test description

* chore: make functions more JS like

* chore: improve readability

* docs: add JSDocs

* chore: improve JSDocs and types

---------

Co-authored-by: Jean Cvllr <[email protected]>
  • Loading branch information
b00ste and CJ42 authored Feb 6, 2023
1 parent d9ce0f0 commit 9268a32
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 1 deletion.
64 changes: 63 additions & 1 deletion src/lib/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { expect } from 'chai';

import assert from 'assert';
import { keccak256, utf8ToHex } from 'web3-utils';
import { keccak256, utf8ToHex, stripHexPrefix } from 'web3-utils';
import {
valueContentEncodingMap,
encodeValueType,
Expand Down Expand Up @@ -132,6 +132,29 @@ describe('encoder', () => {
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',
},
];

testCases.forEach((testCase) => {
Expand Down Expand Up @@ -201,6 +224,45 @@ describe('encoder', () => {
});
});

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

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

describe('when decoding a bytes[CompactBytesArray] that contains `0000` entries', () => {
it("should decode as '' (empty string) in the decoded array", async () => {
const testCase = {
Expand Down
172 changes: 172 additions & 0 deletions src/lib/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ const decodeDataSourceWithHash = (value: string): URLDataWithHash => {
return { hashFunction: hashFunction.name, hash: dataHash, url: dataSource };
};

/**
* Encodes bytes to CompactBytesArray
*
* @param values An array of BytesLike strings
* @returns bytes[CompactBytesArray]
*/
const encodeCompactBytesArray = (values: string[]): string => {
const compactBytesArray = values
.filter((value, index) => {
Expand All @@ -107,6 +113,12 @@ const encodeCompactBytesArray = (values: string[]): string => {
return compactBytesArray;
};

/**
* Decodes CompactBytesArray of type bytes
*
* @param compactBytesArray A bytes[CompactBytesArray]
* @returns An array of BytesLike strings decode from `compactBytesArray`
*/
const decodeCompactBytesArray = (compactBytesArray: string): string[] => {
if (!isHex(compactBytesArray))
throw new Error("Couldn't decode, value is not hex");
Expand Down Expand Up @@ -143,6 +155,160 @@ const decodeCompactBytesArray = (compactBytesArray: string): string[] => {
return encodedValues;
};

/**
* Encodes bytesN to CompactBytesArray
*
* @param values An array of BytesLike strings
* @param numberOfBytes The number of bytes for each value from `values`
* @returns bytesN[CompactBytesArray]
*/
const encodeBytesNCompactBytesArray = (
values: string[],
numberOfBytes: number,
): string => {
values.forEach((value, index) => {
if (stripHexPrefix(value).length > numberOfBytes * 2)
throw new Error(
`Hex bytes${numberOfBytes} value at index ${index} does not fit in ${numberOfBytes} bytes`,
);
});

return encodeCompactBytesArray(values);
};

/**
* Decodes CompactBytesArray of type bytesN
*
* @param compactBytesArray A bytesN[CompactBytesArray]
* @param numberOfBytes The number of bytes allowed per each element from `compactBytesArray`
* @returns An array of BytesLike strings decoded from `compactBytesArray`
*/
const decodeBytesNCompactBytesArray = (
compactBytesArray: string,
numberOfBytes: number,
): string[] => {
const bytesValues = decodeCompactBytesArray(compactBytesArray);
bytesValues.forEach((bytesValue, index) => {
if (stripHexPrefix(bytesValue).length > numberOfBytes * 2)
throw new Error(
`Hex bytes${numberOfBytes} value at index ${index} does not fit in ${numberOfBytes} bytes`,
);
});

return bytesValues;
};

/**
* @returns Encoding/decoding for bytes1[CompactBytesArray] to bytes32[COmpactBytesArray]
*/
const returnTypesOfBytesNCompactBytesArray = () => {
const types: Record<
string,
{ encode: (value: string[]) => string; decode: (value: string) => string[] }
> = {};

for (let i = 1; i < 33; i++) {
types[`bytes${i}[CompactBytesArray]`] = {
encode: (value: string[]) => encodeBytesNCompactBytesArray(value, i),
decode: (value: string) => decodeBytesNCompactBytesArray(value, i),
};
}
return types;
};

/**
* Encodes uintN to CompactBytesArray
* @param values An array of BytesLike strings
* @param numberOfBytes The number of bytes for each value from `values`
* @returns uintN[CompactBytesArray]
*/
const encodeUintNCompactBytesArray = (
values: number[],
numberOfBytes: number,
): string => {
const hexValues: string[] = values.map((value, index) => {
const hexNumber = stripHexPrefix(numberToHex(value)).padStart(
numberOfBytes * 2,
'0',
);
if (hexNumber.length > numberOfBytes * 2)
throw new Error(
`Hex uint${
numberOfBytes * 8
} value at index ${index} does not fit in ${numberOfBytes} bytes`,
);
return hexNumber;
});

return encodeCompactBytesArray(hexValues);
};

/**
* Decodes CompactBytesArray of type uintN
* @param compactBytesArray A uintN[CompactBytesArray]
* @param numberOfBytes The number of bytes allowed per each element from `compactBytesArray`
* @returns An array of numbers decoded from `compactBytesArray`
*/
const decodeUintNCompactBytesArray = (
compactBytesArray: string,
numberOfBytes: number,
): number[] => {
const hexValues = decodeCompactBytesArray(compactBytesArray);

return hexValues.map((hexValue, index) => {
const hexValueStripped = stripHexPrefix(hexValue);
if (hexValueStripped.length > numberOfBytes * 2)
throw new Error(
`Hex uint${
numberOfBytes * 8
} value at index ${index} does not fit in ${numberOfBytes} bytes`,
);
return hexToNumber(hexValue);
});
};

/**
* @returns Encoding/decoding for uint8[CompactBytesArray] to uint256[COmpactBytesArray]
*/
const returnTypesOfUintNCompactBytesArray = () => {
const types: Record<
string,
{ encode: (value: number[]) => string; decode: (value: string) => number[] }
> = {};

for (let i = 1; i < 33; i++) {
types[`uint${i * 8}[CompactBytesArray]`] = {
encode: (value: number[]) => encodeUintNCompactBytesArray(value, i),
decode: (value: string) => decodeUintNCompactBytesArray(value, i),
};
}
return types;
};

/**
* Encodes any set of strings to string[CompactBytesArray]
*
* @param values An array of non restricted strings
* @returns string[CompactBytesArray]
*/
const encodeStringCompactBytesArray = (values: string[]): string => {
const hexValues: string[] = values.map((element) => utf8ToHex(element));

return encodeCompactBytesArray(hexValues);
};

/**
* Decode a string[CompactBytesArray] to an array of strings
* @param compactBytesArray A string[CompactBytesArray]
* @returns An array of strings
*/
const decodeStringCompactBytesArray = (compactBytesArray: string): string[] => {
const hexValues: string[] = decodeCompactBytesArray(compactBytesArray);
const stringValues: string[] = hexValues.map((element) => hexToUtf8(element));

return stringValues;
};

const valueTypeEncodingMap = {
string: {
encode: (value: string) => abiCoder.encodeParameter('string', value),
Expand Down Expand Up @@ -199,6 +365,12 @@ const valueTypeEncodingMap = {
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 bellow
Expand Down

0 comments on commit 9268a32

Please sign in to comment.