From b1d0ab518932ebfa57d3a774d738c8f8d2dba969 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 4 Dec 2024 17:57:49 +0000 Subject: [PATCH 1/4] feat: added feature complete fixed based coders --- packages/abi/package.json | 1 + packages/abi/src/coder/abi-coder-types.ts | 60 ++++- .../src/coder/encoding/encoding-constants.ts | 21 ++ .../abi/src/coder/encoding/encoding-types.ts | 46 ++++ packages/abi/src/coder/encoding/encoding.ts | 11 + packages/abi/src/coder/encoding/index.ts | 2 + packages/abi/src/coder/encoding/v1/fixed.ts | 218 ++++++++++++++++++ packages/abi/src/coder/encoding/v1/index.ts | 26 +++ packages/abi/src/coder/encoding/validation.ts | 64 +++++ packages/abi/src/coder/index.ts | 1 + packages/abi/test/encoding/v1/b256.test.ts | 134 +++++++++++ packages/abi/test/encoding/v1/b512.test.ts | 142 ++++++++++++ packages/abi/test/encoding/v1/boolean.test.ts | 104 +++++++++ packages/abi/test/encoding/v1/u16.test.ts | 110 +++++++++ packages/abi/test/encoding/v1/u256.test.ts | 144 ++++++++++++ packages/abi/test/encoding/v1/u32.test.ts | 110 +++++++++ packages/abi/test/encoding/v1/u64.test.ts | 140 +++++++++++ packages/abi/test/encoding/v1/u8.test.ts | 106 +++++++++ packages/abi/test/encoding/v1/void.test.ts | 28 +++ packages/abi/test/utils/vitest.matcher.ts | 20 ++ pnpm-lock.yaml | 9 +- 21 files changed, 1493 insertions(+), 4 deletions(-) create mode 100644 packages/abi/src/coder/encoding/encoding-constants.ts create mode 100644 packages/abi/src/coder/encoding/encoding-types.ts create mode 100644 packages/abi/src/coder/encoding/encoding.ts create mode 100644 packages/abi/src/coder/encoding/index.ts create mode 100644 packages/abi/src/coder/encoding/v1/fixed.ts create mode 100644 packages/abi/src/coder/encoding/validation.ts create mode 100644 packages/abi/test/encoding/v1/b256.test.ts create mode 100644 packages/abi/test/encoding/v1/b512.test.ts create mode 100644 packages/abi/test/encoding/v1/boolean.test.ts create mode 100644 packages/abi/test/encoding/v1/u16.test.ts create mode 100644 packages/abi/test/encoding/v1/u256.test.ts create mode 100644 packages/abi/test/encoding/v1/u32.test.ts create mode 100644 packages/abi/test/encoding/v1/u64.test.ts create mode 100644 packages/abi/test/encoding/v1/u8.test.ts create mode 100644 packages/abi/test/encoding/v1/void.test.ts create mode 100644 packages/abi/test/utils/vitest.matcher.ts diff --git a/packages/abi/package.json b/packages/abi/package.json index 0fd951ba435..41e3d7d1bed 100644 --- a/packages/abi/package.json +++ b/packages/abi/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@fuel-ts/errors": "workspace:*", + "@fuel-ts/math": "workspace:*", "@fuel-ts/utils": "workspace:*" }, "devDependencies": {} diff --git a/packages/abi/src/coder/abi-coder-types.ts b/packages/abi/src/coder/abi-coder-types.ts index 10051c76805..73a4598b7b3 100644 --- a/packages/abi/src/coder/abi-coder-types.ts +++ b/packages/abi/src/coder/abi-coder-types.ts @@ -1 +1,59 @@ -// Placeholder +import type { BytesLike } from '@fuel-ts/interfaces'; +import type { BN } from '@fuel-ts/math'; + +import type { CoderType } from './encoding'; + +export type Primitive = string | number | boolean; + +export type Option = T | undefined; + +/** + * The type of value you can provide to `Coder.encode` + */ +export type InputValue = + | Primitive + | BN + | Option + | BytesLike + | InputValue[] + | { [key: string]: InputValue } + | Record; + +/** + * The type of value you can get from `Coder.decode` + */ +export type DecodedValue = + | Primitive + | DecodedValue[] + | { [key: string]: DecodedValue } + | Record; + +export abstract class Coder { + /** + * The type of the coder + * + * @see {@link CoderType} for a list of supported types + */ + abstract type: CoderType; + + /** + * Encode a value. + * + * @param value - The value to encode. + * @returns The encoded value. + * + * @throws FuelError - with a ENCODE_ERROR code + */ + abstract encode(value: TEncoded): Uint8Array; + + /** + * Decode a value. + * + * @param value - The value to decode. + * @param offset - The offset to start decoding from. + * @returns A tuple with the [decoded value, offset of the final decoded] + * + * @throws FuelError - with a DECODE_ERROR code + */ + abstract decode(value: Uint8Array, offset?: number): [TDecoded, number]; +} diff --git a/packages/abi/src/coder/encoding/encoding-constants.ts b/packages/abi/src/coder/encoding/encoding-constants.ts new file mode 100644 index 00000000000..79ebb8ffd7b --- /dev/null +++ b/packages/abi/src/coder/encoding/encoding-constants.ts @@ -0,0 +1,21 @@ +export const BOOL_TYPE = 'bool'; +export const U8_TYPE = 'u8'; +export const U16_TYPE = 'u16'; +export const U32_TYPE = 'u32'; +export const U64_TYPE = 'u64'; +export const U256_TYPE = 'u256'; +export const B256_TYPE = 'b256'; +export const B512_TYPE = 'b512'; +export const VOID_TYPE = 'void'; + +export const ENCODING_TYPES = [ + U8_TYPE, + U16_TYPE, + U32_TYPE, + U64_TYPE, + U256_TYPE, + BOOL_TYPE, + B256_TYPE, + B512_TYPE, + VOID_TYPE, +] as const; diff --git a/packages/abi/src/coder/encoding/encoding-types.ts b/packages/abi/src/coder/encoding/encoding-types.ts new file mode 100644 index 00000000000..7dc43f21e72 --- /dev/null +++ b/packages/abi/src/coder/encoding/encoding-types.ts @@ -0,0 +1,46 @@ +import type { + BOOL_TYPE, + U256_TYPE, + U32_TYPE, + U64_TYPE, + U8_TYPE, + U16_TYPE, + B256_TYPE, + VOID_TYPE, + B512_TYPE, + ENCODING_TYPES, +} from './encoding-constants'; +import type { voidCoder, u16, u32, u8, u64, u256, b256, b512, bool } from './v1/fixed'; + +/** + * A type of coder. + * + * Supported types: 'u8' | 'u16' | 'u32' | 'u64' | 'u256' | 'bool' | 'b256' | 'b512' | 'void' + * @see {@link ENCODING_TYPES} for a list of all supported types + */ +export type CoderType = (typeof ENCODING_TYPES)[number]; + +/** + * All the supported coders, across all versions. + */ +export type SupportedCoders = SupportedCodersV1; + +/** + * A supported coder. + */ +export type SupportedCoder = SupportedCoders[keyof SupportedCoders]; + +/** + * Supported coders for version 1. + */ +export interface SupportedCodersV1 { + [U8_TYPE]: typeof u8; + [U16_TYPE]: typeof u16; + [U32_TYPE]: typeof u32; + [U64_TYPE]: typeof u64; + [U256_TYPE]: typeof u256; + [BOOL_TYPE]: typeof bool; + [B256_TYPE]: typeof b256; + [B512_TYPE]: typeof b512; + [VOID_TYPE]: typeof voidCoder; +} diff --git a/packages/abi/src/coder/encoding/encoding.ts b/packages/abi/src/coder/encoding/encoding.ts new file mode 100644 index 00000000000..8ee2fc98205 --- /dev/null +++ b/packages/abi/src/coder/encoding/encoding.ts @@ -0,0 +1,11 @@ +import type { SupportedCodersV1 } from './encoding-types'; +import { v1 } from './v1'; + +export class AbiEncoding { + /** + * Encoding version 1 + * + * See {@link https://docs.fuel.network/docs/specs/abi/json-abi-format/} + */ + static v1: SupportedCodersV1 = v1; +} diff --git a/packages/abi/src/coder/encoding/index.ts b/packages/abi/src/coder/encoding/index.ts new file mode 100644 index 00000000000..a9e9150ac99 --- /dev/null +++ b/packages/abi/src/coder/encoding/index.ts @@ -0,0 +1,2 @@ +export { AbiEncoding } from './encoding'; +export * from './encoding-types'; diff --git a/packages/abi/src/coder/encoding/v1/fixed.ts b/packages/abi/src/coder/encoding/v1/fixed.ts new file mode 100644 index 00000000000..2db6efe73d6 --- /dev/null +++ b/packages/abi/src/coder/encoding/v1/fixed.ts @@ -0,0 +1,218 @@ +import { FuelError } from '@fuel-ts/errors'; +import type { BN, BNInput } from '@fuel-ts/math'; +import { toNumber, toBytes, bn, toHex } from '@fuel-ts/math'; +import { arrayify } from '@fuel-ts/utils'; + +import type { Coder } from '../../abi-coder-types'; +import { + B256_TYPE, + B512_TYPE, + BOOL_TYPE, + U16_TYPE, + U256_TYPE, + U32_TYPE, + U64_TYPE, + U8_TYPE, + VOID_TYPE, +} from '../encoding-constants'; +import type { CoderType } from '../encoding-types'; +import { + assertBnValueByteLengthLessThan, + assertBnValueNonNegative, + assertBooleanValue, + assertEncodedLengthEquals, +} from '../validation'; + +/** + * A number coder (u8, u16, u32) + * + * @param encodedLength - The number of bytes to encode the value. + * @param type - The type of the coder. + * @returns A number based coder. + */ +const createNumberCoder = (encodedLength: number, type: CoderType): Coder => ({ + type, + /** + * Encode a number value. + * + * @param value - The number value to encode. + * @returns The encoded number value. + */ + encode: (value: number): Uint8Array => { + const bnValue = bn(value); + assertBnValueNonNegative(bnValue, type); + assertBnValueByteLengthLessThan(bnValue, encodedLength, type); + return toBytes(bnValue, encodedLength); + }, + decode: (data: Uint8Array, offset: number = 0): [number, number] => { + const elementData = data.slice(offset, offset + encodedLength); + assertEncodedLengthEquals(elementData, encodedLength, type); + return [toNumber(elementData), offset + encodedLength]; + }, +}); + +/** + * A big number coder (u64, u256) + * + * @param encodedLength - The number of bytes to encode the value. + * @param type - The type of the coder. + * @returns A big number based coder. + */ +const createBigNumberCoder = (encodedLength: number, type: CoderType): Coder => ({ + type, + /** + * Encode a big number value. + * + * @param value - The big number value to encode. + * @returns The encoded big number value. + */ + encode: (value: BN | BNInput): Uint8Array => { + let bnValue; + try { + bnValue = bn(value); + } catch (error) { + throw new FuelError( + FuelError.CODES.ENCODE_ERROR, + `Invalid ${type} value - expected a BNInput.`, + { value } + ); + } + assertBnValueNonNegative(bnValue, type); + assertBnValueByteLengthLessThan(bnValue, encodedLength, type); + return toBytes(bnValue, encodedLength); + }, + /** + * Decode a big number value. + * + * @param data - The encoded data to decode. + * @param offset - The offset to start decoding from. + * @returns A tuple with the [decoded value, offset of the final decoded] + */ + decode: (data: Uint8Array, offset: number = 0): [BN, number] => { + const elementData = data.slice(offset, offset + encodedLength); + assertEncodedLengthEquals(elementData, encodedLength, type); + return [bn(elementData), offset + encodedLength]; + }, +}); + +/** + * A hex coder (b256, b512) + * + * @param encodedLength - The number of bytes to encode the value. + * @param type - The type of the coder. + * @returns A hex based coder. + */ +const createHexCoder = (encodedLength: number, type: CoderType): Coder => ({ + type, + /** + * Encode a hex value. + * + * @param value - The hex value to encode. + * @returns The encoded hex value. + * + * @throws {@link FuelError} - when a malformed hex value is provided + * @throws {@link FuelError} - when the encoded value's byte length does not match the expected length + */ + encode: (value: string): Uint8Array => { + let encodedValue; + try { + encodedValue = arrayify(value); + } catch (error) { + throw new FuelError( + FuelError.CODES.ENCODE_ERROR, + `Invalid ${type} value - malformed hex value.`, + { + value, + expectedLength: encodedLength, + } + ); + } + + assertEncodedLengthEquals(encodedValue, encodedLength, type); + return encodedValue; + }, + /** + * Decode a hex value. + * (If the decoded value is zero, it will be padded with zeros to match the expected length.) + * + * @param data - The encoded data to decode. + * @param offset - The offset to start decoding from. + * @returns A tuple with the [decoded value, offset of the final decoded] + * + * @throws {@link FuelError} - when the encoded data length does not match the expected length + */ + decode: (data: Uint8Array, offset: number = 0): [string, number] => { + let bytes = data.slice(offset, offset + encodedLength); + assertEncodedLengthEquals(bytes, encodedLength, type); + + if (bn(bytes).isZero()) { + bytes = new Uint8Array(encodedLength); + } + return [toHex(bytes, encodedLength), offset + encodedLength]; + }, +}); + +export const u8: Coder = createNumberCoder(1, U8_TYPE); +export const u16: Coder = createNumberCoder(2, U16_TYPE); +export const u32: Coder = createNumberCoder(4, U32_TYPE); +export const u64: Coder = createBigNumberCoder(8, U64_TYPE); +export const u256: Coder = createBigNumberCoder(32, U256_TYPE); +export const b256: Coder = createHexCoder(32, B256_TYPE); +export const b512: Coder = createHexCoder(64, B512_TYPE); +export const voidCoder: Coder = { + type: VOID_TYPE, + /** + * Encode a void value. + * + * @returns The encoded void value. + */ + encode: (): Uint8Array => new Uint8Array(), + /** + * Decode a void value. + * + * @param data - The encoded data to decode. + * @param offset - The offset to start decoding from. + * @returns A tuple with the [decoded value, offset of the final decoded] + */ + decode: (_data: Uint8Array, offset: number = 0): [undefined, number] => [undefined, offset], +}; + +export const bool: Coder = { + type: BOOL_TYPE, + /** + * Encode a boolean value. + * + * @param value - The boolean value to encode. + * @returns The encoded boolean value. + */ + encode: (value: boolean): Uint8Array => { + assertBooleanValue(value); + return toBytes(value ? 1 : 0); + }, + /** + * Decode a boolean value. + * + * @param data - The encoded data to decode. + * @param offset - The offset to start decoding from. + * @returns A tuple with the [decoded value, offset of the final decoded] + */ + decode: (data: Uint8Array, initialOffset: number = 0): [boolean, number] => { + if (data.length < 1) { + throw new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid boolean data - not enough data.', { + data, + type: BOOL_TYPE, + expectedLength: 1, + }); + } + + const [value, offset] = u8.decode(data, initialOffset); + if (value !== 0 && value !== 1) { + throw new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid boolean value.', { + data, + value, + type: BOOL_TYPE, + }); + } + return [Boolean(value), offset]; + }, +}; diff --git a/packages/abi/src/coder/encoding/v1/index.ts b/packages/abi/src/coder/encoding/v1/index.ts index e69de29bb2d..263f4374184 100644 --- a/packages/abi/src/coder/encoding/v1/index.ts +++ b/packages/abi/src/coder/encoding/v1/index.ts @@ -0,0 +1,26 @@ +import { + B256_TYPE, + U256_TYPE, + U32_TYPE, + U64_TYPE, + B512_TYPE, + BOOL_TYPE, + VOID_TYPE, + U8_TYPE, + U16_TYPE, +} from '../encoding-constants'; +import type { SupportedCodersV1 } from '../encoding-types'; + +import { voidCoder, u16, u32, u8, u64, u256, b256, b512, bool } from './fixed'; + +export const v1: SupportedCodersV1 = { + [U8_TYPE]: u8, + [U16_TYPE]: u16, + [U32_TYPE]: u32, + [U64_TYPE]: u64, + [U256_TYPE]: u256, + [BOOL_TYPE]: bool, + [B256_TYPE]: b256, + [B512_TYPE]: b512, + [VOID_TYPE]: voidCoder, +}; diff --git a/packages/abi/src/coder/encoding/validation.ts b/packages/abi/src/coder/encoding/validation.ts new file mode 100644 index 00000000000..74efb46c0f4 --- /dev/null +++ b/packages/abi/src/coder/encoding/validation.ts @@ -0,0 +1,64 @@ +import { FuelError } from '@fuel-ts/errors'; +import type { BN } from '@fuel-ts/math'; + +import type { CoderType } from './encoding-types'; + +/** + * Asserts that a value is a boolean. + * + * @throws {@link FuelError} - with a ENCODE_ERROR code if the value is not a boolean + */ +export const assertBooleanValue = (value: unknown) => { + if (typeof value !== 'boolean') { + throw new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid boolean value.', { value }); + } +}; + +/** + * Asserts that a BN value is non-negative. + * + * @throws {@link FuelError} - with a ENCODE_ERROR code if the value is less than zero + */ +export const assertBnValueNonNegative = (value: BN, type: string) => { + if (value.isNeg()) { + throw new FuelError( + FuelError.CODES.ENCODE_ERROR, + `Invalid ${type} value - value is less than zero.`, + { value: value.toString(), type } + ); + } +}; + +/** + * Asserts that a BN value's byte length is less than a maximum. + * + * @throws {@link FuelError} - with a ENCODE_ERROR code if the value's byte length exceeds the maximum + */ +export const assertBnValueByteLengthLessThan = (value: BN, max: number, type: string) => { + if (value.byteLength() > max) { + throw new FuelError( + FuelError.CODES.ENCODE_ERROR, + `Invalid ${type} value - value exceeds maximum.`, + { value: value.toString(), type } + ); + } +}; + +/** + * Asserts that the encoded data length matches the expected length + * + * @throws {@link FuelError} - with a DECODE_ERROR code if the encoded data length does not match the expected length + */ +export const assertEncodedLengthEquals = ( + data: Uint8Array, + expectedLength: number, + type: CoderType +) => { + if (data.length !== expectedLength) { + throw new FuelError(FuelError.CODES.DECODE_ERROR, `Invalid ${type} data - unexpected length.`, { + data, + type, + expectedLength, + }); + } +}; diff --git a/packages/abi/src/coder/index.ts b/packages/abi/src/coder/index.ts index 242ad2e5a73..298cd401eca 100644 --- a/packages/abi/src/coder/index.ts +++ b/packages/abi/src/coder/index.ts @@ -1 +1,2 @@ export { AbiCoder } from './abi-coder'; +export * from './encoding'; diff --git a/packages/abi/test/encoding/v1/b256.test.ts b/packages/abi/test/encoding/v1/b256.test.ts new file mode 100644 index 00000000000..23d965766b9 --- /dev/null +++ b/packages/abi/test/encoding/v1/b256.test.ts @@ -0,0 +1,134 @@ +import { ErrorCode, FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; + +import { AbiEncoding } from '../../../src'; + +describe('b256', () => { + describe('encode', () => { + it('should encode a b256 [zero]', () => { + const coder = AbiEncoding.v1.b256; + const expected = new Uint8Array(32).fill(0); + const value = '0x0000000000000000000000000000000000000000000000000000000000000000'; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it(`should encode a b256 [address]`, () => { + const coder = AbiEncoding.v1.b256; + const expected = new Uint8Array([ + 213, 87, 156, 70, 223, 204, 127, 24, 32, 112, 19, 230, 91, 68, 228, 203, 78, 44, 34, 152, + 244, 172, 69, 123, 168, 248, 39, 67, 243, 30, 147, 11, + ]); + const value = '0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b'; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should throw an error encoding a b256 [too short]', async () => { + const coder = AbiEncoding.v1.b256; + const value = '0xTooShort'; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256 value - malformed hex value.', { + value, + expectedLength: 32, + }) + ); + }); + + it('should throw an error encoding a b256 [too long]', async () => { + const coder = AbiEncoding.v1.b256; + const value = `0x${'a'.repeat(33)}`; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256 value - malformed hex value.', { + value, + expectedLength: 32, + }) + ); + }); + + it('should throw an error encoding a b256 [not a hex string]', async () => { + const coder = AbiEncoding.v1.b256; + const value = 'not a hex string'; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256 value - malformed hex value.') + ); + }); + }); + + describe('decode', () => { + it('should decode a b256 [zero]', () => { + const coder = AbiEncoding.v1.b256; + const expected = '0x0000000000000000000000000000000000000000000000000000000000000000'; + const data = new Uint8Array(32).fill(0); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(32); + }); + + it(`should decode a b256 [address]`, () => { + const coder = AbiEncoding.v1.b256; + const expected = '0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b'; + const data = new Uint8Array([ + 213, 87, 156, 70, 223, 204, 127, 24, 32, 112, 19, 230, 91, 68, 228, 203, 78, 44, 34, 152, + 244, 172, 69, 123, 168, 248, 39, 67, 243, 30, 147, 11, + ]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(32); + }); + + it('should throw an error decoding a b256 [too short]', async () => { + const coder = AbiEncoding.v1.b256; + const data = new Uint8Array(31); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data - unexpected length.', { + data, + expectedLength: 32, + }) + ); + }); + + it('should throw an error decoding a b256 [empty]', async () => { + const coder = AbiEncoding.v1.b256; + const data = new Uint8Array(0); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data - unexpected length.', { + data, + expectedLength: 32, + }) + ); + }); + + it('should throw an error decoding a b256 [with offset]', async () => { + const coder = AbiEncoding.v1.b256; + const offset = 10; + const data = new Uint8Array(32); + + await expectToThrowFuelError( + () => coder.decode(data, offset), + new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data - unexpected length.', { + data: data.slice(offset), + expectedLength: 32, + }) + ); + }); + }); +}); diff --git a/packages/abi/test/encoding/v1/b512.test.ts b/packages/abi/test/encoding/v1/b512.test.ts new file mode 100644 index 00000000000..c7812416294 --- /dev/null +++ b/packages/abi/test/encoding/v1/b512.test.ts @@ -0,0 +1,142 @@ +import { ErrorCode, FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; + +import { AbiEncoding } from '../../../src'; + +describe('b512', () => { + describe('encode', () => { + it('should encode a b512 [zero]', () => { + const coder = AbiEncoding.v1.b512; + const expected = new Uint8Array(64).fill(0); + const value = + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it(`should encode a b512 [address]`, () => { + const coder = AbiEncoding.v1.b512; + const expected = new Uint8Array([ + 142, 157, 218, 111, 119, 147, 116, 90, 197, 170, 207, 158, 144, 124, 174, 48, 178, 160, 31, + 223, 13, 35, 183, 117, 10, 133, 198, 164, 79, 202, 12, 41, 240, 144, 111, 157, 31, 30, 146, + 230, 161, 251, 60, 61, 206, 243, 204, 59, 60, 219, 170, 226, 126, 71, 185, 217, 164, 198, + 164, 252, 228, 207, 22, 178, + ]); + const value = + '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2'; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should throw an error encoding a b512 [too short]', async () => { + const coder = AbiEncoding.v1.b512; + const value = '0xTooShort'; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b512 value - malformed hex value.', { + value, + expectedLength: 64, + }) + ); + }); + + it('should throw an error encoding a b512 [too long]', async () => { + const coder = AbiEncoding.v1.b512; + const value = `0x${'a'.repeat(33)}`; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b512 value - malformed hex value.', { + value, + expectedLength: 64, + }) + ); + }); + + it('should throw an error encoding a b512 [not a hex string]', async () => { + const coder = AbiEncoding.v1.b512; + const value = 'not a hex string'; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b512 value - malformed hex value.') + ); + }); + }); + + describe('decode', () => { + it('should decode a b512 [zero]', () => { + const coder = AbiEncoding.v1.b512; + const expected = + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + const data = new Uint8Array(64).fill(0); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(64); + }); + + it(`should decode a b512 [address]`, () => { + const coder = AbiEncoding.v1.b512; + const expected = + '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2'; + const data = new Uint8Array([ + 142, 157, 218, 111, 119, 147, 116, 90, 197, 170, 207, 158, 144, 124, 174, 48, 178, 160, 31, + 223, 13, 35, 183, 117, 10, 133, 198, 164, 79, 202, 12, 41, 240, 144, 111, 157, 31, 30, 146, + 230, 161, 251, 60, 61, 206, 243, 204, 59, 60, 219, 170, 226, 126, 71, 185, 217, 164, 198, + 164, 252, 228, 207, 22, 178, + ]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(64); + }); + + it('should throw an error decoding a b512 [too short]', async () => { + const coder = AbiEncoding.v1.b512; + const data = new Uint8Array(31); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data - unexpected length.', { + data, + expectedLength: 64, + }) + ); + }); + + it('should throw an error decoding a b512 [empty]', async () => { + const coder = AbiEncoding.v1.b512; + const data = new Uint8Array(0); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data - unexpected length.', { + data, + expectedLength: 64, + }) + ); + }); + + it('should throw an error decoding a b512 [with offset]', async () => { + const coder = AbiEncoding.v1.b512; + const offset = 10; + const data = new Uint8Array(64); + + await expectToThrowFuelError( + () => coder.decode(data, offset), + new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data - unexpected length.', { + data: data.slice(offset), + expectedLength: 64, + }) + ); + }); + }); +}); diff --git a/packages/abi/test/encoding/v1/boolean.test.ts b/packages/abi/test/encoding/v1/boolean.test.ts new file mode 100644 index 00000000000..ff3681b9754 --- /dev/null +++ b/packages/abi/test/encoding/v1/boolean.test.ts @@ -0,0 +1,104 @@ +import { FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; + +import { AbiEncoding } from '../../../src'; + +describe('boolean', () => { + describe('encode', () => { + it('should encode a boolean [true]', () => { + const coder = AbiEncoding.v1.bool; + const value = true; + const expected = new Uint8Array([1]); + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should encode a boolean [false]', () => { + const coder = AbiEncoding.v1.bool; + const value = false; + const expected = new Uint8Array([0]); + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should throw when encoding an invalid boolean value [undefined]', async () => { + const coder = AbiEncoding.v1.bool; + const value = undefined; + + await expectToThrowFuelError( + // @ts-expect-error Expected non-boolean value + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid boolean value.', { + value: undefined, + }) + ); + }); + + it('should throw when encoding an invalid boolean value [string value]', async () => { + const coder = AbiEncoding.v1.bool; + const value = 'true'; + + await expectToThrowFuelError( + // @ts-expect-error Expected non-boolean value + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid boolean value.', { value: 'true' }) + ); + }); + }); + + describe('decode', () => { + it('should decode a boolean [1 = true]', () => { + const coder = AbiEncoding.v1.bool; + const value = new Uint8Array([1]); + const expected = true; + + const [actual, offset] = coder.decode(value); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(value.length); + }); + + it('should decode a boolean [0 = false]', () => { + const coder = AbiEncoding.v1.bool; + const value = new Uint8Array([0]); + const expected = false; + + const [actual, offset] = coder.decode(value); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(value.length); + }); + + it('should throw when decoding an invalid boolean value [empty]', async () => { + const coder = AbiEncoding.v1.bool; + const data = new Uint8Array(0); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid boolean data - not enough data.', { + data, + type: 'bool', + expectedLength: 1, + }) + ); + }); + + it('should throw when decoding an invalid boolean value [2]', async () => { + const coder = AbiEncoding.v1.bool; + const data = new Uint8Array([2]); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid boolean value.', { + data, + type: 'bool', + value: 2, + }) + ); + }); + }); +}); diff --git a/packages/abi/test/encoding/v1/u16.test.ts b/packages/abi/test/encoding/v1/u16.test.ts new file mode 100644 index 00000000000..2ec2d6355b1 --- /dev/null +++ b/packages/abi/test/encoding/v1/u16.test.ts @@ -0,0 +1,110 @@ +import { FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; + +import { AbiEncoding } from '../../../src'; + +describe('u16', () => { + describe('encode', () => { + it('should encode a u16 [min = 0]', () => { + const coder = AbiEncoding.v1.u16; + const expected = new Uint8Array([0, 0]); + const value = 0; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it(`should encode a u16 [max = 65535]`, () => { + const coder = AbiEncoding.v1.u16; + const expected = new Uint8Array([255, 255]); + const value = 65535; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should fail to encode value [min - 1 = -1]', async () => { + const coder = AbiEncoding.v1.u16; + const value = -1; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError( + FuelError.CODES.ENCODE_ERROR, + 'Invalid u16 value - value is less than zero.', + { + value: '-1', + type: 'u16', + } + ) + ); + }); + + it(`should fail to encode value [max + 1 = 65536]`, async () => { + const coder = AbiEncoding.v1.u16; + const value = 65536; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u16 value - value exceeds maximum.', { + value: '65536', + type: 'u16', + }) + ); + }); + }); + + describe('decode', () => { + it('should decode a u16 [min]', () => { + const coder = AbiEncoding.v1.u16; + const expected = 0; + const data = new Uint8Array([0, 0]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(2); + }); + + it(`should decode a u16 [max = 65535]`, () => { + const coder = AbiEncoding.v1.u16; + const expected = 65535; + const data = new Uint8Array([255, 255]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(2); + }); + + it('should throw when decoding invalid u16 data [empty]', async () => { + const coder = AbiEncoding.v1.u16; + const data = new Uint8Array(0); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u16 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u16', + expectedLength: 2, + }) + ); + }); + + it('should throw when decoding invalid u16 data [empty with offset]', async () => { + const coder = AbiEncoding.v1.u16; + const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + + await expectToThrowFuelError( + () => coder.decode(data, 8), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u16 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u16', + expectedLength: 2, + }) + ); + }); + }); +}); diff --git a/packages/abi/test/encoding/v1/u256.test.ts b/packages/abi/test/encoding/v1/u256.test.ts new file mode 100644 index 00000000000..36f6a58c815 --- /dev/null +++ b/packages/abi/test/encoding/v1/u256.test.ts @@ -0,0 +1,144 @@ +import { FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; + +import { AbiEncoding } from '../../../src'; +import { toEqualBn } from '../../utils/vitest.matcher'; + +expect.extend({ toEqualBn }); + +describe('u256', () => { + describe('encode', () => { + it('should encode a u256 [min = 0]', () => { + const coder = AbiEncoding.v1.u256; + const expected = new Uint8Array(32).fill(0); + const value = 0; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it(`should encode a u256 [max = 115792089237316195423570985008687907853269984665640564039457584007913129639935]`, () => { + const coder = AbiEncoding.v1.u256; + const expected = new Uint8Array(32).fill(255); + const value = + '115792089237316195423570985008687907853269984665640564039457584007913129639935'; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should fail to encode value [boolean]', async () => { + const coder = AbiEncoding.v1.u256; + const value = true; + + await expectToThrowFuelError( + // @ts-expect-error: a boolean value boolean + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u256 value - expected a BNInput.', { + value, + }) + ); + }); + + it('should fail to encode value [not a BNInput]', async () => { + const coder = AbiEncoding.v1.u256; + const value = 'not a BNInput'; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u256 value - expected a BNInput.', { + value, + }) + ); + }); + + it('should fail to encode value [min - 1 = -1]', async () => { + const coder = AbiEncoding.v1.u256; + const value = -1; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError( + FuelError.CODES.ENCODE_ERROR, + 'Invalid u256 value - value is less than zero.', + { + value: '-1', + type: 'u256', + } + ) + ); + }); + + it(`should fail to encode value [max + 1 = 115792089237316195423570985008687907853269984665640564039457584007913129639936]`, async () => { + const coder = AbiEncoding.v1.u256; + const value = + '115792089237316195423570985008687907853269984665640564039457584007913129639936'; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u256 value - value exceeds maximum.', { + value: '115792089237316195423570985008687907853269984665640564039457584007913129639936', + type: 'u256', + }) + ); + }); + }); + + describe('decode', () => { + it('should decode a u256 [min]', () => { + const coder = AbiEncoding.v1.u256; + // @ts-expect-error: Custom matcher 'toEqualBn' + const expected = expect.toEqualBn(0); + const data = new Uint8Array(32).fill(0); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(32); + }); + + it(`should decode a u256 [max = 115792089237316195423570985008687907853269984665640564039457584007913129639935]`, () => { + const coder = AbiEncoding.v1.u256; + // @ts-expect-error: Custom matcher 'toEqualBn' + const expected = expect.toEqualBn( + '115792089237316195423570985008687907853269984665640564039457584007913129639935' + ); + const data = new Uint8Array(32).fill(255); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(32); + }); + + it('should throw when decoding invalid u256 data [empty]', async () => { + const coder = AbiEncoding.v1.u256; + const data = new Uint8Array(0); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u256 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u256', + expectedLength: 32, + }) + ); + }); + + it('should throw when decoding invalid u256 data [empty with offset]', async () => { + const coder = AbiEncoding.v1.u256; + const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + + await expectToThrowFuelError( + () => coder.decode(data, 8), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u256 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u256', + expectedLength: 32, + }) + ); + }); + }); +}); diff --git a/packages/abi/test/encoding/v1/u32.test.ts b/packages/abi/test/encoding/v1/u32.test.ts new file mode 100644 index 00000000000..547438845b7 --- /dev/null +++ b/packages/abi/test/encoding/v1/u32.test.ts @@ -0,0 +1,110 @@ +import { FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; + +import { AbiEncoding } from '../../../src'; + +describe('u32', () => { + describe('encode', () => { + it('should encode a u32 [min = 0]', () => { + const coder = AbiEncoding.v1.u32; + const expected = new Uint8Array([0, 0, 0, 0]); + const value = 0; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it(`should encode a u32 [max = 4294967295]`, () => { + const coder = AbiEncoding.v1.u32; + const expected = new Uint8Array([255, 255, 255, 255]); + const value = 4294967295; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should fail to encode value [min - 1 = -1]', async () => { + const coder = AbiEncoding.v1.u32; + const value = -1; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError( + FuelError.CODES.ENCODE_ERROR, + 'Invalid u32 value - value is less than zero.', + { + value: '-1', + type: 'u32', + } + ) + ); + }); + + it(`should fail to encode value [max + 1 = 4294967296]`, async () => { + const coder = AbiEncoding.v1.u32; + const value = 4294967296; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u32 value - value exceeds maximum.', { + value: '4294967296', + type: 'u32', + }) + ); + }); + }); + + describe('decode', () => { + it('should decode a u32 [min]', () => { + const coder = AbiEncoding.v1.u32; + const expected = 0; + const data = new Uint8Array([0, 0, 0, 0]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(4); + }); + + it(`should decode a u32 [max = 4294967295]`, () => { + const coder = AbiEncoding.v1.u32; + const expected = 4294967295; + const data = new Uint8Array([255, 255, 255, 255]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(4); + }); + + it('should throw when decoding invalid u32 data [empty]', async () => { + const coder = AbiEncoding.v1.u32; + const data = new Uint8Array(0); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u32 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u32', + expectedLength: 4, + }) + ); + }); + + it('should throw when decoding invalid u32 data [empty with offset]', async () => { + const coder = AbiEncoding.v1.u32; + const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + + await expectToThrowFuelError( + () => coder.decode(data, 8), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u32 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u32', + expectedLength: 4, + }) + ); + }); + }); +}); diff --git a/packages/abi/test/encoding/v1/u64.test.ts b/packages/abi/test/encoding/v1/u64.test.ts new file mode 100644 index 00000000000..598a26fd04e --- /dev/null +++ b/packages/abi/test/encoding/v1/u64.test.ts @@ -0,0 +1,140 @@ +import { FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; + +import { AbiEncoding } from '../../../src'; +import { toEqualBn } from '../../utils/vitest.matcher'; + +expect.extend({ toEqualBn }); + +describe('u64', () => { + describe('encode', () => { + it('should encode a u64 [min = 0]', () => { + const coder = AbiEncoding.v1.u64; + const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]); + const value = 0; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it(`should encode a u64 [max = 18446744073709551615]`, () => { + const coder = AbiEncoding.v1.u64; + const expected = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]); + const value = '18446744073709551615'; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should fail to encode value [boolean]', async () => { + const coder = AbiEncoding.v1.u64; + const value = true; + + await expectToThrowFuelError( + // @ts-expect-error: a boolean value boolean + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u64 value - expected a BNInput.', { + value, + }) + ); + }); + + it('should fail to encode value [not a BNInput]', async () => { + const coder = AbiEncoding.v1.u64; + const value = 'not a BNInput'; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u64 value - expected a BNInput.', { + value, + }) + ); + }); + + it('should fail to encode value [min - 1 = -1]', async () => { + const coder = AbiEncoding.v1.u64; + const value = -1; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError( + FuelError.CODES.ENCODE_ERROR, + 'Invalid u64 value - value is less than zero.', + { + value: '-1', + type: 'u64', + } + ) + ); + }); + + it(`should fail to encode value [max + 1 = 18446744073709551616]`, async () => { + const coder = AbiEncoding.v1.u64; + const value = '18446744073709551616'; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u64 value - value exceeds maximum.', { + value: '18446744073709551616', + type: 'u64', + }) + ); + }); + }); + + describe('decode', () => { + it('should decode a u64 [min]', () => { + const coder = AbiEncoding.v1.u64; + // @ts-expect-error: Custom matcher 'toEqualBn' + const expected = expect.toEqualBn(0); + const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(8); + }); + + it(`should decode a u64 [max = 18446744073709551615]`, () => { + const coder = AbiEncoding.v1.u64; + // @ts-expect-error: Custom matcher 'toEqualBn' + const expected = expect.toEqualBn('18446744073709551615'); + const data = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(8); + }); + + it('should throw when decoding invalid u64 data [empty]', async () => { + const coder = AbiEncoding.v1.u64; + const data = new Uint8Array(0); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u64 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u64', + expectedLength: 8, + }) + ); + }); + + it('should throw when decoding invalid u64 data [empty with offset]', async () => { + const coder = AbiEncoding.v1.u64; + const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + + await expectToThrowFuelError( + () => coder.decode(data, 8), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u64 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u64', + expectedLength: 8, + }) + ); + }); + }); +}); diff --git a/packages/abi/test/encoding/v1/u8.test.ts b/packages/abi/test/encoding/v1/u8.test.ts new file mode 100644 index 00000000000..8303506c84c --- /dev/null +++ b/packages/abi/test/encoding/v1/u8.test.ts @@ -0,0 +1,106 @@ +import { FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; + +import { AbiEncoding } from '../../../src'; + +describe('u8', () => { + describe('encode', () => { + it('should encode a u8 [min = 0]', () => { + const coder = AbiEncoding.v1.u8; + const expected = new Uint8Array([0]); + const value = 0; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should encode a u8 [max = 255]', () => { + const coder = AbiEncoding.v1.u8; + const expected = new Uint8Array([255]); + const value = 255; + + const actual = coder.encode(value); + + expect(actual).toStrictEqual(expected); + }); + + it('should fail to encode value [-1]', async () => { + const coder = AbiEncoding.v1.u8; + const value = -1; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u8 value - value is less than zero.', { + value: '-1', + type: 'u8', + }) + ); + }); + + it('should fail to encode value [256]', async () => { + const coder = AbiEncoding.v1.u8; + const value = 256; + + await expectToThrowFuelError( + () => coder.encode(value), + new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u8 value - value exceeds maximum.', { + value: '256', + type: 'u8', + }) + ); + }); + }); + + describe('decode', () => { + it('should decode a u8 [min]', () => { + const coder = AbiEncoding.v1.u8; + const expected = 0; + const data = new Uint8Array([0]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(1); + }); + + it('should decode a u8 [max]', () => { + const coder = AbiEncoding.v1.u8; + const expected = 255; + const data = new Uint8Array([255]); + + const [actual, offset] = coder.decode(data); + + expect(actual).toStrictEqual(expected); + expect(offset).toStrictEqual(1); + }); + + it('should throw when decoding invalid u8 data [empty]', async () => { + const coder = AbiEncoding.v1.u8; + const data = new Uint8Array(0); + + await expectToThrowFuelError( + () => coder.decode(data), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u8 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u8', + expectedLength: 1, + }) + ); + }); + + it('should throw when decoding invalid u8 data [empty with offset]', async () => { + const coder = AbiEncoding.v1.u8; + const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + + await expectToThrowFuelError( + () => coder.decode(data, 8), + new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u8 data - unexpected length.', { + data: new Uint8Array([]), + type: 'u8', + expectedLength: 1, + }) + ); + }); + }); +}); diff --git a/packages/abi/test/encoding/v1/void.test.ts b/packages/abi/test/encoding/v1/void.test.ts new file mode 100644 index 00000000000..a134f6749b0 --- /dev/null +++ b/packages/abi/test/encoding/v1/void.test.ts @@ -0,0 +1,28 @@ +import { AbiEncoding } from '../../../src'; + +describe('void', () => { + describe('encode', () => { + it('should encode a void', () => { + const coder = AbiEncoding.v1.void; + const value = undefined; + const expected = new Uint8Array([]); + + const actual = coder.encode(value); + + expect(actual).toEqual(expected); + }); + }); + + describe('decode', () => { + it('should decode a void', () => { + const coder = AbiEncoding.v1.void; + const data = new Uint8Array([]); + const expected = undefined; + + const [actual, offset] = coder.decode(data, 0); + + expect(actual).toEqual(expected); + expect(offset).toEqual(0); + }); + }); +}); diff --git a/packages/abi/test/utils/vitest.matcher.ts b/packages/abi/test/utils/vitest.matcher.ts new file mode 100644 index 00000000000..028f6785a71 --- /dev/null +++ b/packages/abi/test/utils/vitest.matcher.ts @@ -0,0 +1,20 @@ +import { bn } from '@fuel-ts/math'; +import type { BNInput } from '@fuel-ts/math'; + +export const toEqualBn = (_received: BNInput, _argument: BNInput) => { + const received = bn(_received); + const argument = bn(_argument); + + const pass = received.eq(argument); + + if (pass) { + return { + message: () => `Expected ${received.toString()} not to equal ${argument.toString()}`, + pass: true, + }; + } + return { + message: () => `expected ${received.toString()} to equal ${argument.toString()}`, + pass: false, + }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04d4fb1e96b..c086d2e7cbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -640,6 +640,9 @@ importers: '@fuel-ts/errors': specifier: workspace:* version: link:../errors + '@fuel-ts/math': + specifier: workspace:* + version: link:../math '@fuel-ts/utils': specifier: workspace:* version: link:../utils @@ -27260,7 +27263,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -27315,7 +27318,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.1 @@ -27425,7 +27428,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 From 9a12aa942b8fd129e821ad580f2c188b48870bac Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 4 Dec 2024 17:59:47 +0000 Subject: [PATCH 2/4] chore: changeset --- .changeset/chilled-mice-grin.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/chilled-mice-grin.md diff --git a/.changeset/chilled-mice-grin.md b/.changeset/chilled-mice-grin.md new file mode 100644 index 00000000000..8865c2f263b --- /dev/null +++ b/.changeset/chilled-mice-grin.md @@ -0,0 +1,4 @@ +--- +--- + +feat: added `fixed` based coders From 3083d0f1472d103503e88a7163c94f9232fb1fb7 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 4 Dec 2024 18:17:54 +0000 Subject: [PATCH 3/4] chore: added missing package --- packages/abi/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/abi/package.json b/packages/abi/package.json index 41e3d7d1bed..05547873ae3 100644 --- a/packages/abi/package.json +++ b/packages/abi/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@fuel-ts/errors": "workspace:*", + "@fuel-ts/interfaces": "workspace:*", "@fuel-ts/math": "workspace:*", "@fuel-ts/utils": "workspace:*" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c086d2e7cbb..1c2a0ea60fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -640,6 +640,9 @@ importers: '@fuel-ts/errors': specifier: workspace:* version: link:../errors + '@fuel-ts/interfaces': + specifier: workspace:* + version: link:../interfaces '@fuel-ts/math': specifier: workspace:* version: link:../math From 93fb0b6fca1b3010b72256cf3c1d14c3c2c9cd09 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 4 Dec 2024 18:24:14 +0000 Subject: [PATCH 4/4] chore: added missing test groups --- packages/abi/test/encoding/v1/b256.test.ts | 4 ++++ packages/abi/test/encoding/v1/b512.test.ts | 4 ++++ packages/abi/test/encoding/v1/boolean.test.ts | 14 +++++++++----- packages/abi/test/encoding/v1/u16.test.ts | 4 ++++ packages/abi/test/encoding/v1/u256.test.ts | 4 ++++ packages/abi/test/encoding/v1/u32.test.ts | 4 ++++ packages/abi/test/encoding/v1/u64.test.ts | 4 ++++ packages/abi/test/encoding/v1/u8.test.ts | 4 ++++ packages/abi/test/encoding/v1/void.test.ts | 4 ++++ 9 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/abi/test/encoding/v1/b256.test.ts b/packages/abi/test/encoding/v1/b256.test.ts index 23d965766b9..5a78ea9cbf0 100644 --- a/packages/abi/test/encoding/v1/b256.test.ts +++ b/packages/abi/test/encoding/v1/b256.test.ts @@ -3,6 +3,10 @@ import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { AbiEncoding } from '../../../src'; +/** + * @group node + * @group browser + */ describe('b256', () => { describe('encode', () => { it('should encode a b256 [zero]', () => { diff --git a/packages/abi/test/encoding/v1/b512.test.ts b/packages/abi/test/encoding/v1/b512.test.ts index c7812416294..ad0d1ea1f93 100644 --- a/packages/abi/test/encoding/v1/b512.test.ts +++ b/packages/abi/test/encoding/v1/b512.test.ts @@ -3,6 +3,10 @@ import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { AbiEncoding } from '../../../src'; +/** + * @group node + * @group browser + */ describe('b512', () => { describe('encode', () => { it('should encode a b512 [zero]', () => { diff --git a/packages/abi/test/encoding/v1/boolean.test.ts b/packages/abi/test/encoding/v1/boolean.test.ts index ff3681b9754..0336c8e0177 100644 --- a/packages/abi/test/encoding/v1/boolean.test.ts +++ b/packages/abi/test/encoding/v1/boolean.test.ts @@ -3,6 +3,10 @@ import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { AbiEncoding } from '../../../src'; +/** + * @group node + * @group browser + */ describe('boolean', () => { describe('encode', () => { it('should encode a boolean [true]', () => { @@ -81,22 +85,22 @@ describe('boolean', () => { () => coder.decode(data), new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid boolean data - not enough data.', { data, - type: 'bool', + type: 'boolean', expectedLength: 1, }) ); }); - it('should throw when decoding an invalid boolean value [2]', async () => { + it('should throw when decoding an invalid boolean value [256]', async () => { const coder = AbiEncoding.v1.bool; - const data = new Uint8Array([2]); + const data = new Uint8Array([256]); await expectToThrowFuelError( () => coder.decode(data), new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid boolean value.', { data, - type: 'bool', - value: 2, + type: 'boolean', + value: 256, }) ); }); diff --git a/packages/abi/test/encoding/v1/u16.test.ts b/packages/abi/test/encoding/v1/u16.test.ts index 2ec2d6355b1..0e9e1338991 100644 --- a/packages/abi/test/encoding/v1/u16.test.ts +++ b/packages/abi/test/encoding/v1/u16.test.ts @@ -3,6 +3,10 @@ import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { AbiEncoding } from '../../../src'; +/** + * @group node + * @group browser + */ describe('u16', () => { describe('encode', () => { it('should encode a u16 [min = 0]', () => { diff --git a/packages/abi/test/encoding/v1/u256.test.ts b/packages/abi/test/encoding/v1/u256.test.ts index 36f6a58c815..84bd862c368 100644 --- a/packages/abi/test/encoding/v1/u256.test.ts +++ b/packages/abi/test/encoding/v1/u256.test.ts @@ -6,6 +6,10 @@ import { toEqualBn } from '../../utils/vitest.matcher'; expect.extend({ toEqualBn }); +/** + * @group node + * @group browser + */ describe('u256', () => { describe('encode', () => { it('should encode a u256 [min = 0]', () => { diff --git a/packages/abi/test/encoding/v1/u32.test.ts b/packages/abi/test/encoding/v1/u32.test.ts index 547438845b7..288bd12e321 100644 --- a/packages/abi/test/encoding/v1/u32.test.ts +++ b/packages/abi/test/encoding/v1/u32.test.ts @@ -3,6 +3,10 @@ import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { AbiEncoding } from '../../../src'; +/** + * @group node + * @group browser + */ describe('u32', () => { describe('encode', () => { it('should encode a u32 [min = 0]', () => { diff --git a/packages/abi/test/encoding/v1/u64.test.ts b/packages/abi/test/encoding/v1/u64.test.ts index 598a26fd04e..d919bbb2e2b 100644 --- a/packages/abi/test/encoding/v1/u64.test.ts +++ b/packages/abi/test/encoding/v1/u64.test.ts @@ -6,6 +6,10 @@ import { toEqualBn } from '../../utils/vitest.matcher'; expect.extend({ toEqualBn }); +/** + * @group node + * @group browser + */ describe('u64', () => { describe('encode', () => { it('should encode a u64 [min = 0]', () => { diff --git a/packages/abi/test/encoding/v1/u8.test.ts b/packages/abi/test/encoding/v1/u8.test.ts index 8303506c84c..17411bdc9d6 100644 --- a/packages/abi/test/encoding/v1/u8.test.ts +++ b/packages/abi/test/encoding/v1/u8.test.ts @@ -3,6 +3,10 @@ import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { AbiEncoding } from '../../../src'; +/** + * @group node + * @group browser + */ describe('u8', () => { describe('encode', () => { it('should encode a u8 [min = 0]', () => { diff --git a/packages/abi/test/encoding/v1/void.test.ts b/packages/abi/test/encoding/v1/void.test.ts index a134f6749b0..d684acf78b2 100644 --- a/packages/abi/test/encoding/v1/void.test.ts +++ b/packages/abi/test/encoding/v1/void.test.ts @@ -1,5 +1,9 @@ import { AbiEncoding } from '../../../src'; +/** + * @group node + * @group browser + */ describe('void', () => { describe('encode', () => { it('should encode a void', () => {