From 68390ac3e5fc791e48da43a547cd4521054fc46c Mon Sep 17 00:00:00 2001 From: Cameron Manavian Date: Mon, 25 Sep 2023 08:35:38 -0700 Subject: [PATCH] feat: add support for `Bytes` and `RawSlice` as inputs and outputs for Contracts (#1221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * prep new bytes * add setup * fix test * refactor * adjust * tests * improve tests * aDJUST * refactor * add tests * refactor * adjust * adjust * fix length * correct reference * refactor * twenty eagles lick? * adjust * fix post rename * catch magic revert number * fix mapper * fix: linting warning * adjust * convert errors --------- Co-authored-by: Nedim Salkić --- .changeset/twenty-eagles-lick.md | 5 ++ packages/abi-coder/src/abi-coder.ts | 7 +++ packages/abi-coder/src/coders/byte.test.ts | 47 ++++++++++++++ packages/abi-coder/src/coders/byte.ts | 63 +++++++++++++++++++ .../abi-coder/src/coders/raw-slice.test.ts | 35 +++++++++++ packages/abi-coder/src/coders/raw-slice.ts | 47 ++++++++++++++ packages/abi-coder/src/function-fragment.ts | 4 ++ packages/abi-coder/src/resolved-abi-type.ts | 4 ++ packages/abi-coder/src/utilities.ts | 3 + packages/abi-coder/test/interface.test.ts | 27 ++++++++ .../exhaustive-examples/src/main.sw | 12 +++- .../fixtures/forc-projects/Forc.toml | 2 + .../fixtures/forc-projects/bytes/Forc.toml | 3 + .../fixtures/forc-projects/bytes/src/main.sw | 62 ++++++++++++++++++ .../forc-projects/raw-slice/Forc.toml | 3 + .../forc-projects/raw-slice/src/main.sw | 59 +++++++++++++++++ packages/fuel-gauge/src/bytes.test.ts | 47 ++++++++++++++ packages/fuel-gauge/src/raw-slice.test.ts | 40 ++++++++++++ packages/program/src/utils.ts | 9 ++- 19 files changed, 476 insertions(+), 3 deletions(-) create mode 100644 .changeset/twenty-eagles-lick.md create mode 100644 packages/abi-coder/src/coders/byte.test.ts create mode 100644 packages/abi-coder/src/coders/byte.ts create mode 100644 packages/abi-coder/src/coders/raw-slice.test.ts create mode 100644 packages/abi-coder/src/coders/raw-slice.ts create mode 100644 packages/fuel-gauge/fixtures/forc-projects/bytes/Forc.toml create mode 100644 packages/fuel-gauge/fixtures/forc-projects/bytes/src/main.sw create mode 100644 packages/fuel-gauge/fixtures/forc-projects/raw-slice/Forc.toml create mode 100644 packages/fuel-gauge/fixtures/forc-projects/raw-slice/src/main.sw create mode 100644 packages/fuel-gauge/src/bytes.test.ts create mode 100644 packages/fuel-gauge/src/raw-slice.test.ts diff --git a/.changeset/twenty-eagles-lick.md b/.changeset/twenty-eagles-lick.md new file mode 100644 index 00000000000..56d8eb9bf91 --- /dev/null +++ b/.changeset/twenty-eagles-lick.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/abi-coder": minor +--- + +Add support for Bytes and RawSlice diff --git a/packages/abi-coder/src/abi-coder.ts b/packages/abi-coder/src/abi-coder.ts index c2bb895cbcc..034be96ef46 100644 --- a/packages/abi-coder/src/abi-coder.ts +++ b/packages/abi-coder/src/abi-coder.ts @@ -6,9 +6,11 @@ import { ArrayCoder } from './coders/array'; import { B256Coder } from './coders/b256'; import { B512Coder } from './coders/b512'; 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 { RawSliceCoder } from './coders/raw-slice'; import { StringCoder } from './coders/string'; import { StructCoder } from './coders/struct'; import { TupleCoder } from './coders/tuple'; @@ -22,6 +24,7 @@ import { tupleRegEx, OPTION_CODER_TYPE, VEC_CODER_TYPE, + BYTES_CODER_TYPE, } from './constants'; import type { JsonAbi, JsonAbiArgument } from './json-abi'; import { ResolvedAbiType } from './resolved-abi-type'; @@ -56,12 +59,16 @@ export abstract class AbiCoder { case 'u64': case 'raw untyped ptr': return new U64Coder(); + case 'raw untyped slice': + return new RawSliceCoder(); case 'bool': return new BooleanCoder(); case 'b256': return new B256Coder(); case 'struct B512': return new B512Coder(); + case BYTES_CODER_TYPE: + return new ByteCoder(); default: break; } diff --git a/packages/abi-coder/src/coders/byte.test.ts b/packages/abi-coder/src/coders/byte.test.ts new file mode 100644 index 00000000000..17c53bbc318 --- /dev/null +++ b/packages/abi-coder/src/coders/byte.test.ts @@ -0,0 +1,47 @@ +import type { Uint8ArrayWithDynamicData } from '../utilities'; + +import { ByteCoder } from './byte'; + +describe('ByteCoder', () => { + it('should encode a byte', () => { + const coder = new ByteCoder(); + const expected: Uint8ArrayWithDynamicData = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 3, + ]); + expected.dynamicData = { + 0: new Uint8Array([1, 2, 3, 0, 0, 0, 0, 0]), + }; + + const actual = coder.encode([1, 2, 3]); + + expect(actual).toStrictEqual(expected); + }); + + it('should encode a byte [full word]', () => { + const coder = new ByteCoder(); + const expected: Uint8ArrayWithDynamicData = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, + ]); + expected.dynamicData = { + 0: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), + }; + + const actual = coder.encode([1, 2, 3, 4, 5, 6, 7, 8]); + + expect(actual).toStrictEqual(expected); + }); + + it('should decode a byte', () => { + const coder = new ByteCoder(); + const input = new Uint8Array([ + 0, 0, 0, 0, 3, 255, 255, 225, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 10, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, + ]); + const expected = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + const [actual, newOffset] = coder.decode(input, 0); + + expect(actual).toEqual(expected); + expect(newOffset).toEqual(24); + }); +}); diff --git a/packages/abi-coder/src/coders/byte.ts b/packages/abi-coder/src/coders/byte.ts new file mode 100644 index 00000000000..e297c22d1eb --- /dev/null +++ b/packages/abi-coder/src/coders/byte.ts @@ -0,0 +1,63 @@ +import { arrayify, concat } from '@ethersproject/bytes'; +import { ErrorCode } from '@fuel-ts/errors'; +import { bn } from '@fuel-ts/math'; + +import { WORD_SIZE } from '../constants'; +import type { Uint8ArrayWithDynamicData } from '../utilities'; +import { BASE_VECTOR_OFFSET, concatWithDynamicData } from '../utilities'; + +import { Coder } from './abstract-coder'; +import { U64Coder } from './u64'; + +export class ByteCoder extends Coder { + static memorySize = 1; + constructor() { + super('struct', 'struct Bytes', BASE_VECTOR_OFFSET); + } + + encode(value: number[]): Uint8Array { + if (!Array.isArray(value)) { + this.throwError(ErrorCode.ENCODE_ERROR, `Expected array value.`); + } + + const parts: Uint8Array[] = []; + + // pointer (ptr) + const pointer: Uint8ArrayWithDynamicData = new U64Coder().encode(BASE_VECTOR_OFFSET); + + // pointer dynamicData, encode the byte vector now and attach to its pointer + const data = this.#getPaddedData(value); + pointer.dynamicData = { + 0: concatWithDynamicData([data]), + }; + + parts.push(pointer); + + // capacity (cap) + parts.push(new U64Coder().encode(data.byteLength)); + + // length (len) + parts.push(new U64Coder().encode(value.length)); + + return concatWithDynamicData(parts); + } + + #getPaddedData(value: number[]): Uint8Array { + const data: Uint8Array[] = [arrayify(value)]; + + const paddingLength = (WORD_SIZE - (value.length % WORD_SIZE)) % WORD_SIZE; + if (paddingLength) { + data.push(new Uint8Array(paddingLength)); + } + + return concat(data); + } + + decode(data: Uint8Array, offset: number): [Uint8Array, number] { + const len = data.slice(16, 24); + const length = bn(new U64Coder().decode(len, 0)[0]).toNumber(); + const byteData = data.slice(BASE_VECTOR_OFFSET, BASE_VECTOR_OFFSET + length * 8); + + return [byteData, offset + BASE_VECTOR_OFFSET]; + } +} diff --git a/packages/abi-coder/src/coders/raw-slice.test.ts b/packages/abi-coder/src/coders/raw-slice.test.ts new file mode 100644 index 00000000000..adf81641101 --- /dev/null +++ b/packages/abi-coder/src/coders/raw-slice.test.ts @@ -0,0 +1,35 @@ +import type { BN } from '@fuel-ts/math'; + +import type { Uint8ArrayWithDynamicData } from '../utilities'; + +import { RawSliceCoder } from './raw-slice'; + +describe('RawSliceCoder', () => { + it('should encode a raw-slice', () => { + const coder = new RawSliceCoder(); + const expected: Uint8ArrayWithDynamicData = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 24, + ]); + expected.dynamicData = { + 0: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3]), + }; + + const actual = coder.encode([1, 2, 3]); + + expect(actual).toStrictEqual(expected); + }); + + it('should decode a raw-slice', () => { + const coder = new RawSliceCoder(); + const input = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, + 0, 7, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 9, + ]); + + const [actual, newOffset] = coder.decode(input, 0); + + expect(actual.map((v: BN) => v.toNumber())).toStrictEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(newOffset).toEqual(80); + }); +}); diff --git a/packages/abi-coder/src/coders/raw-slice.ts b/packages/abi-coder/src/coders/raw-slice.ts new file mode 100644 index 00000000000..843c41ad370 --- /dev/null +++ b/packages/abi-coder/src/coders/raw-slice.ts @@ -0,0 +1,47 @@ +import { ErrorCode } from '@fuel-ts/errors'; +import type { BN } from '@fuel-ts/math'; + +import { WORD_SIZE } from '../constants'; +import type { Uint8ArrayWithDynamicData } from '../utilities'; +import { BASE_RAW_SLICE_OFFSET, concatWithDynamicData } from '../utilities'; + +import { Coder } from './abstract-coder'; +import { ArrayCoder } from './array'; +import { U64Coder } from './u64'; + +export class RawSliceCoder extends Coder { + constructor() { + super('raw untyped slice', 'raw untyped slice', BASE_RAW_SLICE_OFFSET); + } + + encode(value: number[]): Uint8Array { + if (!Array.isArray(value)) { + this.throwError(ErrorCode.ENCODE_ERROR, `Expected array value.`); + } + + const parts: Uint8Array[] = []; + const coder = new U64Coder(); + + // pointer (ptr) + const pointer: Uint8ArrayWithDynamicData = new U64Coder().encode(BASE_RAW_SLICE_OFFSET); + + // pointer dynamicData, encode the vector now and attach to its pointer + pointer.dynamicData = { + 0: concatWithDynamicData(value.map((v) => coder.encode(v))), + }; + + parts.push(pointer); + + // length (len) + parts.push(new U64Coder().encode(value.length * WORD_SIZE)); + + return concatWithDynamicData(parts); + } + + decode(data: Uint8Array, offset: number): [BN[], number] { + const internalCoder = new ArrayCoder(new U64Coder(), data.length / 8); + const decoded = internalCoder.decode(data, offset); + + return decoded; + } +} diff --git a/packages/abi-coder/src/function-fragment.ts b/packages/abi-coder/src/function-fragment.ts index 79cd45aff00..c147a54bb5c 100644 --- a/packages/abi-coder/src/function-fragment.ts +++ b/packages/abi-coder/src/function-fragment.ts @@ -8,6 +8,7 @@ import { bn } from '@fuel-ts/math'; import { AbiCoder } from './abi-coder'; import type { DecodedValue, InputValue } from './coders/abstract-coder'; import type { ArrayCoder } from './coders/array'; +import { ByteCoder } from './coders/byte'; import { TupleCoder } from './coders/tuple'; import type { U64Coder } from './coders/u64'; import { VecCoder } from './coders/vec'; @@ -87,6 +88,9 @@ export class FunctionFragment< if (heapCoder instanceof VecCoder) { return heapCoder.coder.encodedLength; } + if (heapCoder instanceof ByteCoder) { + return ByteCoder.memorySize; + } return heapCoder.encodedLength; } catch (e) { diff --git a/packages/abi-coder/src/resolved-abi-type.ts b/packages/abi-coder/src/resolved-abi-type.ts index 2c4585341e6..6c0ed9de0c5 100644 --- a/packages/abi-coder/src/resolved-abi-type.ts +++ b/packages/abi-coder/src/resolved-abi-type.ts @@ -154,6 +154,10 @@ export class ResolvedAbiType { return 'rawptr'; } + if (this.type === 'raw untyped slice') { + return 'rawslice'; + } + const strMatch = stringRegEx.exec(this.type)?.groups; if (strMatch) { return `str[${strMatch.length}]`; diff --git a/packages/abi-coder/src/utilities.ts b/packages/abi-coder/src/utilities.ts index 69a61ab9404..780ff022884 100644 --- a/packages/abi-coder/src/utilities.ts +++ b/packages/abi-coder/src/utilities.ts @@ -16,6 +16,9 @@ export type Uint8ArrayWithDynamicData = Uint8Array & { const VEC_PROPERTY_SPACE = 3; // ptr + cap + length export const BASE_VECTOR_OFFSET = VEC_PROPERTY_SPACE * WORD_SIZE; +const RAW_SLICE_PROPERTY_SPACE = 2; // ptr + length +export const BASE_RAW_SLICE_OFFSET = RAW_SLICE_PROPERTY_SPACE * WORD_SIZE; + // this is a fork of @ethersproject/bytes:concat // this collects individual dynamicData data and relocates it to top level export function concatWithDynamicData(items: ReadonlyArray): Uint8ArrayWithDynamicData { diff --git a/packages/abi-coder/test/interface.test.ts b/packages/abi-coder/test/interface.test.ts index d3bb06f8e3d..35e82501913 100644 --- a/packages/abi-coder/test/interface.test.ts +++ b/packages/abi-coder/test/interface.test.ts @@ -295,6 +295,33 @@ describe('Abi interface', () => { value: { arr: [B256_DECODED, B256_DECODED, B256_DECODED], tuple: [B256_DECODED, U8_MAX] }, encodedValue: [B256_ENCODED, B256_ENCODED, B256_ENCODED, B256_ENCODED, U8_MAX_ENCODED], }, + { + fn: exhaustiveExamplesInterface.functions.bytes, + title: '[struct Bytes]', + value: [[1, 2, 3]], + encodedValue: new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 3, 1, 2, 3, 0, 0, + 0, 0, 0, + ]), + decodedTransformer: (decoded: unknown | undefined) => { + const data = (decoded as BN[]).slice(0, 3); + return Array.from(data); + }, + decodedTransfoarmer: (decoded: unknown | undefined) => Array.from(decoded as Uint8Array), + }, + { + fn: exhaustiveExamplesInterface.functions.raw_slice, + title: '[raw_slice]', + value: [[1, 2, 3]], + encodedValue: new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, + ]), + decodedTransformer: (decoded: unknown | undefined) => { + const data = (decoded as BN[]).slice(2); + return data.map((v: BN) => v.toNumber()); + }, + }, { fn: exhaustiveExamplesInterface.functions.tuple_as_param, title: '[tuple] as param', diff --git a/packages/abi-coder/test/sway-projects/exhaustive-examples/src/main.sw b/packages/abi-coder/test/sway-projects/exhaustive-examples/src/main.sw index 7c0cad93ae1..6076d90be3c 100644 --- a/packages/abi-coder/test/sway-projects/exhaustive-examples/src/main.sw +++ b/packages/abi-coder/test/sway-projects/exhaustive-examples/src/main.sw @@ -1,5 +1,6 @@ contract; use std::b512::B512; +use std::bytes::Bytes; enum EnumWithGeneric { VariantOne: T, @@ -132,7 +133,9 @@ abi MyContract { fn struct_generic_simple(x: StructB) -> StructB; fn struct_with_tuple(x: StructB<(bool, u64)>) -> StructB<(bool, u64)>; fn struct_with_implicitGenerics(arg: StructWithImplicitGenerics) -> StructWithImplicitGenerics; - + fn bytes(arg: Bytes) -> Bytes; + fn raw_slice(arg: raw_slice) -> raw_slice; + fn tuple_as_param(x: (u8, StructA, str[3]>)) -> (u8, StructA, str[3]>); fn array_simple(x: [u8; 4]) -> [u8; 4]; fn array_struct(x: [SimpleStruct; 3]) -> [SimpleStruct; 3]; @@ -244,6 +247,13 @@ impl MyContract for Contract { arg } + fn bytes(arg: Bytes) -> Bytes { + arg + } + fn raw_slice(arg: raw_slice) -> raw_slice { + arg + } + fn two_u8_vectors(x: Vec, y: Vec) -> (Vec, Vec) { (x, y) } diff --git a/packages/fuel-gauge/fixtures/forc-projects/Forc.toml b/packages/fuel-gauge/fixtures/forc-projects/Forc.toml index 20c6e0f9ce8..4d012ebfe2d 100644 --- a/packages/fuel-gauge/fixtures/forc-projects/Forc.toml +++ b/packages/fuel-gauge/fixtures/forc-projects/Forc.toml @@ -5,6 +5,7 @@ members = [ "advanced-logging-other-contract-abi", "auth_testing_abi", "auth_testing_contract", + "bytes", "call-test-contract", "configurable-contract", "collision_in_fn_names", @@ -25,6 +26,7 @@ members = [ "predicate-with-configurable", "predicate-u32", "predicate-vector-types", + "raw-slice", "revert-error", "script-main-args", "script-main-return-struct", diff --git a/packages/fuel-gauge/fixtures/forc-projects/bytes/Forc.toml b/packages/fuel-gauge/fixtures/forc-projects/bytes/Forc.toml new file mode 100644 index 00000000000..f67a8ec9a33 --- /dev/null +++ b/packages/fuel-gauge/fixtures/forc-projects/bytes/Forc.toml @@ -0,0 +1,3 @@ +[project] +license = "Apache-2.0" +name = "bytes" diff --git a/packages/fuel-gauge/fixtures/forc-projects/bytes/src/main.sw b/packages/fuel-gauge/fixtures/forc-projects/bytes/src/main.sw new file mode 100644 index 00000000000..93b4a6fda7e --- /dev/null +++ b/packages/fuel-gauge/fixtures/forc-projects/bytes/src/main.sw @@ -0,0 +1,62 @@ +contract; + +use std::bytes::Bytes; + +#[allow(dead_code)] +enum SomeEnum { + First: bool, + Second: T, +} + +struct Wrapper { + inner: T, + inner_enum: SomeEnum, +} + +fn expected_bytes() -> Bytes { + let mut bytes = Bytes::new(); + + bytes.push(40u8); + bytes.push(41u8); + bytes.push(42u8); + + bytes +} + +abi MyContract { + fn accept_bytes(bytes: Bytes); + fn accept_nested_bytes(wrapper: Wrapper>); + fn return_bytes(len: u8) -> Bytes; +} + +impl MyContract for Contract { + fn accept_bytes(bytes: Bytes) { + require(bytes == expected_bytes(), "given bytes didn't match the expected bytes"); + } + + fn accept_nested_bytes(wrapper: Wrapper>) { + if let SomeEnum::Second(enum_bytes) = wrapper.inner_enum { + let exp = expected_bytes(); + require(enum_bytes.get(0).unwrap() == exp.get(0).unwrap(), "wrapper.inner_enum 0 didnt match"); + require(enum_bytes.get(1).unwrap() == exp.get(1).unwrap(), "wrapper.inner_enum 1 didnt match"); + require(enum_bytes.get(2).unwrap() == exp.get(2).unwrap(), "wrapper.inner_enum 2 didnt match"); + } else { + require(false, "enum was not of variant Second"); + } + + let inner_vec = wrapper.inner; + require(inner_vec.len() == 2, "Expected wrapper.inner vector to have 2 elements"); + require(inner_vec.get(0).unwrap() == expected_bytes(), "wrapper.inner[0] didn't match expectation"); + require(inner_vec.get(1).unwrap() == expected_bytes(), "wrapper.inner[1] didn't match expectation"); + } + + fn return_bytes(len: u8) -> Bytes { + let mut bytes = Bytes::new(); + let mut i: u8 = 0; + while i < len { + bytes.push(i); + i += 1u8; + } + bytes + } +} \ No newline at end of file diff --git a/packages/fuel-gauge/fixtures/forc-projects/raw-slice/Forc.toml b/packages/fuel-gauge/fixtures/forc-projects/raw-slice/Forc.toml new file mode 100644 index 00000000000..296855f7eb1 --- /dev/null +++ b/packages/fuel-gauge/fixtures/forc-projects/raw-slice/Forc.toml @@ -0,0 +1,3 @@ +[project] +license = "Apache-2.0" +name = "raw-slice" diff --git a/packages/fuel-gauge/fixtures/forc-projects/raw-slice/src/main.sw b/packages/fuel-gauge/fixtures/forc-projects/raw-slice/src/main.sw new file mode 100644 index 00000000000..7ea541fc69c --- /dev/null +++ b/packages/fuel-gauge/fixtures/forc-projects/raw-slice/src/main.sw @@ -0,0 +1,59 @@ +contract; + +#[allow(dead_code)] +enum SomeEnum { + First: bool, + Second: T, +} + +struct Wrapper { + inner: T, + inner_enum: SomeEnum, +} + +abi RawSliceContract { + fn return_raw_slice(length: u64) -> raw_slice; + fn accept_raw_slice(slice: raw_slice); + fn accept_nested_raw_slice(wrapper: Wrapper>); +} + +fn validate_raw_slice(input: raw_slice) { + let vec: Vec = Vec::from(input); + require(vec.len() == 3, "raw slice len is not 3"); + require(vec.get(2).unwrap() == 42, "expected 3rd slice entry to be 42"); + require(vec.get(1).unwrap() == 41, "expected 2nd slice entry to be 41"); + require(vec.get(0).unwrap() == 40, "expected 1st slice entry to be 40"); +} + +fn validate_vec(vec: Vec) { + require(vec.len() == 2, "vec should have two elements"); + validate_raw_slice(vec.get(0).unwrap()); + validate_raw_slice(vec.get(1).unwrap()); +} + +impl RawSliceContract for Contract { + fn return_raw_slice(length: u64) -> raw_slice { + let mut vec = Vec::new(); + let mut counter = 0; + while counter < length { + vec.push(counter); + counter = counter + 1; + } + vec.as_raw_slice() + } + + fn accept_raw_slice(slice: raw_slice) { + validate_raw_slice(slice); + } + + fn accept_nested_raw_slice(wrapper: Wrapper>) { + if let SomeEnum::Second(enum_raw_slice) = wrapper.inner_enum + { + validate_raw_slice(enum_raw_slice); + } else { + require(false, "enum was not of variant Second"); + } + + validate_vec(wrapper.inner); + } +} diff --git a/packages/fuel-gauge/src/bytes.test.ts b/packages/fuel-gauge/src/bytes.test.ts new file mode 100644 index 00000000000..600c50c0b6a --- /dev/null +++ b/packages/fuel-gauge/src/bytes.test.ts @@ -0,0 +1,47 @@ +import { type Contract } from 'fuels'; + +import { getSetupContract } from './utils'; + +const setupContract = getSetupContract('bytes'); +let contractInstance: Contract; +beforeAll(async () => { + contractInstance = await setupContract(); +}); + +describe('Bytes Tests', () => { + it('should test bytes output', async () => { + const INPUT = 10; + + const { value } = await contractInstance.functions.return_bytes(INPUT).call(); + + expect(value).toStrictEqual(new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + }); + + it('should test bytes output [100 items]', async () => { + const INPUT = 100; + + const { value } = await contractInstance.functions.return_bytes(INPUT).call(); + + expect(value).toStrictEqual(new Uint8Array(Array.from({ length: 100 }, (e, i) => i))); + }); + + it('should test bytes input', async () => { + const INPUT = [40, 41, 42]; + + await contractInstance.functions.accept_bytes(INPUT).call(); + + expect(true).toBeTruthy(); + }); + + it('should test bytes input [nested]', async () => { + const bytes = [40, 41, 42]; + const INPUT = { + inner: [bytes, bytes], + inner_enum: { Second: bytes }, + }; + + await contractInstance.functions.accept_nested_bytes(INPUT).call(); + + expect(true).toBeTruthy(); + }); +}); diff --git a/packages/fuel-gauge/src/raw-slice.test.ts b/packages/fuel-gauge/src/raw-slice.test.ts new file mode 100644 index 00000000000..fa9411e2135 --- /dev/null +++ b/packages/fuel-gauge/src/raw-slice.test.ts @@ -0,0 +1,40 @@ +import type { BN } from 'fuels'; +import { type Contract } from 'fuels'; + +import { getSetupContract } from './utils'; + +const setupContract = getSetupContract('raw-slice'); +let contractInstance: Contract; +beforeAll(async () => { + contractInstance = await setupContract(); +}); + +describe('Raw Slice Tests', () => { + it('should test raw slice output', async () => { + const INPUT = 10; + + const { value } = await contractInstance.functions.return_raw_slice(INPUT).call(); + + expect(value.map((v: BN) => v.toNumber())).toStrictEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + + it('should test raw slice input', async () => { + const INPUT = [40, 41, 42]; + + await contractInstance.functions.accept_raw_slice(INPUT).call(); + + expect(true).toBeTruthy(); + }); + + it('should test raw slice input [nested]', async () => { + const slice = [40, 41, 42]; + const INPUT = { + inner: [slice, slice], + inner_enum: { Second: slice }, + }; + + await contractInstance.functions.accept_nested_raw_slice(INPUT).call(); + + expect(true).toBeTruthy(); + }); +}); diff --git a/packages/program/src/utils.ts b/packages/program/src/utils.ts index 0b527f38e22..8151f28017b 100644 --- a/packages/program/src/utils.ts +++ b/packages/program/src/utils.ts @@ -6,8 +6,13 @@ import { PANIC_REASONS, PANIC_DOC_URL } from './configs'; /** * @hidden */ -const getFailureReason = (reason: string): string => - PANIC_REASONS.includes(reason) ? reason : 'unknown'; +const getFailureReason = (reason: string): string => { + if (PANIC_REASONS.includes(reason)) { + return reason; + } + + return reason === 'Revert(123)' ? 'MismatchedSelector' : 'unknown'; +}; /** * @hidden