diff --git a/.changeset/light-bikes-cover.md b/.changeset/light-bikes-cover.md new file mode 100644 index 00000000000..2b633e66931 --- /dev/null +++ b/.changeset/light-bikes-cover.md @@ -0,0 +1,7 @@ +--- +"@fuel-ts/abi-coder": minor +"@fuel-ts/forc": minor +"@fuel-ts/program": minor +--- + +feat!: support `v1` encoding in program types diff --git a/packages/abi-coder/src/FunctionFragment.ts b/packages/abi-coder/src/FunctionFragment.ts index 0925ab1594b..73ff018532c 100644 --- a/packages/abi-coder/src/FunctionFragment.ts +++ b/packages/abi-coder/src/FunctionFragment.ts @@ -11,13 +11,16 @@ import type { DecodedValue, InputValue } from './encoding/coders/AbstractCoder'; import { ByteCoder } from './encoding/coders/v0/ByteCoder'; import { TupleCoder } from './encoding/coders/v0/TupleCoder'; import { VecCoder } from './encoding/coders/v0/VecCoder'; +import { StdStringCoder } from './encoding/coders/v1/StdStringCoder'; +import { TupleCoder as TupleCoderV1 } from './encoding/coders/v1/TupleCoder'; import type { JsonAbi, JsonAbiArgument, JsonAbiFunction, JsonAbiFunctionAttribute, } from './types/JsonAbi'; -import { OPTION_CODER_TYPE } from './utils/constants'; +import type { EncodingVersion } from './utils/constants'; +import { ENCODING_V0, ENCODING_V1, OPTION_CODER_TYPE } from './utils/constants'; import type { Uint8ArrayWithDynamicData } from './utils/utilities'; import { isPointerType, unpackDynamicData, findOrThrow, isHeapType } from './utils/utilities'; @@ -27,6 +30,8 @@ export class FunctionFragment< > { readonly signature: string; readonly selector: string; + readonly selectorBytes: Uint8Array; + readonly encoding: EncodingVersion; readonly name: string; readonly jsonFn: JsonAbiFunction; readonly attributes: readonly JsonAbiFunctionAttribute[]; @@ -44,6 +49,8 @@ export class FunctionFragment< this.name = name; this.signature = FunctionFragment.getSignature(this.jsonAbi, this.jsonFn); this.selector = FunctionFragment.getFunctionSelector(this.signature); + this.selectorBytes = new StdStringCoder().encode(name); + this.encoding = this.jsonAbi.encoding ?? ENCODING_V0; this.isInputDataPointer = this.#isInputDataPointer(); this.outputMetadata = { isHeapType: this.#isOutputDataHeap(), @@ -113,12 +120,14 @@ export class FunctionFragment< const coders = nonEmptyInputs.map((t) => AbiCoder.getCoder(this.jsonAbi, t, { isRightPadded: nonEmptyInputs.length > 1, + encoding: this.encoding, }) ); - const coder = new TupleCoder(coders); - const results: Uint8ArrayWithDynamicData = coder.encode(shallowCopyValues); - + if (this.encoding === ENCODING_V1) { + return new TupleCoderV1(coders).encode(shallowCopyValues); + } + const results: Uint8ArrayWithDynamicData = new TupleCoder(coders).encode(shallowCopyValues); return unpackDynamicData(results, offset, results.byteLength); } @@ -182,7 +191,7 @@ export class FunctionFragment< const result = nonEmptyInputs.reduce( (obj: { decoded: unknown[]; offset: number }, input) => { - const coder = AbiCoder.getCoder(this.jsonAbi, input); + const coder = AbiCoder.getCoder(this.jsonAbi, input, { encoding: this.encoding }); const [decodedValue, decodedValueByteSize] = coder.decode(bytes, obj.offset); return { @@ -206,7 +215,9 @@ export class FunctionFragment< } const bytes = arrayify(data); - const coder = AbiCoder.getCoder(this.jsonAbi, this.jsonFn.output); + const coder = AbiCoder.getCoder(this.jsonAbi, this.jsonFn.output, { + encoding: this.encoding, + }); return coder.decode(bytes, 0) as [DecodedValue | undefined, number]; } diff --git a/packages/abi-coder/src/Interface.ts b/packages/abi-coder/src/Interface.ts index 7e3ddb3fdb5..77ec9bf811e 100644 --- a/packages/abi-coder/src/Interface.ts +++ b/packages/abi-coder/src/Interface.ts @@ -96,6 +96,7 @@ export class Interface { return AbiCoder.encode(this.jsonAbi, configurable.configurableType, value, { isRightPadded: true, + encoding: this.jsonAbi.encoding, }); } diff --git a/packages/abi-coder/src/encoding/coders/v0/ByteCoder.test.ts b/packages/abi-coder/src/encoding/coders/v0/ByteCoder.test.ts index 3e9b9fb1760..d54e552c6b1 100644 --- a/packages/abi-coder/src/encoding/coders/v0/ByteCoder.test.ts +++ b/packages/abi-coder/src/encoding/coders/v0/ByteCoder.test.ts @@ -24,6 +24,20 @@ describe('ByteCoder', () => { expect(actual).toStrictEqual(expected); }); + it('should encode a byte [byte array]', () => { + 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(Uint8Array.from([1, 2, 3])); + + expect(actual).toStrictEqual(expected); + }); + it('should encode a byte [full word]', () => { const coder = new ByteCoder(); const expected: Uint8ArrayWithDynamicData = new Uint8Array([ @@ -38,15 +52,6 @@ describe('ByteCoder', () => { expect(actual).toStrictEqual(expected); }); - it('should throw when value to encode is not array', async () => { - const coder = new ByteCoder(); - const nonArrayInput = { ...[1] }; - await expectToThrowFuelError( - () => coder.encode(nonArrayInput), - new FuelError(ErrorCode.ENCODE_ERROR, 'Expected array value.') - ); - }); - it('should decode a byte', () => { const coder = new ByteCoder(); const input = new Uint8Array([ diff --git a/packages/abi-coder/src/encoding/coders/v0/ByteCoder.ts b/packages/abi-coder/src/encoding/coders/v0/ByteCoder.ts index 32f2d891271..444d3877a31 100644 --- a/packages/abi-coder/src/encoding/coders/v0/ByteCoder.ts +++ b/packages/abi-coder/src/encoding/coders/v0/ByteCoder.ts @@ -15,11 +15,7 @@ export class ByteCoder extends Coder { super('struct', 'struct Bytes', BASE_VECTOR_OFFSET); } - encode(value: number[]): Uint8Array { - if (!Array.isArray(value)) { - throw new FuelError(ErrorCode.ENCODE_ERROR, `Expected array value.`); - } - + encode(value: number[] | Uint8Array): Uint8Array { const parts: Uint8Array[] = []; // pointer (ptr) @@ -42,8 +38,8 @@ export class ByteCoder extends Coder { return concatWithDynamicData(parts); } - #getPaddedData(value: number[]): Uint8Array { - const data: Uint8Array[] = [Uint8Array.from(value)]; + #getPaddedData(value: number[] | Uint8Array): Uint8Array { + const data = value instanceof Uint8Array ? [value] : [new Uint8Array(value)]; const paddingLength = (WORD_SIZE - (value.length % WORD_SIZE)) % WORD_SIZE; if (paddingLength) { diff --git a/packages/abi-coder/src/encoding/coders/v0/EnumCoder.ts b/packages/abi-coder/src/encoding/coders/v0/EnumCoder.ts index 160dea65083..17afb3cb597 100644 --- a/packages/abi-coder/src/encoding/coders/v0/EnumCoder.ts +++ b/packages/abi-coder/src/encoding/coders/v0/EnumCoder.ts @@ -37,7 +37,7 @@ export class EnumCoder> extends Coder< (max, coder) => Math.max(max, coder.encodedLength), 0 ); - super('enum', `enum ${name}`, caseIndexCoder.encodedLength + encodedValueSize); + super(`enum ${name}`, `enum ${name}`, caseIndexCoder.encodedLength + encodedValueSize); this.name = name; this.coders = coders; this.#caseIndexCoder = caseIndexCoder; diff --git a/packages/abi-coder/src/encoding/coders/v0/OptionCoder.ts b/packages/abi-coder/src/encoding/coders/v0/OptionCoder.ts index 20646fb8f4f..f333d2dea02 100644 --- a/packages/abi-coder/src/encoding/coders/v0/OptionCoder.ts +++ b/packages/abi-coder/src/encoding/coders/v0/OptionCoder.ts @@ -23,8 +23,7 @@ export class OptionCoder> extends EnumCode } decode(data: Uint8Array, offset: number): [DecodedValueOf, number] { - // An empty option will be one less than the expected encoded length - if (data.length < this.encodedLength - 1) { + if (data.length < this.encodedLength) { throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid option data size.`); } diff --git a/packages/abi-coder/src/encoding/coders/v1/ByteCoder.test.ts b/packages/abi-coder/src/encoding/coders/v1/ByteCoder.test.ts index 2bb95e0bcd6..062ed94d151 100644 --- a/packages/abi-coder/src/encoding/coders/v1/ByteCoder.test.ts +++ b/packages/abi-coder/src/encoding/coders/v1/ByteCoder.test.ts @@ -17,6 +17,13 @@ describe('ByteCoder', () => { expect(actual).toStrictEqual(expected); }); + it('should encode a byte [byte array]', () => { + const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 3, 1, 2, 3]); + const actual = coder.encode(Uint8Array.from([1, 2, 3])); + + expect(actual).toStrictEqual(expected); + }); + it('should encode a byte [full word]', () => { const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 8, 1, 2, 3, 4, 5, 6, 7, 8]); const actual = coder.encode([1, 2, 3, 4, 5, 6, 7, 8]); diff --git a/packages/abi-coder/src/encoding/coders/v1/ByteCoder.ts b/packages/abi-coder/src/encoding/coders/v1/ByteCoder.ts index 1996a395458..285ec8dd986 100644 --- a/packages/abi-coder/src/encoding/coders/v1/ByteCoder.ts +++ b/packages/abi-coder/src/encoding/coders/v1/ByteCoder.ts @@ -11,13 +11,9 @@ export class ByteCoder extends Coder { super('struct', 'struct Bytes', WORD_SIZE); } - encode(value: number[]): Uint8Array { - if (!Array.isArray(value)) { - throw new FuelError(ErrorCode.ENCODE_ERROR, `Expected array value.`); - } - - const bytes = new Uint8Array(value); - const lengthBytes = new BigNumberCoder('u64').encode(value.length); + encode(value: number[] | Uint8Array): Uint8Array { + const bytes = value instanceof Uint8Array ? value : new Uint8Array(value); + const lengthBytes = new BigNumberCoder('u64').encode(bytes.length); return new Uint8Array([...lengthBytes, ...bytes]); } diff --git a/packages/abi-coder/src/encoding/coders/v1/EnumCoder.ts b/packages/abi-coder/src/encoding/coders/v1/EnumCoder.ts index 7f9d5d4f205..680bc1e01e6 100644 --- a/packages/abi-coder/src/encoding/coders/v1/EnumCoder.ts +++ b/packages/abi-coder/src/encoding/coders/v1/EnumCoder.ts @@ -36,7 +36,7 @@ export class EnumCoder> extends Coder< (max, coder) => Math.max(max, coder.encodedLength), 0 ); - super('enum', `enum ${name}`, caseIndexCoder.encodedLength + encodedValueSize); + super(`enum ${name}`, `enum ${name}`, caseIndexCoder.encodedLength + encodedValueSize); this.name = name; this.coders = coders; this.#caseIndexCoder = caseIndexCoder; diff --git a/packages/abi-coder/src/encoding/coders/v1/OptionCoder.ts b/packages/abi-coder/src/encoding/coders/v1/OptionCoder.ts new file mode 100644 index 00000000000..45f16d8035c --- /dev/null +++ b/packages/abi-coder/src/encoding/coders/v1/OptionCoder.ts @@ -0,0 +1,35 @@ +import type { Coder } from '../AbstractCoder'; + +import type { InputValueOf, DecodedValueOf } from './EnumCoder'; +import { EnumCoder } from './EnumCoder'; + +type SwayOption = { None: [] } | { Some: T }; +export type Option = T | undefined; + +export class OptionCoder> extends EnumCoder { + encode(value: InputValueOf): Uint8Array { + const result = super.encode(this.toSwayOption(value) as unknown as InputValueOf); + return result; + } + + private toSwayOption(input: InputValueOf): SwayOption { + if (input !== undefined) { + return { Some: input }; + } + + return { None: [] }; + } + + decode(data: Uint8Array, offset: number): [DecodedValueOf, number] { + const [decoded, newOffset] = super.decode(data, offset); + return [this.toOption(decoded) as DecodedValueOf, newOffset]; + } + + private toOption(output?: DecodedValueOf): Option { + if (output && 'Some' in output) { + return output.Some; + } + + return undefined; + } +} diff --git a/packages/abi-coder/src/encoding/coders/v1/StructCoder.ts b/packages/abi-coder/src/encoding/coders/v1/StructCoder.ts index f9337e3a8bb..55f96895b28 100644 --- a/packages/abi-coder/src/encoding/coders/v1/StructCoder.ts +++ b/packages/abi-coder/src/encoding/coders/v1/StructCoder.ts @@ -3,7 +3,8 @@ import { concatBytes } from '@fuel-ts/utils'; import type { TypesOfCoder } from '../AbstractCoder'; import { Coder } from '../AbstractCoder'; -import { OptionCoder } from '../v0/OptionCoder'; + +import { OptionCoder } from './OptionCoder'; type InputValueOf> = { [P in keyof TCoders]: TypesOfCoder['Input']; diff --git a/packages/abi-coder/src/encoding/strategies/getCoderForEncoding.ts b/packages/abi-coder/src/encoding/strategies/getCoderForEncoding.ts index affeb32ed07..ca4f2b35aa9 100644 --- a/packages/abi-coder/src/encoding/strategies/getCoderForEncoding.ts +++ b/packages/abi-coder/src/encoding/strategies/getCoderForEncoding.ts @@ -1,6 +1,7 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { GetCoderFn } from '../../types/GetCoder'; +import type { EncodingVersion } from '../../utils/constants'; import { ENCODING_V0, ENCODING_V1 } from '../../utils/constants'; import { getCoder as getCoderV0 } from './getCoderV0'; @@ -13,7 +14,7 @@ import { getCoder as getCoderV1 } from './getCoderV1'; * @throws for an unsupported encoding version. * @returns the appropriate encoding strategy. */ -export function getCoderForEncoding(encoding: string = ENCODING_V0): GetCoderFn { +export function getCoderForEncoding(encoding: EncodingVersion = ENCODING_V0): GetCoderFn { switch (encoding) { case ENCODING_V1: return getCoderV1; diff --git a/packages/abi-coder/src/encoding/strategies/getCoderV1.ts b/packages/abi-coder/src/encoding/strategies/getCoderV1.ts index db74dc6c361..c9163f2630f 100644 --- a/packages/abi-coder/src/encoding/strategies/getCoderV1.ts +++ b/packages/abi-coder/src/encoding/strategies/getCoderV1.ts @@ -32,13 +32,14 @@ import { ArrayCoder } from '../coders/v0/ArrayCoder'; import { B256Coder } from '../coders/v0/B256Coder'; import { B512Coder } from '../coders/v0/B512Coder'; import { BigNumberCoder } from '../coders/v0/BigNumberCoder'; -import { OptionCoder } from '../coders/v0/OptionCoder'; import { BooleanCoder } from '../coders/v1/BooleanCoder'; import { ByteCoder } from '../coders/v1/ByteCoder'; import { EnumCoder } from '../coders/v1/EnumCoder'; import { NumberCoder } from '../coders/v1/NumberCoder'; +import { OptionCoder } from '../coders/v1/OptionCoder'; import { RawSliceCoder } from '../coders/v1/RawSliceCoder'; import { StdStringCoder } from '../coders/v1/StdStringCoder'; +import { StrSliceCoder } from '../coders/v1/StrSliceCoder'; import { StringCoder } from '../coders/v1/StringCoder'; import { StructCoder } from '../coders/v1/StructCoder'; import { TupleCoder } from '../coders/v1/TupleCoder'; @@ -79,6 +80,8 @@ export const getCoder: GetCoderFn = ( return new ByteCoder(); case STD_STRING_CODER_TYPE: return new StdStringCoder(); + case STR_SLICE_CODER_TYPE: + return new StrSliceCoder(); default: break; } @@ -149,13 +152,6 @@ export const getCoder: GetCoderFn = ( return new TupleCoder(coders as Coder[]); } - if (resolvedAbiType.type === STR_SLICE_CODER_TYPE) { - throw new FuelError( - ErrorCode.INVALID_DATA, - 'String slices can not be decoded from logs. Convert the slice to `str[N]` with `__to_str_array`' - ); - } - throw new FuelError( ErrorCode.CODER_NOT_FOUND, `Coder not found: ${JSON.stringify(resolvedAbiType)}.` diff --git a/packages/abi-coder/src/index.ts b/packages/abi-coder/src/index.ts index 21707ad02e1..b0bc0217fe9 100644 --- a/packages/abi-coder/src/index.ts +++ b/packages/abi-coder/src/index.ts @@ -23,5 +23,8 @@ export { ASSET_ID_LEN, CONTRACT_ID_LEN, calculateVmTxMemory, + type EncodingVersion, + ENCODING_V0, + ENCODING_V1, } from './utils/constants'; export { BigNumberCoder } from './encoding/coders/v0/BigNumberCoder'; diff --git a/packages/abi-coder/src/types/EncodingOptions.ts b/packages/abi-coder/src/types/EncodingOptions.ts index a0b4290a3d8..7577288937f 100644 --- a/packages/abi-coder/src/types/EncodingOptions.ts +++ b/packages/abi-coder/src/types/EncodingOptions.ts @@ -1,3 +1,5 @@ +import type { EncodingVersion } from '../utils/constants'; + /** * These are configurable options to be used when encoding. * @@ -56,7 +58,7 @@ * fuel specs (https://github.com/FuelLabs/fuel-specs/blob/master/src/abi/argument-encoding.md#version-1 */ export type EncodingOptions = { - encoding?: string; + encoding?: EncodingVersion; isSmallBytes?: boolean; isRightPadded?: boolean; }; diff --git a/packages/abi-coder/src/types/JsonAbi.ts b/packages/abi-coder/src/types/JsonAbi.ts index fd417d6cbcc..e936819d861 100644 --- a/packages/abi-coder/src/types/JsonAbi.ts +++ b/packages/abi-coder/src/types/JsonAbi.ts @@ -1,14 +1,16 @@ /** * Types for Fuel JSON ABI Format as defined on: - * https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/abi.md#json-abi-format + * https://github.com/FuelLabs/fuel-specs/blob/master/src/abi/json-abi-format.md */ +import type { EncodingVersion } from '../utils/constants'; + export interface JsonAbi { readonly types: readonly JsonAbiType[]; readonly loggedTypes: readonly JsonAbiLoggedType[]; readonly functions: readonly JsonAbiFunction[]; readonly configurables: readonly JsonAbiConfigurable[]; - readonly encoding?: string; + readonly encoding?: EncodingVersion; } export interface JsonAbiType { diff --git a/packages/abi-coder/src/utils/constants.ts b/packages/abi-coder/src/utils/constants.ts index bc5f05bc6ee..4210da14451 100644 --- a/packages/abi-coder/src/utils/constants.ts +++ b/packages/abi-coder/src/utils/constants.ts @@ -25,6 +25,7 @@ export const genericRegEx = /^generic (?\w+)$/; /** * Encoding versions */ +export type EncodingVersion = '0' | '1'; export const ENCODING_V0 = '0'; export const ENCODING_V1 = '1'; /** diff --git a/packages/forc/VERSION_EXPERIMENTAL b/packages/forc/VERSION_EXPERIMENTAL index fb6e7a44dd5..d06c2d46465 100644 --- a/packages/forc/VERSION_EXPERIMENTAL +++ b/packages/forc/VERSION_EXPERIMENTAL @@ -1 +1 @@ -0.51.1 \ No newline at end of file +0.52.1 \ No newline at end of file diff --git a/packages/fuel-gauge/src/experimental-contract.test.ts b/packages/fuel-gauge/src/experimental-contract.test.ts new file mode 100644 index 00000000000..96c24fbfaeb --- /dev/null +++ b/packages/fuel-gauge/src/experimental-contract.test.ts @@ -0,0 +1,89 @@ +import { readFileSync } from 'fs'; +import type { Contract } from 'fuels'; +import { bn } from 'fuels'; +import { join } from 'path'; + +import { setup } from './utils'; + +let contractInstance: Contract; + +const U8_MAX = 2 ** 8 - 1; +const U16_MAX = 2 ** 16 - 1; +const U32_MAX = 2 ** 32 - 1; +const U64_MAX = bn(2).pow(64).sub(1); +const U256_MAX = bn(2).pow(256).sub(1); +const B512 = + '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2'; + +/** + * @group node + */ +describe('Experimental Contract', () => { + beforeAll(async () => { + const projectName = 'contract-echo'; + const path = join( + __dirname, + `../test/fixtures/forc-projects-experimental/${projectName}/out/release/${projectName}` + ); + const contractBytecode = readFileSync(`${path}.bin`); + const abi = JSON.parse(readFileSync(`${path}-abi.json`, 'utf8')); + + contractInstance = await setup({ contractBytecode, abi }); + }); + + it('echos mixed struct with all types', async () => { + const struct = { + a: U8_MAX, + b: U16_MAX, + c: U32_MAX, + d: U64_MAX, + e: U256_MAX, + f: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6', + g: B512, + native: 'Pending', + mixed: { Value: true }, + grades: [1, 4, 6, 22], + fuel: 'fuel', + hello: 'Hello World', + opt: 42, + nada: undefined, + bytes: Uint8Array.from([40, 41, 42]), + tuple: [U8_MAX, U16_MAX, U32_MAX, 'fuel'], + vec_u8: [40, 41, 42], + str_slice: 'fuel', + deep: { + a: U8_MAX, + b: U16_MAX, + c: U32_MAX, + d: U64_MAX, + e: U256_MAX, + f: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6', + g: B512, + native: 'Pending', + mixed: { Value: true }, + grades: [1, 4, 6, 22], + fuel: 'fuel', + hello: 'Hello World', + opt: 42, + nada: undefined, + bytes: Uint8Array.from([40, 41, 42]), + tuple: [U8_MAX, U16_MAX, U32_MAX, 'fuel'], + vec_u8: [40, 41, 42], + even_deeper: { + nested_vec: [1, 2, 3, 4, 5], + nested_str: 'fuel labs', + nested_raw: [88, 89, 90, 100], + }, + }, + }; + + const { value } = await contractInstance.functions.echo_struct(struct).call(); + expect(value).toStrictEqual(struct); + }); + + it('extracts str slice from revert', async () => { + await expect(contractInstance.functions.test_revert().call()).rejects.toThrow( + 'The transaction reverted because a "require" statement has thrown "This is a revert error".' + ); + }); +}); diff --git a/packages/fuel-gauge/src/experimental-logging.test.ts b/packages/fuel-gauge/src/experimental-logging.test.ts index a54a67609c2..12e372a57b7 100644 --- a/packages/fuel-gauge/src/experimental-logging.test.ts +++ b/packages/fuel-gauge/src/experimental-logging.test.ts @@ -1,7 +1,6 @@ -import { randomBytes } from 'crypto'; import { readFileSync } from 'fs'; import type { BN, Contract } from 'fuels'; -import { bn, hexlify } from 'fuels'; +import { bn } from 'fuels'; import { join } from 'path'; import { setup } from './utils'; @@ -17,10 +16,10 @@ const B512 = '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2'; beforeAll(async () => { - const contractName = 'logging'; + const projectName = 'logging'; const path = join( __dirname, - `../test/fixtures/forc-projects-experimental/${contractName}/out/release/${contractName}` + `../test/fixtures/forc-projects-experimental/${projectName}/out/release/${projectName}` ); const contractBytecode = readFileSync(`${path}.bin`); const abi = JSON.parse(readFileSync(`${path}-abi.json`, 'utf8')); @@ -37,10 +36,7 @@ describe('Experimental Logging', () => { it('logs u8', async () => { const expected = U8_MAX; - const { logs } = await contractInstance.functions - .log_u8(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u8(expected).call(); expect(logs).toStrictEqual([expected]); }); @@ -48,10 +44,7 @@ describe('Experimental Logging', () => { it('logs u16', async () => { const expected = U16_MAX; - const { logs } = await contractInstance.functions - .log_u16(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u16(expected).call(); expect(logs).toStrictEqual([expected]); }); @@ -59,10 +52,7 @@ describe('Experimental Logging', () => { it('logs u32', async () => { const expected = U32_MAX; - const { logs } = await contractInstance.functions - .log_u32(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u32(expected).call(); expect(logs).toStrictEqual([expected]); }); @@ -70,10 +60,7 @@ describe('Experimental Logging', () => { it('logs u8 u16 u32 multiple params', async () => { const expected = [U8_MAX, U16_MAX, U32_MAX]; - const { value, logs } = await contractInstance.functions - .log_u8_u16_u32(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { value, logs } = await contractInstance.functions.log_u8_u16_u32(...expected).call(); expect(value).toEqual(expected); expect(logs).toEqual(expected); @@ -82,10 +69,7 @@ describe('Experimental Logging', () => { it('logs u64', async () => { const expected = U32_MAX + 1; - const { logs } = await contractInstance.functions - .log_u64(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u64(expected).call(); expect(bn(logs[0]).toNumber()).toStrictEqual(expected); }); @@ -101,10 +85,7 @@ describe('Experimental Logging', () => { it('logs u64 u8 multiple params', async () => { const expected = [U32_MAX + 1, U8_MAX]; - const { logs } = await contractInstance.functions - .log_u64_u8(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u64_u8(...expected).call(); expect(bn(logs[0]).toNumber()).toStrictEqual(expected[0]); expect(logs[1]).toEqual(expected[1]); @@ -113,10 +94,7 @@ describe('Experimental Logging', () => { it('logs boolean', async () => { const expected = true; - const { logs } = await contractInstance.functions - .log_boolean(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_boolean(expected).call(); expect(logs).toStrictEqual([expected]); }); @@ -124,10 +102,7 @@ describe('Experimental Logging', () => { it('logs boolean boolean multiple params', async () => { const expected = [true, false]; - const { logs } = await contractInstance.functions - .log_boolean_boolean(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_boolean_boolean(...expected).call(); expect(logs).toEqual(expected); }); @@ -135,10 +110,7 @@ describe('Experimental Logging', () => { it('logs number boolean mixed params', async () => { const expected = [U32_MAX, true]; - const { logs } = await contractInstance.functions - .log_number_boolean(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_number_boolean(...expected).call(); expect(logs).toEqual(expected); }); @@ -146,32 +118,39 @@ describe('Experimental Logging', () => { it('logs b256', async () => { const expected = B256; - const { logs } = await contractInstance.functions - .log_b256(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_b256(expected).call(); expect(logs).toStrictEqual([expected]); }); + it('logs b256 vec', async () => { + const expected = [B256, B256]; + + const { logs } = await contractInstance.functions.log_vec_b256(expected).call(); + + expect(logs).toEqual([expected]); + }); + it('logs b512', async () => { const expected = B512; - const { logs } = await contractInstance.functions - .log_b512(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_b512(expected).call(); expect(logs).toStrictEqual([expected]); }); + it('logs b512 vec', async () => { + const expected = [B512, B512]; + + const { logs } = await contractInstance.functions.log_vec_b512(expected).call(); + + expect(logs).toEqual([expected]); + }); + it('logs b256 b512 multiple params', async () => { const expected = [B256, B512]; - const { logs } = await contractInstance.functions - .log_b256_b512(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_b256_b512(...expected).call(); expect(logs).toEqual(expected); }); @@ -179,21 +158,7 @@ describe('Experimental Logging', () => { it('logs u8 vec', async () => { const expected = [U8_MAX, 1, U8_MAX, 5]; - const { logs } = await contractInstance.functions - .log_vec_u8(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); - - expect(logs).toEqual([expected]); - }); - - it('logs b256 vec', async () => { - const expected = [hexlify(randomBytes(32)), hexlify(randomBytes(32))]; - - const { logs } = await contractInstance.functions - .log_vec_b256(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_vec_u8(expected).call(); expect(logs).toEqual([expected]); }); @@ -201,20 +166,17 @@ describe('Experimental Logging', () => { it('logs bytes', async () => { const expected = [40, 41, 42]; - const { logs } = await contractInstance.functions - .log_bytes(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_bytes(expected).call(); expect(logs).toEqual([Uint8Array.from(expected)]); }); it('logs StdString', async () => { - const expected = 'hello world'; + const expected = 'fuel'; const { logs } = await contractInstance.functions .log_std_string(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) + .txParams({ gasLimit: 1_000_000, gasPrice }) .call(); expect(logs).toEqual([expected]); @@ -223,10 +185,7 @@ describe('Experimental Logging', () => { it('logs u16 bytes multiple params', async () => { const expected = [U16_MAX, [40, 41, 42]]; - const { logs } = await contractInstance.functions - .log_u16_bytes(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u16_bytes(...expected).call(); expect(logs).toEqual([U16_MAX, Uint8Array.from([40, 41, 42])]); }); @@ -234,10 +193,7 @@ describe('Experimental Logging', () => { it('logs u8 array', async () => { const expected = [U8_MAX, 5, U8_MAX]; - const { logs } = await contractInstance.functions - .log_u8_array(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u8_array(expected).call(); expect(logs).toEqual([expected]); }); @@ -245,10 +201,7 @@ describe('Experimental Logging', () => { it('logs u16 array', async () => { const expected = [U16_MAX, 5, U16_MAX]; - const { logs } = await contractInstance.functions - .log_u16_array(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u16_array(expected).call(); expect(logs).toEqual([expected]); }); @@ -256,10 +209,7 @@ describe('Experimental Logging', () => { it('log u16 u8 array multiple params', async () => { const expected = [U16_MAX, [U8_MAX, 5, U8_MAX]]; - const { logs } = await contractInstance.functions - .log_u16_u8_array(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u16_u8_array(...expected).call(); expect(logs).toEqual(expected); }); @@ -267,10 +217,7 @@ describe('Experimental Logging', () => { it('logs string', async () => { const expected = 'fuel'; - const { logs } = await contractInstance.functions - .log_str_4(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_str_4(expected).call(); expect(logs).toEqual([expected]); }); @@ -278,10 +225,7 @@ describe('Experimental Logging', () => { it('logs u8 string multiple params', async () => { const expected = [U8_MAX, 'at']; - const { logs } = await contractInstance.functions - .log_u8_str_2(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u8_str_2(...expected).call(); expect(logs).toEqual(expected); }); @@ -289,10 +233,7 @@ describe('Experimental Logging', () => { it('logs u8 u16 tuple', async () => { const expected = [U8_MAX, U16_MAX]; - const { logs } = await contractInstance.functions - .log_u8_u16_tuple(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_u8_u16_tuple(expected).call(); expect(logs).toEqual([expected]); }); @@ -301,15 +242,9 @@ describe('Experimental Logging', () => { const expectedFoo = { Foo: U32_MAX }; const expectedBar = { Bar: true }; - const { logs: logsFoo } = await contractInstance.functions - .log_enum(expectedFoo) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs: logsFoo } = await contractInstance.functions.log_enum(expectedFoo).call(); - const { logs: logsBar } = await contractInstance.functions - .log_enum(expectedBar) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs: logsBar } = await contractInstance.functions.log_enum(expectedBar).call(); expect(logsFoo).toEqual([expectedFoo]); expect(logsBar).toEqual([expectedBar]); @@ -319,15 +254,9 @@ describe('Experimental Logging', () => { const expectedFoo = 'Foo'; const expectedBar = 'Bar'; - const { logs: logsFoo } = await contractInstance.functions - .log_native_enum(expectedFoo) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs: logsFoo } = await contractInstance.functions.log_native_enum(expectedFoo).call(); - const { logs: logsBar } = await contractInstance.functions - .log_native_enum(expectedBar) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs: logsBar } = await contractInstance.functions.log_native_enum(expectedBar).call(); expect(logsFoo).toEqual([expectedFoo]); expect(logsBar).toEqual([expectedBar]); @@ -336,10 +265,7 @@ describe('Experimental Logging', () => { it('logs boolean enum multiple params', async () => { const expected = [true, { Foo: U32_MAX }]; - const { logs } = await contractInstance.functions - .log_boolean_enum(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_boolean_enum(...expected).call(); expect(logs).toEqual(expected); }); @@ -350,10 +276,7 @@ describe('Experimental Logging', () => { b: U16_MAX, }; - const { logs } = await contractInstance.functions - .log_struct(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_struct(expected).call(); expect(logs).toEqual([expected]); }); @@ -370,10 +293,7 @@ describe('Experimental Logging', () => { }, ]; - const { logs } = await contractInstance.functions - .log_struct_vec(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_struct_vec(expected).call(); expect(logs).toEqual([expected]); }); @@ -387,10 +307,7 @@ describe('Experimental Logging', () => { true, ]; - const { logs } = await contractInstance.functions - .log_struct_boolean(...expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_struct_boolean(...expected).call(); expect(logs).toEqual(expected); }); @@ -399,15 +316,9 @@ describe('Experimental Logging', () => { const expectedSome = U8_MAX; const expectedNone = undefined; - const { logs: logsSome } = await contractInstance.functions - .log_option_u8(expectedSome) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs: logsSome } = await contractInstance.functions.log_option_u8(expectedSome).call(); - const { logs: logsNone } = await contractInstance.functions - .log_option_u8(expectedNone) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs: logsNone } = await contractInstance.functions.log_option_u8(expectedNone).call(); expect(logsSome).toEqual([expectedSome]); expect(logsNone).toEqual([expectedNone]); @@ -416,22 +327,15 @@ describe('Experimental Logging', () => { it('logs raw slice', async () => { const expected = [40, 41, 42]; - const { logs } = await contractInstance.functions - .log_raw_slice(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_raw_slice(expected).call(); expect(logs).toEqual([expected]); }); - // Requires v1 encoding to be supported for contract calls - it.skip('logs str slice', async () => { + it('logs str slice', async () => { const expected = 'fuel'; - const { logs } = await contractInstance.functions - .log_str_slice(expected) - .txParams({ gasPrice, gasLimit: 10_000 }) - .call(); + const { logs } = await contractInstance.functions.log_str_slice(expected).call(); expect(logs).toEqual([expected]); }); diff --git a/packages/fuel-gauge/src/experimental-predicate.test.ts b/packages/fuel-gauge/src/experimental-predicate.test.ts new file mode 100644 index 00000000000..9f564156db3 --- /dev/null +++ b/packages/fuel-gauge/src/experimental-predicate.test.ts @@ -0,0 +1,97 @@ +import { readFileSync } from 'fs'; +import type { JsonAbi, Provider, WalletLocked, WalletUnlocked } from 'fuels'; +import { BaseAssetId, Predicate, bn } from 'fuels'; +import { join } from 'path'; + +import { fundPredicate, setupWallets } from './predicate/utils/predicate'; + +let wallet: WalletUnlocked; +let receiver: WalletLocked; +let bytecode: Buffer; +let abi: JsonAbi; +let provider: Provider; + +const U8_MAX = 255; +const U16_MAX = 65535; +const U32_MAX = 4294967295; +const U64_MAX = bn(2).pow(64).sub(1); +const U256_MAX = bn(2).pow(256).sub(1); +const B512 = + '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2'; + +/** + * @group node + */ +describe('Experimental Predicate', () => { + beforeAll(async () => { + [wallet, receiver] = await setupWallets(); + const name = 'predicate-echo'; + const path = join( + __dirname, + `../test/fixtures/forc-projects-experimental/${name}/out/release/${name}` + ); + bytecode = readFileSync(`${path}.bin`); + abi = JSON.parse(readFileSync(`${path}-abi.json`, 'utf8')); + provider = wallet.provider; + }); + + it('echos struct', async () => { + const struct = { + a: U8_MAX, + b: U16_MAX, + c: U32_MAX, + d: U64_MAX, + e: U256_MAX, + f: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6', + g: B512, + native: 'Pending', + mixed: { Value: true }, + grades: [1, 4, 6, 22], + fuel: 'fuel', + hello: 'Hello World', + opt: 42, + nada: undefined, + bytes: Uint8Array.from([40, 41, 42]), + tuple: [U8_MAX, U16_MAX, U32_MAX, 'fuel'], + vec_u8: [40, 41, 42], + deep: { + a: U8_MAX, + b: U16_MAX, + c: U32_MAX, + d: U64_MAX, + e: U256_MAX, + f: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6', + g: B512, + native: 'Pending', + mixed: { Value: true }, + grades: [1, 4, 6, 22], + fuel: 'fuel', + hello: 'Hello World', + opt: 42, + nada: undefined, + bytes: Uint8Array.from([40, 41, 42]), + tuple: [U8_MAX, U16_MAX, U32_MAX, 'fuel'], + vec_u8: [40, 41, 42], + }, + }; + + const predicate = new Predicate<[typeof struct]>({ + bytecode, + provider, + abi, + inputData: [struct], + }); + + const initialBalance = await receiver.getBalance(BaseAssetId); + expect(initialBalance).toStrictEqual(bn(0)); + + const amountToReceiver = 100; + + await fundPredicate(wallet, predicate, 100_000); + const tx = await predicate.transfer(receiver.address, amountToReceiver, BaseAssetId); + await tx.waitForResult(); + + const finalBalance = await receiver.getBalance(BaseAssetId); + expect(finalBalance).toStrictEqual(bn(amountToReceiver)); + }); +}); diff --git a/packages/fuel-gauge/src/experimental-script.test.ts b/packages/fuel-gauge/src/experimental-script.test.ts new file mode 100644 index 00000000000..bd0534ed9b6 --- /dev/null +++ b/packages/fuel-gauge/src/experimental-script.test.ts @@ -0,0 +1,122 @@ +import { readFileSync } from 'fs'; +import { Script, bn } from 'fuels'; +import { join } from 'path'; + +import { createWallet } from './utils'; + +let echoScript: Script; +let printScript: Script; +const U8_MAX = 255; +const U16_MAX = 65535; +const U32_MAX = 4294967295; +const U64_MAX = bn(2).pow(64).sub(1); +const U256_MAX = bn(2).pow(256).sub(1); +const B512 = + '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2'; + +const getScript = async (name: string) => { + const wallet = await createWallet(); + const path = join( + __dirname, + `../test/fixtures/forc-projects-experimental/${name}/out/release/${name}` + ); + const bytes = readFileSync(`${path}.bin`); + const abi = JSON.parse(readFileSync(`${path}-abi.json`, 'utf8')); + return new Script(bytes, abi, wallet); +}; + +beforeAll(async () => { + printScript = await getScript('script-print'); + echoScript = await getScript('script-echo'); +}); + +/** + * @group node + */ +describe('Experimental Script', () => { + it('prints mixed struct with all types', async () => { + const { value } = await printScript.functions.main(B512).call(); + expect(value).toStrictEqual({ + a: U8_MAX, + b: U16_MAX, + c: U32_MAX, + d: U64_MAX, + e: U256_MAX, + f: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6', + g: B512, + native: 'Pending', + mixed: { Value: true }, + grades: [1, 4, 6, 22], + fuel: 'fuel', + hello: 'Hello World', + opt: 42, + nada: undefined, + bytes: Uint8Array.from([40, 41, 42]), + tuple: [U8_MAX, U16_MAX, U32_MAX, 'fuel'], + vec_u8: [40, 41, 42], + deep: { + a: U8_MAX, + b: U16_MAX, + c: U32_MAX, + d: U64_MAX, + e: U256_MAX, + f: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6', + g: B512, + native: 'Pending', + mixed: { Value: true }, + grades: [1, 4, 6, 22], + fuel: 'fuel', + hello: 'Hello World', + opt: 42, + nada: undefined, + bytes: Uint8Array.from([40, 41, 42]), + tuple: [U8_MAX, U16_MAX, U32_MAX, 'fuel'], + vec_u8: [40, 41, 42], + }, + }); + }); + + it('echos number struct', async () => { + const struct = { + a: U8_MAX, + b: U16_MAX, + c: U32_MAX, + d: U64_MAX, + e: U256_MAX, + f: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6', + g: B512, + native: 'Pending', + mixed: { Value: true }, + grades: [1, 4, 6, 22], + fuel: 'fuel', + hello: 'Hello World', + opt: 42, + nada: undefined, + bytes: Uint8Array.from([40, 41, 42]), + tuple: [U8_MAX, U16_MAX, U32_MAX, 'fuel'], + vec_u8: [40, 41, 42], + deep: { + a: U8_MAX, + b: U16_MAX, + c: U32_MAX, + d: U64_MAX, + e: U256_MAX, + f: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6', + g: B512, + native: 'Pending', + mixed: { Value: true }, + grades: [1, 4, 6, 22], + fuel: 'fuel', + hello: 'Hello World', + opt: 42, + nada: undefined, + bytes: Uint8Array.from([40, 41, 42]), + tuple: [U8_MAX, U16_MAX, U32_MAX, 'fuel'], + vec_u8: [40, 41, 42], + }, + }; + + const { value } = await echoScript.functions.main(struct).call(); + expect(value).toStrictEqual(struct); + }); +}); diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/Forc.toml index 3d346cb2d65..dc76225a689 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/Forc.toml +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/Forc.toml @@ -1,2 +1,8 @@ [workspace] -members = ["logging"] +members = [ + "predicate-echo", + "script-echo", + "script-print", + "logging", + "contract-echo", +] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/contract-echo/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/contract-echo/Forc.toml new file mode 100644 index 00000000000..57b95ffcfef --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/contract-echo/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "contract-echo" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/contract-echo/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/contract-echo/src/main.sw new file mode 100644 index 00000000000..8d81efbf771 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/contract-echo/src/main.sw @@ -0,0 +1,82 @@ +contract; + +use std::b512::B512; +use std::string::String; +use std::option::Option; +use std::bytes::Bytes; +use std::vec::Vec; + +enum NativeEnum { + Checked: (), + Pending: (), +} + +enum MixedEnum { + Value: bool, + Data: u16, +} + +struct EvenDeeperStruct { + nested_vec: Vec, + nested_str: str, + nested_raw: raw_slice, +} + +struct DeeperStruct { + a: u8, + b: u16, + c: u32, + d: u64, + e: u256, + f: b256, + g: B512, + native: NativeEnum, + mixed: MixedEnum, + grades: [u8; 4], + fuel: str[4], + hello: String, + opt: Option, + nada: Option, + bytes: Bytes, + tuple: (u8, u16, u32, str[4]), + vec_u8: Vec, + even_deeper: EvenDeeperStruct, +} + +struct MixedStruct { + a: u8, + b: u16, + c: u32, + d: u64, + e: u256, + f: b256, + g: B512, + native: NativeEnum, + mixed: MixedEnum, + grades: [u8; 4], + fuel: str[4], + hello: String, + opt: Option, + nada: Option, + bytes: Bytes, + tuple: (u8, u16, u32, str[4]), + vec_u8: Vec, + deep: DeeperStruct, + str_slice: str, +} + +abi MyContract { + fn echo_struct(param: MixedStruct) -> MixedStruct; + fn test_revert() -> bool; +} + +impl MyContract for Contract { + fn echo_struct(param: MixedStruct) -> MixedStruct { + param + } + + fn test_revert() -> bool { + require(false, "This is a revert error"); + true + } +} diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/logging/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/logging/src/main.sw index 610ca6eb35d..0a2aa368788 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/logging/src/main.sw +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/logging/src/main.sw @@ -37,7 +37,8 @@ abi LoggingContract { fn log_b512(a: B512) -> B512; fn log_b256_b512(a: b256, b: B512) -> (b256, B512); fn log_vec_u8(a: Vec) -> Vec; - fn log_vec_b256(a: Vec) -> Vec; + fn log_vec_b256(a: Vec); + fn log_vec_b512(a: Vec); fn log_u16_vec_u8(a: u16, b: Vec) -> (u16, Vec); fn log_bytes(a: Bytes) -> Bytes; fn log_u16_bytes(a: u16, b: Bytes) -> (u16, Bytes); @@ -139,9 +140,12 @@ impl LoggingContract for Contract { a } - fn log_vec_b256(a: Vec) -> Vec { + fn log_vec_b256(a: Vec) { + log(a); + } + + fn log_vec_b512(a: Vec) { log(a); - a } fn log_bytes(a: Bytes) -> Bytes { diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/predicate-echo/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/predicate-echo/Forc.toml new file mode 100644 index 00000000000..a17df04822d --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/predicate-echo/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "predicate-echo" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/predicate-echo/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/predicate-echo/src/main.sw new file mode 100644 index 00000000000..2e13f3d402f --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/predicate-echo/src/main.sw @@ -0,0 +1,65 @@ +predicate; + +use std::b512::B512; +use std::string::String; +use std::option::Option; +use std::bytes::Bytes; +use std::vec::Vec; + +enum NativeEnum { + Checked: (), + Pending: (), +} + +enum MixedEnum { + Value: bool, + Data: u16, +} + +struct DeeperStruct { + a: u8, + b: u16, + c: u32, + d: u64, + e: u256, + f: b256, + g: B512, + native: NativeEnum, + mixed: MixedEnum, + grades: [u8; 4], + fuel: str[4], + hello: String, + opt: Option, + nada: Option, + bytes: Bytes, + tuple: (u8, u16, u32, str[4]), + vec_u8: Vec, +} + +struct MixedStruct { + a: u8, + b: u16, + c: u32, + d: u64, + e: u256, + f: b256, + g: B512, + native: NativeEnum, + mixed: MixedEnum, + grades: [u8; 4], + fuel: str[4], + hello: String, + opt: Option, + nada: Option, + bytes: Bytes, + tuple: (u8, u16, u32, str[4]), + vec_u8: Vec, + deep: DeeperStruct, +} + +fn main(param: MixedStruct) -> bool { + if (param.deep.d == 18446744073709551615) { + return true; + } + return false; +} diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-echo/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-echo/Forc.toml new file mode 100644 index 00000000000..34b6fef5427 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-echo/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "script-echo" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-echo/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-echo/src/main.sw new file mode 100644 index 00000000000..7a274d55355 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-echo/src/main.sw @@ -0,0 +1,62 @@ +script; + +use std::b512::B512; +use std::string::String; +use std::option::Option; +use std::bytes::Bytes; +use std::vec::Vec; + +enum NativeEnum { + Checked: (), + Pending: (), +} + +enum MixedEnum { + Value: bool, + Data: u16, +} + +struct DeeperStruct { + a: u8, + b: u16, + c: u32, + d: u64, + e: u256, + f: b256, + g: B512, + native: NativeEnum, + mixed: MixedEnum, + grades: [u8; 4], + fuel: str[4], + hello: String, + opt: Option, + nada: Option, + bytes: Bytes, + tuple: (u8, u16, u32, str[4]), + vec_u8: Vec, +} + +struct MixedStruct { + a: u8, + b: u16, + c: u32, + d: u64, + e: u256, + f: b256, + g: B512, + native: NativeEnum, + mixed: MixedEnum, + grades: [u8; 4], + fuel: str[4], + hello: String, + opt: Option, + nada: Option, + bytes: Bytes, + tuple: (u8, u16, u32, str[4]), + vec_u8: Vec, + deep: DeeperStruct, +} + +fn main(param: MixedStruct) -> MixedStruct { + param +} diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-print/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-print/Forc.toml new file mode 100644 index 00000000000..647a1bd0549 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-print/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "script-print" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-print/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-print/src/main.sw new file mode 100644 index 00000000000..9b97833f77f --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects-experimental/script-print/src/main.sw @@ -0,0 +1,112 @@ +script; + +use std::b512::B512; +use std::string::String; +use std::option::Option; +use std::bytes::Bytes; +use std::vec::Vec; + +enum NativeEnum { + Checked: (), + Pending: (), +} + +enum MixedEnum { + Value: bool, + Data: u16, +} + +struct DeeperStruct { + a: u8, + b: u16, + c: u32, + d: u64, + e: u256, + f: b256, + g: B512, + native: NativeEnum, + mixed: MixedEnum, + grades: [u8; 4], + fuel: str[4], + hello: String, + opt: Option, + nada: Option, + bytes: Bytes, + tuple: (u8, u16, u32, str[4]), + vec_u8: Vec, +} + +struct MixedStruct { + a: u8, + b: u16, + c: u32, + d: u64, + e: u256, + f: b256, + g: B512, + native: NativeEnum, + mixed: MixedEnum, + grades: [u8; 4], + fuel: str[4], + hello: String, + opt: Option, + nada: Option, + bytes: Bytes, + tuple: (u8, u16, u32, str[4]), + vec_u8: Vec, + deep: DeeperStruct, +} + +fn main(param_one: B512) -> MixedStruct { + let mut my_bytes = Bytes::new(); + + my_bytes.push(40u8); + my_bytes.push(41u8); + my_bytes.push(42u8); + + let mut my_vec_u8 = Vec::new(); + my_vec_u8.push(40u8); + my_vec_u8.push(41u8); + my_vec_u8.push(42u8); + + let my_struct = MixedStruct { + a: 255, + b: 65535, + c: 4294967295, + d: 18446744073709551615, + e: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFu256, + f: 0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6, + g: param_one, + native: NativeEnum::Pending, + mixed: MixedEnum::Value(true), + grades: [1, 4, 6, 22], + fuel: __to_str_array("fuel"), + hello: String::from_ascii_str("Hello World"), + opt: Option::Some(42), + nada: Option::None, + bytes: my_bytes, + tuple: (255, 65535, 4294967295, __to_str_array("fuel")), + vec_u8: my_vec_u8, + deep: DeeperStruct { + a: 255, + b: 65535, + c: 4294967295, + d: 18446744073709551615, + e: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFu256, + f: 0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6, + g: param_one, + native: NativeEnum::Pending, + mixed: MixedEnum::Value(true), + grades: [1, 4, 6, 22], + fuel: __to_str_array("fuel"), + hello: String::from_ascii_str("Hello World"), + opt: Option::Some(42), + nada: Option::None, + bytes: my_bytes, + tuple: (255, 65535, 4294967295, __to_str_array("fuel")), + vec_u8: my_vec_u8, + }, + }; + + my_struct +} diff --git a/packages/program/src/contract-call-script.ts b/packages/program/src/contract-call-script.ts index c290edd5e7a..f35df0311fc 100644 --- a/packages/program/src/contract-call-script.ts +++ b/packages/program/src/contract-call-script.ts @@ -1,5 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { WORD_SIZE, B256Coder, ASSET_ID_LEN, BigNumberCoder } from '@fuel-ts/abi-coder'; +import { + WORD_SIZE, + B256Coder, + ASSET_ID_LEN, + BigNumberCoder, + CONTRACT_ID_LEN, + ENCODING_V1, +} from '@fuel-ts/abi-coder'; import type { CallResult, TransactionResultCallReceipt, @@ -37,6 +44,11 @@ type CallOutputInfo = { encodedLength: number; }; +type ContractCallScriptFn = ( + call: ContractCall, + segmentOffset: number +) => { scriptData: Uint8Array[]; callParamOffsets: CallOpcodeParamsOffset }; + const DEFAULT_OPCODE_PARAMS: CallOpcodeParamsOffset = { assetIdOffset: 0, amountOffset: 0, @@ -201,6 +213,124 @@ const getFunctionOutputInfos = (functionScopes: InvocationScopeLike[]): CallOutp }; }); +/** + * Obtains script data for a contract call according to the V0 specification. + * + * @param call - the contract call to obtain for script data for. + * @param segmentOffset - the segment to generate pointers and offset data from. + * @returns the populated script data and call parameter offsets. + */ +export const getScriptDataV0: ContractCallScriptFn = ( + call: ContractCall, + segmentOffset: number +): { scriptData: Uint8Array[]; callParamOffsets: CallOpcodeParamsOffset } => { + const scriptData: Uint8Array[] = []; + let gasForwardedSize: number = 0; + + const callParamOffsets: CallOpcodeParamsOffset = { + amountOffset: segmentOffset, + assetIdOffset: segmentOffset + WORD_SIZE, + gasForwardedOffset: call.gas ? segmentOffset + WORD_SIZE + ASSET_ID_LEN : 0, + callDataOffset: segmentOffset + WORD_SIZE + ASSET_ID_LEN + gasForwardedSize, + }; + + // 1. Amount + scriptData.push(new BigNumberCoder('u64').encode(call.amount || 0)); + // 2. Asset ID + scriptData.push(new B256Coder().encode(call.assetId?.toString() || BaseAssetId)); + // 3. Contract ID + scriptData.push(call.contractId.toBytes()); + // 4. Function selector + scriptData.push(new BigNumberCoder('u64').encode(call.fnSelector)); + // 5. Gas to be forwarded + if (call.gas) { + scriptData.push(new BigNumberCoder('u64').encode(call.gas)); + + gasForwardedSize = WORD_SIZE; + } + + // 6. Calldata offset + if (call.isInputDataPointer) { + const pointerInputOffset = segmentOffset + POINTER_DATA_OFFSET + gasForwardedSize; + scriptData.push(new BigNumberCoder('u64').encode(pointerInputOffset)); + } + + // 7. Encoded arguments (optional) (variable length) + const args = arrayify(call.data); + scriptData.push(args); + + return { + scriptData, + callParamOffsets, + }; +}; + +/** + * Obtains script data for a contract call according to the V1 specification. + * + * @param call - the contract call to obtain for script data for. + * @param segmentOffset - the segment to generate pointers and offset data from. + * @returns the populated script data and call parameter offsets. + */ +export const getScriptDataV1: ContractCallScriptFn = ( + call: ContractCall, + segmentOffset: number +): { scriptData: Uint8Array[]; callParamOffsets: CallOpcodeParamsOffset } => { + const scriptData: Uint8Array[] = []; + const callSegmentOffset = segmentOffset + WORD_SIZE; + let gasForwardedSize: number = 0; + + // 1. Amount + scriptData.push(new BigNumberCoder('u64').encode(call.amount || 0)); + // 2. Asset ID + scriptData.push(new B256Coder().encode(call.assetId?.toString() || BaseAssetId)); + // 3. Gas to be forwarded + if (call.gas) { + scriptData.push(new BigNumberCoder('u64').encode(call.gas)); + gasForwardedSize = WORD_SIZE; + } + + const callParamOffsets: CallOpcodeParamsOffset = { + amountOffset: callSegmentOffset, + assetIdOffset: callSegmentOffset + WORD_SIZE, + gasForwardedOffset: callSegmentOffset + WORD_SIZE + ASSET_ID_LEN, + callDataOffset: callSegmentOffset + WORD_SIZE + ASSET_ID_LEN + gasForwardedSize, + }; + const encodedSelectorOffset = + callParamOffsets.callDataOffset + CONTRACT_ID_LEN + WORD_SIZE + WORD_SIZE; + const customInputOffset = encodedSelectorOffset + call.fnSelectorBytes.length; + const bytes = arrayify(call.data); + + // 4. Contract ID + scriptData.push(call.contractId.toBytes()); + // 5. Function selector offset + scriptData.push(new BigNumberCoder('u64').encode(encodedSelectorOffset)); + // 6. CallData offset + scriptData.push(new BigNumberCoder('u64').encode(customInputOffset)); + // 7. Function selector + scriptData.push(call.fnSelectorBytes); + // 8. Encoded arguments + scriptData.push(bytes); + + return { + scriptData, + callParamOffsets, + }; +}; + +/** + * Retrieves a script data function for a specific encoding version. + * + * @param encoding - the encoding version used for the contract call. + * @returns an appropriate script data function. + */ +export const getScriptDataForEncoding = (encoding?: string): ContractCallScriptFn => { + if (encoding === ENCODING_V1) { + return getScriptDataV1; + } + return getScriptDataV0; +}; + export const getContractCallScript = ( functionScopes: InvocationScopeLike[], maxInputs: BN @@ -213,78 +343,43 @@ export const getContractCallScript = ( ), (contractCalls): EncodedScriptCall => { const TOTAL_CALLS = contractCalls.length; + if (TOTAL_CALLS === 0) { return { data: new Uint8Array(), script: new Uint8Array() }; } - // Calculate instructions length for call instructions + // Get total data offset AFTER all scripts const callInstructionsLength = getCallInstructionsLength(contractCalls); - // pad length + // Pad length const paddingLength = (8 - (callInstructionsLength % 8)) % 8; const paddedInstructionsLength = callInstructionsLength + paddingLength; - // get total data offset AFTER all scripts + // Base offset const dataOffset = calculateScriptDataBaseOffset(maxInputs.toNumber()) + paddedInstructionsLength; // The data for each call is ordered into segments const paramOffsets: CallOpcodeParamsOffset[] = []; - let segmentOffset = dataOffset; - // the data about the contract output const outputInfos: CallOutputInfo[] = []; + let segmentOffset = dataOffset; const scriptData: Uint8Array[] = []; for (let i = 0; i < TOTAL_CALLS; i += 1) { const call = contractCalls[i]; + const { scriptData: callScriptData, callParamOffsets } = getScriptDataForEncoding( + call.encoding + )(call, segmentOffset); + // store output and param offsets for asm instructions later outputInfos.push({ isHeap: call.isOutputDataHeap, encodedLength: call.outputEncodedLength, }); - - let gasForwardedSize = 0; - - paramOffsets.push({ - amountOffset: segmentOffset, - assetIdOffset: segmentOffset + WORD_SIZE, - gasForwardedOffset: call.gas ? segmentOffset + WORD_SIZE + ASSET_ID_LEN : 0, - callDataOffset: segmentOffset + WORD_SIZE + ASSET_ID_LEN + gasForwardedSize, - }); - - /// script data, consisting of the following items in the given order: - /// 1. Amount to be forwarded `(1 * `[`WORD_SIZE`]`)` - scriptData.push(new BigNumberCoder('u64').encode(call.amount || 0)); - /// 2. Asset ID to be forwarded ([`AssetId::LEN`]) - scriptData.push(new B256Coder().encode(call.assetId?.toString() || BaseAssetId)); - /// 3. Contract ID ([`ContractId::LEN`]); - scriptData.push(call.contractId.toBytes()); - /// 4. Function selector `(1 * `[`WORD_SIZE`]`)` - scriptData.push(new BigNumberCoder('u64').encode(call.fnSelector)); - /// 5. Gas to be forwarded `(1 * `[`WORD_SIZE`]`)` - if (call.gas) { - scriptData.push(new BigNumberCoder('u64').encode(call.gas)); - - gasForwardedSize = WORD_SIZE; - } - - /// 6. Calldata offset (optional) `(1 * `[`WORD_SIZE`]`)` - // If the method call takes custom inputs or has more than - // one argument, we need to calculate the `call_data_offset`, - // which points to where the data for the custom types start in the - // transaction. If it doesn't take any custom inputs, this isn't necessary. - if (call.isInputDataPointer) { - const pointerInputOffset = segmentOffset + POINTER_DATA_OFFSET + gasForwardedSize; - scriptData.push(new BigNumberCoder('u64').encode(pointerInputOffset)); - } - - /// 7. Encoded arguments (optional) (variable length) - const args = arrayify(call.data); - scriptData.push(args); - - // move offset for next call + scriptData.push(concat(callScriptData)); + paramOffsets.push(callParamOffsets); segmentOffset = dataOffset + concat(scriptData).byteLength; } diff --git a/packages/program/src/functions/base-invocation-scope.ts b/packages/program/src/functions/base-invocation-scope.ts index f3184996513..57d06e3f243 100644 --- a/packages/program/src/functions/base-invocation-scope.ts +++ b/packages/program/src/functions/base-invocation-scope.ts @@ -39,6 +39,8 @@ function createContractCall(funcScope: InvocationScopeLike, offset: number): Con return { contractId: (program as AbstractContract).id, fnSelector: func.selector, + fnSelectorBytes: func.selectorBytes, + encoding: func.encoding, data, isInputDataPointer: func.isInputDataPointer, isOutputDataHeap: func.outputMetadata.isHeapType, diff --git a/packages/program/src/types.ts b/packages/program/src/types.ts index 72d376dcace..d010f896644 100644 --- a/packages/program/src/types.ts +++ b/packages/program/src/types.ts @@ -1,4 +1,4 @@ -import type { FunctionFragment, JsonAbi } from '@fuel-ts/abi-coder'; +import type { FunctionFragment, JsonAbi, EncodingVersion } from '@fuel-ts/abi-coder'; import type { CoinQuantity, CoinQuantityLike } from '@fuel-ts/account'; import type { AbstractProgram, AbstractAddress, BytesLike } from '@fuel-ts/interfaces'; import type { BigNumberish } from '@fuel-ts/math'; @@ -12,6 +12,8 @@ export type ContractCall = { contractId: AbstractAddress; data: BytesLike; fnSelector: string; + fnSelectorBytes: Uint8Array; + encoding?: EncodingVersion; isInputDataPointer: boolean; isOutputDataHeap: boolean; outputEncodedLength: number;