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 simpler Option style #471

Merged
merged 9 commits into from
Sep 1, 2022
Merged
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
6 changes: 6 additions & 0 deletions .changeset/weak-dancers-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/abi-coder": patch
"@fuel-ts/contract": patch
---

add option type improvement
39 changes: 26 additions & 13 deletions packages/abi-coder/src/abi-coder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ import BooleanCoder from './coders/boolean';
import ByteCoder from './coders/byte';
import EnumCoder from './coders/enum';
import NumberCoder from './coders/number';
import OptionCoder from './coders/option';
import StringCoder from './coders/string';
import StructCoder from './coders/struct';
import TupleCoder from './coders/tuple';
import {
arrayRegEx,
enumRegEx,
stringRegEx,
structRegEx,
tupleRegEx,
OPTION_CODER_TYPE,
} from './constants';
import type { JsonAbiFragmentType } from './json-abi';
import { filterEmptyParams } from './utilities';

export const stringRegEx = /str\[(?<length>[0-9]+)\]/;
export const arrayRegEx = /\[(?<item>[\w\s\\[\]]+);\s*(?<length>[0-9]+)\]/;
export const structRegEx = /^struct (?<name>\w+)$/;
export const enumRegEx = /^enum (?<name>\w+)$/;
export const tupleRegEx = /^\((?<items>.*)\)$/;
export const genericRegEx = /^generic (?<name>\w+)$/;
import { filterEmptyParams, hasOptionTypes } from './utilities';

const logger = new Logger(process.env.BUILD_VERSION || '~');

Expand Down Expand Up @@ -82,6 +84,11 @@ export default class AbiCoder {
obj[component.name] = this.getCoder(component);
return obj;
}, {});

const isOptionEnum = param.type === OPTION_CODER_TYPE;
if (isOptionEnum) {
return new OptionCoder(enumMatch.name, coders);
}
return new EnumCoder(enumMatch.name, coders);
}

