Skip to content

Commit

Permalink
chore: refactor abi-coder
Browse files Browse the repository at this point in the history
  • Loading branch information
AlicanC committed May 14, 2022
1 parent 9e5deaf commit f6289c3
Show file tree
Hide file tree
Showing 15 changed files with 384 additions and 61 deletions.
59 changes: 59 additions & 0 deletions packages/abi-coder/src/__snapshots__/coders.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ArrayCoder as a [u8; 0] can encode [] then decode [] 1`] = `"0x"`;

exports[`ArrayCoder as a [u8; 4] can encode [0, 13, 37, 255] then decode [0, 13, 37, 255] 1`] = `"0x0000000000000000000000000000000d000000000000002500000000000000ff"`;

exports[`B256Coder as a b256 can encode "0x0000000000000000000000000000000000000000000000000000000000000000" then decode "0x0000000000000000000000000000000000000000000000000000000000000000" 1`] = `"0x0000000000000000000000000000000000000000000000000000000000000000"`;

exports[`B256Coder as a b256 can encode "0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b" then decode "0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b" 1`] = `"0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b"`;

exports[`BooleanCoder as a boolean can encode false then decode false 1`] = `"0x0000000000000000"`;

exports[`BooleanCoder as a boolean can encode true then decode true 1`] = `"0x0000000000000001"`;

exports[`ByteCoder as a byte can encode "0x00" then decode 0 1`] = `"0x0000000000000000"`;

exports[`ByteCoder as a byte can encode "0xff" then decode 255 1`] = `"0x00000000000000ff"`;

exports[`ByteCoder as a byte can encode 0 then decode 0 1`] = `"0x0000000000000000"`;

exports[`ByteCoder as a byte can encode 255 then decode 255 1`] = `"0x00000000000000ff"`;

exports[`NumberCoder as a u8 can encode 0 then decode 0 1`] = `"0x0000000000000000"`;

exports[`NumberCoder as a u8 can encode 0n then decode 0 1`] = `"0x0000000000000000"`;

exports[`NumberCoder as a u8 can encode 255 then decode 255 1`] = `"0x00000000000000ff"`;

exports[`NumberCoder as a u8 can encode 255n then decode 255 1`] = `"0x00000000000000ff"`;

exports[`NumberCoder as a u16 can encode 0 then decode 0 1`] = `"0x0000000000000000"`;

exports[`NumberCoder as a u16 can encode 0n then decode 0 1`] = `"0x0000000000000000"`;

exports[`NumberCoder as a u16 can encode 65535 then decode 65535 1`] = `"0x000000000000ffff"`;

exports[`NumberCoder as a u16 can encode 65535n then decode 65535 1`] = `"0x000000000000ffff"`;

exports[`NumberCoder as a u32 can encode 0 then decode 0 1`] = `"0x0000000000000000"`;

exports[`NumberCoder as a u32 can encode 0n then decode 0 1`] = `"0x0000000000000000"`;

exports[`NumberCoder as a u32 can encode 4294967295 then decode 4294967295 1`] = `"0x00000000ffffffff"`;

exports[`NumberCoder as a u32 can encode 4294967295n then decode 4294967295 1`] = `"0x00000000ffffffff"`;

exports[`NumberCoder as a u64 can encode 0 then decode 0n 1`] = `"0x0000000000000000"`;

exports[`NumberCoder as a u64 can encode 0n then decode 0n 1`] = `"0x0000000000000000"`;

exports[`NumberCoder as a u64 can encode 18446744073709551615n then decode 18446744073709551615n 1`] = `"0xffffffffffffffff"`;

exports[`StringCoder as a str[0] can encode "" then decode "" 1`] = `"0x"`;

exports[`StringCoder as a str[255] can encode "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" then decode "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 1`] = `"0x61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616100"`;

exports[`TupleCoder as a () can encode [] then decode [] 1`] = `"0x"`;

exports[`TupleCoder as a (u64, u64) can encode [13, 37] then decode [13n, 37n] 1`] = `"0x000000000000000d0000000000000025"`;
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe('AbiCoder', () => {
encoded
) as DecodedValue[];

expect(Array.from(decoded)).toEqual([255n]);
expect(Array.from(decoded)).toEqual([255]);
});

