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 encoding / decoding for bytes[CompactBytesArray] #261

Merged
merged 10 commits into from
Jan 24, 2023
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',
CJ42 marked this conversation as resolved.
Show resolved Hide resolved
'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
63 changes: 63 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,64 @@ 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[] = [];

compactBytesArray = stripHexPrefix(compactBytesArray);

while (pointer < compactBytesArray.length) {
const length = hexToNumber(
'0x' + compactBytesArray.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' + compactBytesArray.slice(pointer + 4, pointer + 2 * (length + 2)),
);
}

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

if (pointer > compactBytesArray.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 +191,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