Expand All @@ -96,17 +103,23 @@ export default class AbiCoder {

encode(types: ReadonlyArray<JsonAbiFragmentType>, values: InputValue[]): Uint8Array {
const nonEmptyTypes = filterEmptyParams(types);
const shallowCopyValues = values.slice();

if (Array.isArray(values) && nonEmptyTypes.length !== values.length) {
logger.throwError('Types/values length mismatch', Logger.errors.INVALID_ARGUMENT, {
count: { types: nonEmptyTypes.length, values: values.length },
value: { types, values },
});
if (!hasOptionTypes(types)) {
logger.throwError('Types/values length mismatch', Logger.errors.INVALID_ARGUMENT, {
count: { types: nonEmptyTypes.length, values: values.length },
value: { types, values },
});
} else {
shallowCopyValues.length = types.length;
shallowCopyValues.fill(undefined as unknown as InputValue, values.length);
}
}

const coders = nonEmptyTypes.map((type) => this.getCoder(type));
const coder = new TupleCoder(coders);
return coder.encode(values);
return coder.encode(shallowCopyValues);
}

decode(types: ReadonlyArray<JsonAbiFragmentType>, data: BytesLike): DecodedValue[] | undefined {
Expand Down
4 changes: 2 additions & 2 deletions packages/abi-coder/src/coders/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import type { TypesOfCoder } from './abstract-coder';
import Coder from './abstract-coder';
import NumberCoder from './number';

type InputValueOf<TCoders extends Record<string, Coder>> = RequireExactlyOne<{
export type InputValueOf<TCoders extends Record<string, Coder>> = RequireExactlyOne<{
[P in keyof TCoders]: TypesOfCoder<TCoders[P]>['Input'];
}>;
type DecodedValueOf<TCoders extends Record<string, Coder>> = RequireExactlyOne<{
export type DecodedValueOf<TCoders extends Record<string, Coder>> = RequireExactlyOne<{
[P in keyof TCoders]: TypesOfCoder<TCoders[P]>['Decoded'];
}>;

Expand Down
34 changes: 34 additions & 0 deletions packages/abi-coder/src/coders/option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type Coder from './abstract-coder';
import type { InputValueOf, DecodedValueOf } from './enum';
import EnumCoder from './enum';

type Option<T> = T | undefined;
type SwayOption<T> = { None: [] } | { Some: T };

export default class OptionCoder<TCoders extends Record<string, Coder>> extends EnumCoder<TCoders> {
encode(value: InputValueOf<TCoders>): Uint8Array {
const result = super.encode(this.toSwayOption(value) as unknown as InputValueOf<TCoders>);
return result;
}

toSwayOption(input: InputValueOf<TCoders>): SwayOption<unknown> {
if (input !== undefined) {
return { Some: input };
}

return { None: [] };
}

decode(data: Uint8Array, offset: number): [DecodedValueOf<TCoders>, number] {
const [decoded, newOffset] = super.decode(data, offset);
return [this.toOption(decoded) as DecodedValueOf<TCoders>, newOffset];
}

toOption(output?: DecodedValueOf<TCoders>): Option<unknown> {
if (output && 'Some' in output) {
return output.Some;
}

return undefined;
}
}
7 changes: 7 additions & 0 deletions packages/abi-coder/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const OPTION_CODER_TYPE = 'enum Option';
export const stringRegEx = /str\[(?<length>[0-9]+)\]/;
export const arrayRegEx = /\[(?<item>[\w\s\\[\]]+);\s*(?<length>[0-9]+)\]/;
export const structRegEx = /^struct (?<name>\w+)$/;
export const enumRegEx = /^enum (?<name>\w+)$/;
export const tupleRegEx = /^\((?<items>.*)\)$/;
export const genericRegEx = /^generic (?<name>\w+)$/;
2 changes: 1 addition & 1 deletion packages/abi-coder/src/fragments/param-type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineReadOnly } from '@ethersproject/properties';

import { arrayRegEx, enumRegEx, structRegEx, stringRegEx } from '../abi-coder';
import { arrayRegEx, enumRegEx, structRegEx, stringRegEx } from '../constants';

export interface JsonFragmentType {
readonly name?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/abi-coder/src/json-abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/abi.md#json-abi-format
*/

import { genericRegEx } from './abi-coder';
import { genericRegEx } from './constants';

export interface JsonAbiFragmentType {
readonly type: string;
Expand Down
6 changes: 6 additions & 0 deletions packages/abi-coder/src/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { OPTION_CODER_TYPE } from './constants';
import type { ParamType } from './fragments/param-type';

export function filterEmptyParams<T>(types: T): T;
export function filterEmptyParams(types: ReadonlyArray<string | ParamType>) {
return types.filter((t) => (t as Readonly<ParamType>)?.type !== '()' && t !== '()');
}

export function hasOptionTypes<T>(types: T): T;
export function hasOptionTypes(types: ReadonlyArray<string | ParamType>) {
return types.some((t) => (t as Readonly<ParamType>)?.type === OPTION_CODER_TYPE);
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ describe('Coverage Contract', () => {
expect((await contractInstance.functions.get_contract_id().call()).value).toStrictEqual({
value: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
});
expect((await contractInstance.functions.get_some_option_u8().call()).value).toEqual(113);
expect((await contractInstance.functions.get_none_option_u8().call()).value).toEqual(undefined);
});

it('should test u8 variable type', async () => {
Expand Down Expand Up @@ -164,7 +166,7 @@ describe('Coverage Contract', () => {
expect(value).toStrictEqual(INPUT);
});

it.only('should test enum < 8 byte variable type', async () => {
it('should test enum < 8 byte variable type', async () => {
const INPUT = { Empty: [] };
const { value } = await contractInstance.functions.echo_enum_small(INPUT).call();
expect(value).toStrictEqual(INPUT);
Expand All @@ -175,4 +177,45 @@ describe('Coverage Contract', () => {
const { value } = await contractInstance.functions.echo_enum_big(INPUT).call();
expect(value).toStrictEqual(INPUT);
});

it('should test Option<u8> type', async () => {
const INPUT = 187;
const { value } = await contractInstance.functions.echo_option_u8(INPUT).call();
expect(value).toStrictEqual(INPUT);
});

it('should test Option<u32> extraction [Some]', async () => {
const INPUT_SOME = 123;
const { value: Some } = await contractInstance.functions
.echo_option_extract_u32(INPUT_SOME)
.call();
expect(Some).toStrictEqual(INPUT_SOME);
});

it('should test Option<u32> extraction [None]', async () => {
const INPUT_NONE = undefined;
const { value: None } = await contractInstance.functions
.echo_option_extract_u32(INPUT_NONE)
camsjams marked this conversation as resolved.
Show resolved Hide resolved
.call();
expect(None).toStrictEqual(500);

const { value: NoneVoid } = await contractInstance.functions.echo_option_extract_u32().call();
expect(NoneVoid).toStrictEqual(500);
});

it('should test multiple Option<u32> params [Some]', async () => {
const INPUT_A = 1;
const INPUT_B = 4;
const INPUT_C = 5;
const { value: Some } = await contractInstance.functions
.echo_option_three_u8(INPUT_A, INPUT_B, INPUT_C)
.call();
expect(Some).toStrictEqual(10);
});

it('should test multiple Option<u32> params [None]', async () => {
const INPUT = 1;
const { value: Some } = await contractInstance.functions.echo_option_three_u8(INPUT).call();
expect(Some).toStrictEqual(1);
});
});
43 changes: 42 additions & 1 deletion packages/contract/src/__test__/coverage-contract/src/main.sw
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::*;
use core::*;
use std::storage::*;
use std::contract_id::ContractId;
use std::option::Option;

pub struct U8Struct {
i: u8,
Expand Down Expand Up @@ -41,7 +42,9 @@ abi CoverageContract {
fn get_large_array() -> [u32;2];
fn get_empty_enum() -> SmallEnum;
fn get_contract_id() -> ContractId;
fn echo_u8(input: u8) -> u8;
fn get_some_option_u8() -> Option<u8>;
fn get_none_option_u8() -> Option<u8>;
fn echo_u8(input: u8) -> u8;
fn echo_u16(input: u16) -> u16;
fn echo_u32(input: u32) -> u32;
fn echo_u64(input: u64) -> u64;
Expand All @@ -62,6 +65,9 @@ abi CoverageContract {
fn echo_struct_b256(input: B256Struct) -> B256Struct;
fn echo_enum_small(input: SmallEnum) -> SmallEnum;
fn echo_enum_big(input: BigEnum) -> BigEnum;
fn echo_option_u8(input: Option<u8>) -> Option<u8>;
fn echo_option_extract_u32(input: Option<u32>) -> u32;
fn echo_option_three_u8(inputA: Option<u8>, inputB: Option<u8>, inputC: Option<u8>) -> u8;
}

impl CoverageContract for Contract {
Expand Down Expand Up @@ -106,6 +112,16 @@ impl CoverageContract for Contract {
~ContractId::from(id)
}

fn get_some_option_u8() -> Option<u8> {
let o:Option<u8> = Option::Some(113);
o
}

fn get_none_option_u8() -> Option<u8> {
let o:Option<u8> = Option::None;
o
}

fn echo_u8(input: u8) -> u8 {
input
}
Expand Down Expand Up @@ -169,4 +185,29 @@ impl CoverageContract for Contract {
fn echo_enum_big(input: BigEnum) -> BigEnum {
input
}
fn echo_option_u8(input: Option<u8>) -> Option<u8> {
input
}
fn echo_option_extract_u32(input: Option<u32>) -> u32 {
match input {
Option::Some(value) => value,
Option::None => 500u32,
}
}
fn echo_option_three_u8(inputA: Option<u8>, inputB: Option<u8>, inputC: Option<u8>) -> u8 {
let value1 = match inputA {
Option::Some(value) => value,
Option::None => 0,
};
let value2 = match inputB {
Option::Some(value) => value,
Option::None => 0,
};
let value3 = match inputC {
Option::Some(value) => value,
Option::None => 0,
};

value1 + value2 + value3
}
}
18 changes: 9 additions & 9 deletions packages/contract/src/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ export const contractCallScript = new Script<ContractCall[], Uint8Array[]>(
fn_selector: new NumberCoder('u64').decode(functionSelector, 0)[0],
fn_arg: fnArg,
parameters: {
amount: call.amount ? { Some: BigInt(call.amount) } : { None: [] },
asset_id: call.assetId ? { Some: { value: call.assetId } } : { None: [] },
gas: call.gas ? { Some: BigInt(call.gas) } : { None: [] },
amount: call.amount ? BigInt(call.amount) : undefined,
asset_id: call.assetId ? { value: call.assetId } : undefined,
gas: call.gas ? BigInt(call.gas) : undefined,
},
};

scriptCallSlot = { Some: scriptCall };
scriptCallSlot = scriptCall;
} else {
scriptCallSlot = { None: [] };
scriptCallSlot = undefined;
}

scriptCallSlots.push(scriptCallSlot);
Expand Down Expand Up @@ -100,12 +100,12 @@ export const contractCallScript = new Script<ContractCall[], Uint8Array[]>(

const contractCallResults: any[] = [];
(scriptReturn.call_returns as any[]).forEach((callResult, i) => {
if (callResult.Some) {
if (callResult.Some.Data) {
const [offset, length] = callResult.Some.Data;
if (callResult) {
if (callResult.Data) {
const [offset, length] = callResult.Data;
contractCallResults[i] = returnData.slice(Number(offset), Number(offset + length));
} else {
contractCallResults[i] = new NumberCoder('u64').encode(callResult.Some.Value);
contractCallResults[i] = new NumberCoder('u64').encode(callResult.Value);
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
InvokeFunction,
} from "fuels";

import type { SwayEnum } from "./common";
import type { Enum, Option } from "./common";

export type ContractIdInput = { value: string };

Expand Down Expand Up @@ -62,23 +62,23 @@ export type ScriptReturnOutput = {
];
};

export type CallArgInput = SwayEnum<{
export type CallArgInput = Enum<{
Value: BigNumberish;
Reference: BigNumberish;
}>;

export type CallArgOutput = SwayEnum<{ Value: bigint; Reference: bigint }>;
export type CallArgOutput = Enum<{ Value: bigint; Reference: bigint }>;

export type OptionInput = SwayEnum<{ Some: CallInput; None: [] }>;
export type OptionInput = Option<CallInput>;

export type OptionOutput = SwayEnum<{ Some: CallOutput; None: [] }>;
export type OptionOutput = Option<CallOutput>;

export type CallReturnInput = SwayEnum<{
export type CallReturnInput = Enum<{
Value: BigNumberish;
Reference: [BigNumberish, BigNumberish];
}>;

export type CallReturnOutput = SwayEnum<{
export type CallReturnOutput = Enum<{
Value: bigint;
Reference: [bigint, bigint];
}>;
Expand Down
Loading