it('encodes and decodes boolean', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/abi-coder/src/abi-coder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { BytesLike } from '@ethersproject/bytes';
import { arrayify, hexConcat } from '@ethersproject/bytes';
import { Logger } from '@ethersproject/logger';

import type { DecodedValue, Values } from './coders/abstract-coder';
import type { DecodedValue, InputValue } from './coders/abstract-coder';
import type Coder from './coders/abstract-coder';
import ArrayCoder from './coders/array';
import B256Coder from './coders/b256';
Expand All @@ -12,8 +12,8 @@ import ByteCoder from './coders/byte';
import NumberCoder from './coders/number';
import StringCoder from './coders/string';
import TupleCoder from './coders/tuple';
import { filterEmptyParams } from './coders/utilities';
import type { JsonAbiFragmentType } from './json-abi';
import { filterEmptyParams } from './utilities';

const stringRegEx = /str\[([0-9]+)\]/;
const arrayRegEx = /\[(\w+);\s*([0-9]+)\]/;
Expand Down Expand Up @@ -93,7 +93,7 @@ export default class AbiCoder {

encode(
types: ReadonlyArray<JsonAbiFragmentType>,
values: Values[] | Record<string, Values>
values: InputValue[] | Record<string, InputValue>
): string {
const nonEmptyTypes = filterEmptyParams(types);

Expand Down
210 changes: 210 additions & 0 deletions packages/abi-coder/src/coders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { hexlify } from '@ethersproject/bytes';
import { toHex } from '@fuel-ts/math';

import type Coder from './coders/abstract-coder';
import ArrayCoder from './coders/array';
import B256Coder from './coders/b256';
import BooleanCoder from './coders/boolean';
import ByteCoder from './coders/byte';
import NumberCoder from './coders/number';
import StringCoder from './coders/string';
import TupleCoder from './coders/tuple';

const B256_ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000';
const B256 = '0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b';
const U8_MAX = 2 ** 8 - 1;
const U16_MAX = 2 ** 16 - 1;
const U32_MAX = 2 ** 32 - 1;
const U64_MAX = 2n ** 64n - 1n;

/**
* Tests for implementations of Coder.
*
* This is an array of tuples containing one item for each Coder.
*
* Each item is another tuple, containing:
* - The name of the Coder
* - A list of good cases we expect to succeed
* - A list of bad cases we expect to fail
*
* And each case is also a tuple, containing:
* - An instance of the Coder
* - InputValue to provide to `coder.encode`
* - DecodedValue to expect from `coder.decode`
*/
const testCases = [
[
'ArrayCoder',
[
[new ArrayCoder(new NumberCoder('', 'u8'), 0, ''), [], []],
[new ArrayCoder(new NumberCoder('', 'u8'), 4, ''), [0, 13, 37, U8_MAX], [0, 13, 37, U8_MAX]],
],
[
// Under
[new ArrayCoder(new NumberCoder('', 'u8'), 1, ''), [], []],
// Over
[new ArrayCoder(new NumberCoder('', 'u8'), 1, ''), [1, 2], [1, 2]],
// Wrong
[new ArrayCoder(new NumberCoder('', 'u8'), 1, ''), ['whoops'], ['whoops']],
[new ArrayCoder(new NumberCoder('', 'u8'), 1, ''), [U8_MAX + 1], [U8_MAX + 1]],
],
],
[
'B256Coder',
[
[new B256Coder('b256', ''), B256_ZERO, B256_ZERO],
[new B256Coder('b256', ''), B256, B256],
],
[
// Under
[
new B256Coder('b256', ''),
B256_ZERO.slice(0, B256_ZERO.length - 1),
B256_ZERO.slice(0, B256_ZERO.length - 1),
],
// Over
[new B256Coder('b256', ''), `${B256_ZERO}0`, `${B256_ZERO}0`],
[new B256Coder('b256', ''), `${B256_ZERO}00`, `${B256_ZERO}00`],
// Wrong
[new B256Coder('b256', ''), 'whoops', 'whoops'],
],
],
[
'BooleanCoder',
[
[new BooleanCoder(''), true, true],
[new BooleanCoder(''), false, false],
],
[
// Under
[new BooleanCoder(''), -1, -1],
// Over
[new BooleanCoder(''), 2, 2],
// Wrong
[new BooleanCoder(''), 'a', 'a'],
],
],
[
'ByteCoder',
[
// `string` inputs
[new ByteCoder(''), toHex(0), 0],
[new ByteCoder(''), toHex(U8_MAX), U8_MAX],
// `number` inputs
[new ByteCoder(''), 0, 0],
[new ByteCoder(''), U8_MAX, 255],
],
[
// Under
[new ByteCoder(''), -1, -1],
// Over
[new ByteCoder(''), toHex(U8_MAX + 1), U8_MAX + 1],
[new ByteCoder(''), U8_MAX + 1, U8_MAX + 1],
// Wrong
[new ByteCoder(''), 'whoops', 'whoops'],
],
],
[
'NumberCoder',
[
// `number` inputs
[new NumberCoder('', 'u8'), 0, 0],
[new NumberCoder('', 'u8'), U8_MAX, U8_MAX],
[new NumberCoder('', 'u16'), 0, 0],
[new NumberCoder('', 'u16'), U16_MAX, U16_MAX],
[new NumberCoder('', 'u32'), 0, 0],
[new NumberCoder('', 'u32'), U32_MAX, U32_MAX],
[new NumberCoder('', 'u64'), 0, 0n],
// `bigint` inputs
[new NumberCoder('', 'u8'), 0n, 0],
[new NumberCoder('', 'u8'), BigInt(U8_MAX), U8_MAX],
[new NumberCoder('', 'u16'), 0n, 0],
[new NumberCoder('', 'u16'), BigInt(U16_MAX), U16_MAX],
[new NumberCoder('', 'u32'), 0n, 0],
[new NumberCoder('', 'u32'), BigInt(U32_MAX), U32_MAX],
[new NumberCoder('', 'u64'), 0n, 0n],
[new NumberCoder('', 'u64'), U64_MAX, U64_MAX],
],
[
// Under
[new NumberCoder('', 'u8'), -1, -1],
[new NumberCoder('', 'u16'), -1, -1],
[new NumberCoder('', 'u32'), -1, -1],
[new NumberCoder('', 'u64'), -1n, -1n],
// Over
[new NumberCoder('', 'u8'), U8_MAX + 1, U8_MAX + 1],
[new NumberCoder('', 'u16'), U16_MAX + 1, U16_MAX + 1],
[new NumberCoder('', 'u32'), U32_MAX + 1, U32_MAX + 1],
[new NumberCoder('', 'u64'), U64_MAX + 1n, U64_MAX + 1n],
// Wrong
[new NumberCoder('', 'u8'), 'whoops', 'whoops'],
],
],
[
'StringCoder',
[
[new StringCoder('', 0), '', ''],
[new StringCoder('', U8_MAX), 'a'.repeat(U8_MAX), 'a'.repeat(U8_MAX)],
],
[
// Under
[new StringCoder('', 0), 'a', 'a'],
[new StringCoder('', 1), '', ''],
// Over
[new StringCoder('', 1), 'aa', 'aa'],
// Wrong
[new StringCoder('', 1), 'aa', 'aa'],
],
],
[
'TupleCoder',
[
[new TupleCoder([], ''), [], []],
[
new TupleCoder([new NumberCoder('', 'u64'), new NumberCoder('', 'u64')], ''),
[13, 37],
[13n, 37n],
],
],
[
// Under
[new TupleCoder([new NumberCoder('', 'u8')], ''), [], []],
// Over
[new TupleCoder([new NumberCoder('', 'u8')], ''), [U8_MAX, U8_MAX], [U8_MAX, U8_MAX]],
// Wrong
[new TupleCoder([new NumberCoder('', 'u8')], ''), [U8_MAX + 1], [U8_MAX + 1]],
[new TupleCoder([new NumberCoder('', 'u8')], ''), ['whoops'], ['whoops']],
],
],
] as const;

describe.each(testCases)('%s', (coderName, goodCases, badCases) => {
it.each(
goodCases.map(([coder, input, output]): [string, any, any, Coder] => [
coder.getAbiType(),
input,
output,
coder,
])
)('as a %s can encode %p then decode %p', (abiType, input, output, coder) => {
const encoded = coder.encode(input);
expect(hexlify(encoded)).toMatchSnapshot();
const [decoded] = coder.decode(encoded, 0);
expect(decoded).toEqual(output);
});
it.each(
badCases.map(([coder, input, output]): [string, any, any, Coder] => [
coder.getAbiType(),
input,
output,
coder,
])
)('as a %s can not encode %p then decode %p', (abiType, input, output, coder) => {
expect(() => {
const encoded = coder.encode(input);
const [decoded] = coder.decode(encoded, 0);
expect(decoded).toEqual(output);
}).toThrow();
});
});
28 changes: 21 additions & 7 deletions packages/abi-coder/src/coders/abstract-coder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@ const logger = new Logger(process.env.BUILD_VERSION || '~');

type Primitive = string | number | boolean | bigint;

export type Values =
/**
* The type of value you can provide to `Coder.encode`
*/
export type InputValue =
| Primitive
| BytesLike
| Values[]
| { [key: string]: Values }
| InputValue[]
| { [key: string]: InputValue }
| Record<string, Primitive | BytesLike>;

/**
* The type of value you can get from `Coder.decode`
*/
export type DecodedValue =
| Primitive
| DecodedValue[]
| { [key: string]: DecodedValue }
| Record<string, Primitive>;

export default abstract class Coder {
export default abstract class Coder<
TInput extends InputValue = InputValue,
TDecoded extends DecodedValue = DecodedValue
> {
// The coder name:
// - address, uint256, tuple, array, etc.
readonly name: string;
Expand All @@ -38,11 +47,16 @@ export default abstract class Coder {
this.localName = localName;
}

throwError(message: string, value: unknown): void {
throwError(message: string, value: unknown): never {
logger.throwArgumentError(message, this.name, value);
// `logger.throwArgumentError` throws, but TS doesn't know it
// so we throw here to make sure our `never` works
throw new Error('unreachable');
}

abstract encode(value: Values, length?: number): Uint8Array;
abstract getAbiType(): string;

abstract decode(data: Uint8Array, offset: number, length?: number): [DecodedValue, number];
abstract encode(value: TInput, length?: number): Uint8Array;

abstract decode(data: Uint8Array, offset: number, length?: number): [TDecoded, number];
}
Loading

0 comments on commit f6289c3

Please sign in to comment.