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: added fixed based coders #3446

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/chilled-mice-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

feat: added `fixed` based coders
2 changes: 2 additions & 0 deletions packages/abi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
},
"dependencies": {
"@fuel-ts/errors": "workspace:*",
"@fuel-ts/interfaces": "workspace:*",
"@fuel-ts/math": "workspace:*",
"@fuel-ts/utils": "workspace:*"
},
"devDependencies": {}
Expand Down
60 changes: 59 additions & 1 deletion packages/abi/src/coder/abi-coder-types.ts
Original file line number Diff line number Diff line change
@@ -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> = T | undefined;

/**
* The type of value you can provide to `Coder.encode`
*/
export type InputValue<T = void> =
| Primitive
| BN
| Option<T>
| BytesLike
| InputValue<T>[]
| { [key: string]: InputValue<T> }
| 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 abstract class Coder<TEncoded = unknown, TDecoded = unknown> {
/**
* 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];
}
21 changes: 21 additions & 0 deletions packages/abi/src/coder/encoding/encoding-constants.ts
Original file line number Diff line number Diff line change
@@ -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;
46 changes: 46 additions & 0 deletions packages/abi/src/coder/encoding/encoding-types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions packages/abi/src/coder/encoding/encoding.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions packages/abi/src/coder/encoding/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AbiEncoding } from './encoding';
export * from './encoding-types';
218 changes: 218 additions & 0 deletions packages/abi/src/coder/encoding/v1/fixed.ts
Original file line number Diff line number Diff line change
@@ -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<number, number> => ({
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<BNInput, BN> => ({
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<string, string> => ({
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<number, number> = createNumberCoder(1, U8_TYPE);
export const u16: Coder<number, number> = createNumberCoder(2, U16_TYPE);
export const u32: Coder<number, number> = createNumberCoder(4, U32_TYPE);
export const u64: Coder<BNInput, BN> = createBigNumberCoder(8, U64_TYPE);
export const u256: Coder<BNInput, BN> = createBigNumberCoder(32, U256_TYPE);
export const b256: Coder<string, string> = createHexCoder(32, B256_TYPE);
export const b512: Coder<string, string> = createHexCoder(64, B512_TYPE);
export const voidCoder: Coder<undefined, undefined> = {
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<boolean, boolean> = {
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];
},
};
Loading
Loading