Skip to content

Commit

Permalink
feat: add encoding / decoding for bytes[CompactBytesArray] (#261)
Browse files Browse the repository at this point in the history
* feat: add  encoding / decoding for `bytes[CompactBytesArray]`

Co-authored-by: b00ste.lyx <[email protected]>

* chore: fix linter errors

* test: add test for empty elements

* refactor: array filter/reduce + simplify large values in tests

* test: put back the `0x` prefix

* chore: fix linter error

* refactor: return empty string when decoding CompactBytesArray

* test: re-organise unit tests

* chore: fix linter error

* chore: `let` > `const`

Co-authored-by: b00ste.lyx <[email protected]>
  • Loading branch information
CJ42 and b00ste authored Jan 24, 2023
1 parent 9066ee7 commit 8d3e4e9
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 0 deletions.
89 changes: 89 additions & 0 deletions src/lib/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ describe('encoder', () => {
encodedValue:
'0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000001337000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000054ef',
},
{
valueType: 'bytes[CompactBytesArray]',
decodedValue: [
'0xaabb',
'0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe',
'0xbeefbeefbeefbeefbeef',
],
encodedValue:
'0x0002aabb0020cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe000abeefbeefbeefbeefbeef',
},
{
valueType: 'bytes[CompactBytesArray]',
decodedValue: [`0x${'cafe'.repeat(256)}`, `0x${'beef'.repeat(250)}`],
encodedValue: `0x0200${'cafe'.repeat(256)}01f4${'beef'.repeat(250)}`,
},
];

testCases.forEach((testCase) => {
Expand All @@ -134,6 +149,80 @@ describe('encoder', () => {
});
});

describe('when encoding bytes[CompactBytesArray]', () => {
it('should encode `0x` elements as `0x0000`', async () => {
const testCase = {
valueType: 'bytes[CompactBytesArray]',
decodedValue: ['0xaabb', '0x', '0x', '0xbeefbeefbeefbeefbeef'],
encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef',
};

const encodedValue = encodeValueType(
testCase.valueType,
testCase.decodedValue,
);
assert.deepStrictEqual(encodedValue, testCase.encodedValue);
});

it("should encode '' (empty strings) elements as `0x0000`", async () => {
const testCase = {
valueType: 'bytes[CompactBytesArray]',
decodedValue: ['0xaabb', '', '', '0xbeefbeefbeefbeefbeef'],
encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef',
};

const encodedValue = encodeValueType(
testCase.valueType,
testCase.decodedValue,
);
assert.deepStrictEqual(encodedValue, testCase.encodedValue);
});

it('should throw when trying to encode a array that contains non hex string as `bytes[CompactBytesArray]`', async () => {
expect(() => {
encodeValueType('bytes[CompactBytesArray]', [
'some random string',
'another random strings',
'0xaabbccdd',
]);
}).to.throw(
"Couldn't encode bytes[CompactBytesArray], value at index 0 is not hex",
);
});

it('should throw when trying to encode a `bytes[CompactBytesArray]` with a bytes length bigger than 65_535', async () => {
expect(() => {
encodeValueType('bytes[CompactBytesArray]', [
'0x' + 'ab'.repeat(66_0000),
]);
}).to.throw(
"Couldn't encode bytes[CompactBytesArray], value at index 0 exceeds 65_535 bytes",
);
});
});

describe('when decoding a bytes[CompactBytesArray] that contains `0000` entries', () => {
it("should decode as '' (empty string) in the decoded array", async () => {
const testCase = {
valueType: 'bytes[CompactBytesArray]',
decodedValue: ['0xaabb', '', '', '0xbeefbeefbeefbeefbeef'],
encodedValue: '0x0002aabb00000000000abeefbeefbeefbeefbeef',
};

const decodedValue = decodeValueType(
testCase.valueType,
testCase.encodedValue,
);
assert.deepStrictEqual(decodedValue, testCase.decodedValue);
});

it('should throw when trying to decode a `bytes[CompactBytesArray]` with an invalid length byte', async () => {
expect(() => {
decodeValueType('bytes[CompactBytesArray]', '0x0005cafe');
}).to.throw("Couldn't decode bytes[CompactBytesArray]");
});
});

it('should throw when valueType is unknown', () => {
assert.throws(
() => {
Expand Down
67 changes: 67 additions & 0 deletions src/lib/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
padLeft,
toChecksumAddress,
utf8ToHex,
stripHexPrefix,
} from 'web3-utils';

import { JSONURLDataToEncode, URLDataWithHash } from '../types';
Expand Down Expand Up @@ -80,6 +81,68 @@ const decodeDataSourceWithHash = (value: string): URLDataWithHash => {
return { hashFunction: hashFunction.name, hash: dataHash, url: dataSource };
};

const encodeCompactBytesArray = (values: string[]): string => {
const compactBytesArray = values
.filter((value, index) => {
if (!isHex(value)) {
throw new Error(
`Couldn't encode bytes[CompactBytesArray], value at index ${index} is not hex`,
);
}

if (value.length > 65_535 * 2 + 2) {
throw new Error(
`Couldn't encode bytes[CompactBytesArray], value at index ${index} exceeds 65_535 bytes`,
);
}

return true;
})
.reduce((acc, value) => {
const numberOfBytes = stripHexPrefix(value).length / 2;
const hexNumber = padLeft(numberToHex(numberOfBytes), 4);
return acc + stripHexPrefix(hexNumber) + stripHexPrefix(value);
}, '0x');

return compactBytesArray;
};

const decodeCompactBytesArray = (compactBytesArray: string): string[] => {
if (!isHex(compactBytesArray))
throw new Error("Couldn't decode, value is not hex");

let pointer = 0;
const encodedValues: string[] = [];

const strippedCompactBytesArray = stripHexPrefix(compactBytesArray);

while (pointer < strippedCompactBytesArray.length) {
const length = hexToNumber(
'0x' + strippedCompactBytesArray.slice(pointer, pointer + 4),
);

if (length === 0) {
// empty entries (`0x0000`) in a CompactBytesArray are returned as empty entries in the array
encodedValues.push('');
} else {
encodedValues.push(
'0x' +
strippedCompactBytesArray.slice(
pointer + 4,
pointer + 2 * (length + 2),
),
);
}

pointer += 2 * (length + 2);
}

if (pointer > strippedCompactBytesArray.length)
throw new Error("Couldn't decode bytes[CompactBytesArray]");

return encodedValues;
};

const valueTypeEncodingMap = {
string: {
encode: (value: string) => abiCoder.encodeParameter('string', value),
Expand Down Expand Up @@ -132,6 +195,10 @@ const valueTypeEncodingMap = {
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),
},
};

// Use enum for type bellow
Expand Down

0 comments on commit 8d3e4e9

Please sign in to comment.