diff --git a/.changeset/shiny-ligers-eat.md b/.changeset/shiny-ligers-eat.md new file mode 100644 index 00000000000..2b234d24e76 --- /dev/null +++ b/.changeset/shiny-ligers-eat.md @@ -0,0 +1,7 @@ +--- +"@fuel-ts/abi-coder": patch +"@fuel-ts/contract": patch +"@fuel-ts/providers": patch +--- + +Added vec support diff --git a/packages/abi-coder/src/abi-coder.test.ts b/packages/abi-coder/src/abi-coder.test.ts index c6d3fc40203..0f44e9cb439 100644 --- a/packages/abi-coder/src/abi-coder.test.ts +++ b/packages/abi-coder/src/abi-coder.test.ts @@ -1,4 +1,4 @@ -import { hexlify } from '@ethersproject/bytes'; +import { hexlify, concat } from '@ethersproject/bytes'; import { bn, toHex } from '@fuel-ts/math'; import AbiCoder from './abi-coder'; @@ -165,4 +165,188 @@ describe('AbiCoder', () => { ]) ); }); + + it('encodes vectors', () => { + const types = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'u64', + isParamType: true, + }, + { + name: 'cap', + type: 'u64', + isParamType: true, + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + isParamType: true, + }, + ], + isParamType: true, + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + isParamType: true, + }, + ], + isParamType: true, + }, + ]; + + const input = [36]; + const encoded = abiCoder.encode(types, [input]); + + const pointer = [0, 0, 0, 0, 0, 0, 0, 24]; + const capacity = [0, 0, 0, 0, 0, 0, 0, input.length]; + const length = [0, 0, 0, 0, 0, 0, 0, input.length]; + const data = [0, 0, 0, 0, 0, 0, 0, input[0]]; + const vecData = concat([pointer, capacity, length, data]); + + const expected = hexlify(vecData); + + expect(hexlify(encoded)).toBe(expected); + }); + + it('encodes vectors with multiple items', () => { + const types = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'u64', + isParamType: true, + }, + { + name: 'cap', + type: 'u64', + isParamType: true, + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + isParamType: true, + }, + ], + isParamType: true, + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + isParamType: true, + }, + ], + isParamType: true, + }, + ]; + + const input = [36, 42, 57]; + const encoded = abiCoder.encode(types, [input]); + + const pointer = [0, 0, 0, 0, 0, 0, 0, 24]; + const capacity = [0, 0, 0, 0, 0, 0, 0, input.length]; + const length = [0, 0, 0, 0, 0, 0, 0, input.length]; + const data1 = [0, 0, 0, 0, 0, 0, 0, input[0]]; + const data2 = [0, 0, 0, 0, 0, 0, 0, input[1]]; + const data3 = [0, 0, 0, 0, 0, 0, 0, input[2]]; + const vecData = concat([pointer, capacity, length, data1, data2, data3]); + + const expected = hexlify(vecData); + + expect(hexlify(encoded)).toBe(expected); + }); + + it('encodes vectors with multiple items [with offset]', () => { + const types = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'u64', + isParamType: true, + }, + { + name: 'cap', + type: 'u64', + isParamType: true, + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + isParamType: true, + }, + ], + isParamType: true, + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + isParamType: true, + }, + ], + isParamType: true, + }, + ]; + + const input = [36, 42, 57]; + const encoded = abiCoder.encode(types, [input], 14440); + + const pointer = [0, 0, 0, 0, 0, 0, 56, 128]; + const capacity = [0, 0, 0, 0, 0, 0, 0, input.length]; + const length = [0, 0, 0, 0, 0, 0, 0, input.length]; + const data1 = [0, 0, 0, 0, 0, 0, 0, input[0]]; + const data2 = [0, 0, 0, 0, 0, 0, 0, input[1]]; + const data3 = [0, 0, 0, 0, 0, 0, 0, input[2]]; + const vecData = concat([pointer, capacity, length, data1, data2, data3]); + + const expected = hexlify(vecData); + + expect(hexlify(encoded)).toBe(expected); + }); }); diff --git a/packages/abi-coder/src/abi-coder.ts b/packages/abi-coder/src/abi-coder.ts index 982e7df7147..36bdb3767f0 100644 --- a/packages/abi-coder/src/abi-coder.ts +++ b/packages/abi-coder/src/abi-coder.ts @@ -1,6 +1,6 @@ // See: https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI import type { BytesLike } from '@ethersproject/bytes'; -import { arrayify } from '@ethersproject/bytes'; +import { concat, arrayify } from '@ethersproject/bytes'; import { Logger } from '@ethersproject/logger'; import type { DecodedValue, InputValue } from './coders/abstract-coder'; @@ -16,6 +16,7 @@ import StringCoder from './coders/string'; import StructCoder from './coders/struct'; import TupleCoder from './coders/tuple'; import U64Coder from './coders/u64'; +import VecCoder from './coders/vec'; import { arrayRegEx, enumRegEx, @@ -23,9 +24,10 @@ import { structRegEx, tupleRegEx, OPTION_CODER_TYPE, + VEC_CODER_TYPE, } from './constants'; import type { JsonAbiFragmentType } from './json-abi'; -import { filterEmptyParams, hasOptionTypes } from './utilities'; +import { filterEmptyParams, getVectorAdjustments, hasOptionTypes } from './utilities'; const logger = new Logger(process.env.BUILD_VERSION || '~'); @@ -69,6 +71,15 @@ export default class AbiCoder { return new StringCoder(length); } + if (param.type === VEC_CODER_TYPE && Array.isArray(param.typeArguments)) { + const typeArgument = param.typeArguments[0]; + if (!typeArgument) { + throw new Error('Expected Vec type to have a type argument'); + } + const itemCoder = this.getCoder(typeArgument); + return new VecCoder(itemCoder); + } + const structMatch = structRegEx.exec(param.type)?.groups; if (structMatch && Array.isArray(param.components)) { const coders = param.components.reduce((obj, component) => { @@ -103,7 +114,7 @@ export default class AbiCoder { return logger.throwArgumentError('Invalid type', 'type', param.type); } - encode(types: ReadonlyArray, values: InputValue[]): Uint8Array { + encode(types: ReadonlyArray, values: InputValue[], offset = 0): Uint8Array { const nonEmptyTypes = filterEmptyParams(types); const shallowCopyValues = values.slice(); @@ -120,8 +131,12 @@ export default class AbiCoder { } const coders = nonEmptyTypes.map((type) => this.getCoder(type)); + const vectorData = getVectorAdjustments(coders, shallowCopyValues, offset); + const coder = new TupleCoder(coders); - return coder.encode(shallowCopyValues); + const results = coder.encode(shallowCopyValues); + + return concat([results, concat(vectorData)]); } decode(types: ReadonlyArray, data: BytesLike): DecodedValue[] | undefined { diff --git a/packages/abi-coder/src/coders/abstract-coder.ts b/packages/abi-coder/src/coders/abstract-coder.ts index d1059737f91..fab97e221af 100644 --- a/packages/abi-coder/src/coders/abstract-coder.ts +++ b/packages/abi-coder/src/coders/abstract-coder.ts @@ -34,6 +34,7 @@ export default abstract class Coder { readonly name: string; readonly type: string; readonly encodedLength: number; + offset?: number; constructor(name: string, type: string, encodedLength: number) { this.name = name; @@ -48,6 +49,10 @@ export default abstract class Coder { throw new Error('unreachable'); } + setOffset(offset: number): void { + this.offset = offset; + } + abstract encode(value: TInput, length?: number): Uint8Array; abstract decode(data: Uint8Array, offset: number, length?: number): [TDecoded, number]; diff --git a/packages/abi-coder/src/coders/vec.ts b/packages/abi-coder/src/coders/vec.ts new file mode 100644 index 00000000000..569ff5bb324 --- /dev/null +++ b/packages/abi-coder/src/coders/vec.ts @@ -0,0 +1,59 @@ +import { concat } from '@ethersproject/bytes'; + +import { WORD_SIZE } from '../constants'; + +import type { TypesOfCoder } from './abstract-coder'; +import Coder from './abstract-coder'; +import U64Coder from './u64'; + +const VEC_PROPERTY_SPACE = 3; // ptr + cap + length + +type InputValueOf = Array['Input']>; +type DecodedValueOf = Array['Decoded']>; + +export default class VecCoder extends Coder< + InputValueOf, + DecodedValueOf +> { + coder: TCoder; + + constructor(coder: TCoder) { + super('struct', `struct Vec`, 0); + this.coder = coder; + } + + static getBaseOffset(): number { + return VEC_PROPERTY_SPACE * WORD_SIZE; + } + + getEncodedVectorData(value: InputValueOf): Uint8Array { + if (!Array.isArray(value)) { + this.throwError('expected array value', value); + } + + const encodedValues = Array.from(value).map((v) => this.coder.encode(v)); + return concat(encodedValues); + } + + encode(value: InputValueOf): Uint8Array { + if (!Array.isArray(value)) { + this.throwError('expected array value', value); + } + + const parts: Uint8Array[] = []; + // pointer (ptr) + const pointer = this.offset || 0; + parts.push(new U64Coder().encode(pointer)); + // capacity (cap) + parts.push(new U64Coder().encode(value.length)); + // length (len) + parts.push(new U64Coder().encode(value.length)); + + return concat(parts); + } + + decode(data: Uint8Array, offset: number): [DecodedValueOf, number] { + this.throwError('unexpected Vec decode', 'not implemented'); + return [undefined as unknown as DecodedValueOf, offset]; + } +} diff --git a/packages/abi-coder/src/constants.ts b/packages/abi-coder/src/constants.ts index 70aa0187418..eb02cac4d90 100644 --- a/packages/abi-coder/src/constants.ts +++ b/packages/abi-coder/src/constants.ts @@ -1,7 +1,34 @@ export const OPTION_CODER_TYPE = 'enum Option'; +export const VEC_CODER_TYPE = 'struct Vec'; export const stringRegEx = /str\[(?[0-9]+)\]/; export const arrayRegEx = /\[(?[\w\s\\[\]]+);\s*(?[0-9]+)\]/; export const structRegEx = /^struct (?\w+)$/; export const enumRegEx = /^enum (?\w+)$/; export const tupleRegEx = /^\((?.*)\)$/; export const genericRegEx = /^generic (?\w+)$/; + +export const WORD_SIZE = 8; +export const BYTES_32 = 32; +export const MAX_INPUTS = 255; +export const ASSET_ID_LEN = BYTES_32; +export const CONTRACT_ID_LEN = BYTES_32; + +// VM_TX_MEMORY = 10240 +export const VM_TX_MEMORY = + BYTES_32 + // Tx ID + WORD_SIZE + // Tx size + // Asset ID/Balance coin input pairs + MAX_INPUTS * (ASSET_ID_LEN + WORD_SIZE); + +// TRANSACTION_SCRIPT_FIXED_SIZE = 112 +export const TRANSACTION_SCRIPT_FIXED_SIZE = + WORD_SIZE + // Identifier + WORD_SIZE + // Gas price + WORD_SIZE + // Gas limit + WORD_SIZE + // Maturity + WORD_SIZE + // Script size + WORD_SIZE + // Script data size + WORD_SIZE + // Inputs size + WORD_SIZE + // Outputs size + WORD_SIZE + // Witnesses size + BYTES_32; // Receipts root diff --git a/packages/abi-coder/src/index.ts b/packages/abi-coder/src/index.ts index 04056deefae..49b3544d981 100644 --- a/packages/abi-coder/src/index.ts +++ b/packages/abi-coder/src/index.ts @@ -10,9 +10,11 @@ export { default as StringCoder } from './coders/string'; export { default as StructCoder } from './coders/struct'; export { default as TupleCoder } from './coders/tuple'; export { default as U64Coder } from './coders/u64'; +export { default as VecCoder } from './coders/vec'; export * from './utilities'; export * from './fragments/fragment'; export { default as FunctionFragment } from './fragments/function-fragment'; export { default as Interface } from './interface'; export { default as AbiCoder } from './abi-coder'; export * from './json-abi'; +export * from './constants'; diff --git a/packages/abi-coder/src/interface.ts b/packages/abi-coder/src/interface.ts index 1c1397bf705..84e7e17a028 100644 --- a/packages/abi-coder/src/interface.ts +++ b/packages/abi-coder/src/interface.ts @@ -105,7 +105,8 @@ export default class Interface { encodeFunctionData( functionFragment: FunctionFragment | string, - values: Array + values: Array, + offset = 0 ): Uint8Array { const fragment = typeof functionFragment === 'string' ? this.getFunction(functionFragment) : functionFragment; @@ -122,7 +123,7 @@ export default class Interface { } const isRef = inputs.length > 1 || isReferenceType(inputs[0].type); - const args = this.abiCoder.encode(inputs, values); + const args = this.abiCoder.encode(inputs, values, offset); return concat([selector, new BooleanCoder().encode(isRef), args]); } diff --git a/packages/abi-coder/src/utilities.ts b/packages/abi-coder/src/utilities.ts index f29fcd8e7e8..8896b6721e8 100644 --- a/packages/abi-coder/src/utilities.ts +++ b/packages/abi-coder/src/utilities.ts @@ -1,3 +1,6 @@ +import type { InputValue } from './coders/abstract-coder'; +import type Coder from './coders/abstract-coder'; +import VecCoder from './coders/vec'; import { OPTION_CODER_TYPE } from './constants'; import type { ParamType } from './fragments/param-type'; @@ -10,3 +13,48 @@ export function hasOptionTypes(types: T): T; export function hasOptionTypes(types: ReadonlyArray) { return types.some((t) => (t as Readonly)?.type === OPTION_CODER_TYPE); } + +type ByteInfo = { vecByteLength: number } | { byteLength: number }; +export function getVectorAdjustments( + coders: Coder[], + values: InputValue[], + offset = 0 +) { + const vectorData: Uint8Array[] = []; + const byteMap: ByteInfo[] = coders.map((encoder, i) => { + if (!(encoder instanceof VecCoder)) { + return { byteLength: encoder.encodedLength }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = encoder.getEncodedVectorData(values[i] as any); + vectorData.push(data); + return { vecByteLength: data.byteLength }; + }); + + const baseVectorOffset = vectorData.length * VecCoder.getBaseOffset() + offset; + const offsetMap = coders.map((encoder, paramIndex) => { + if (!(encoder instanceof VecCoder)) { + return 0; + } + + return byteMap.reduce((sum, byteInfo, byteIndex) => { + if ('byteLength' in byteInfo) { + return sum + byteInfo.byteLength; + } + + if (byteIndex === 0 && byteIndex === paramIndex) { + return baseVectorOffset; + } + + if (byteIndex < paramIndex) { + return sum + byteInfo.vecByteLength + baseVectorOffset; + } + + return sum; + }, 0); + }); + + coders.forEach((code, i) => code.setOffset(offsetMap[i])); + return vectorData; +} diff --git a/packages/contract/src/__test__/contract-factory.test.ts b/packages/contract/src/__test__/contract-factory.test.ts index 8c17c58d9cb..832d54418ff 100644 --- a/packages/contract/src/__test__/contract-factory.test.ts +++ b/packages/contract/src/__test__/contract-factory.test.ts @@ -80,6 +80,7 @@ describe('Contract Factory', () => { contract: expect.objectContaining({ id: contact.id }), func: expect.objectContaining({ name: 'increment_counter' }), args: [1], + bytesOffset: 720, callParameters: undefined, txParameters: undefined, forward: undefined, diff --git a/packages/contract/src/__test__/coverage-contract/coverage-contract.test.ts b/packages/contract/src/__test__/coverage-contract/coverage-contract.test.ts index cf3c14a55d3..c536ce45bed 100644 --- a/packages/contract/src/__test__/coverage-contract/coverage-contract.test.ts +++ b/packages/contract/src/__test__/coverage-contract/coverage-contract.test.ts @@ -1,7 +1,7 @@ import { NativeAssetId } from '@fuel-ts/constants'; import type { BN } from '@fuel-ts/math'; import { bn, toHex } from '@fuel-ts/math'; -import { Provider } from '@fuel-ts/providers'; +import { Provider, LogReader } from '@fuel-ts/providers'; import { TestUtils } from '@fuel-ts/wallet'; import { readFileSync } from 'fs'; import { join } from 'path'; @@ -66,6 +66,11 @@ describe('Coverage Contract', () => { expect(value).toBe(3); }); + it('should test u8 variable type multiple params', async () => { + const { value } = await contractInstance.functions.echo_u8_addition(3, 4, 3).call(); + expect(value).toBe(10); + }); + it('should test u16 variable type', async () => { const { value } = await contractInstance.functions.echo_u16(RUST_U8_MAX + 1).call(); expect(value).toBe(RUST_U8_MAX + 1); @@ -222,4 +227,120 @@ describe('Coverage Contract', () => { const { value: Some } = await contractInstance.functions.echo_option_three_u8(INPUT).call(); expect(Some).toStrictEqual(1); }); + + it('should test u8 empty vector input', async () => { + const { value } = await contractInstance.functions.check_u8_vector([]).call(); + expect(value).toBeFalsy(); + }); + + it('should test u8 vector input', async () => { + const { value, transactionResult } = await contractInstance.functions + .check_u8_vector([1, 2, 3, 4, 5]) + .call(); + expect(value).toBeTruthy(); + const logReader = new LogReader(transactionResult.receipts); + expect(logReader.toArray()).toStrictEqual([ + 'vector.buf.ptr', + '14464', + 'vector.buf.cap', + '5', + 'vector.len', + '5', + 'addr_of vector', + '14440', + ]); + }); + + it('should echo u8 vector input', async () => { + const { value } = await contractInstance.functions + .echo_u8_vector_first([23, 6, 1, 51, 2]) + .call(); + + expect(value).toBe(23); + }); + + it('should echo a vector of optional u8 input', async () => { + const { value } = await contractInstance.functions.echo_u8_option_vector_first([28]).call(); + + expect(value).toBe(28); + }); + + it('should echo u64 vector input', async () => { + const INPUT = bn(54).toHex(); + const { value } = await contractInstance.functions + .echo_u64_vector_last([200, 100, 24, 51, 23, INPUT]) + .call(); + expect(value.toHex()).toBe(INPUT); + }); + + it('should echo u32 vector addition of mixed params', async () => { + const { value } = await contractInstance.functions + .echo_u32_vector_addition_other_type([100, 2], 47) + .call(); + expect(value).toBe(147); + }); + + it('should echo u32 vector addition', async () => { + const { value } = await contractInstance.functions + .echo_u32_vector_addition([100, 2], [24, 54]) + .call(); + expect(value).toBe(124); + }); + + it('should echo u32 vector addition [variable lengths]', async () => { + const { value } = await contractInstance.functions + .echo_u32_vector_addition([100, 2, 1, 2, 3], [24, 54]) + .call(); + expect(value).toBe(124); + }); + + it('should echo struct vector input', async () => { + const first = { + foo: 1, + bar: 10, + }; + const { value } = await contractInstance.functions + .echo_struct_vector_first([ + first, + { + foo: 2, + bar: 20, + }, + { + foo: 3, + bar: 30, + }, + ]) + .call(); + expect(value).toStrictEqual(first); + }); + + it('should echo complex struct vector input', async () => { + const last = { + foo: 3, + bar: bn(31337).toHex(), + baz: 'abcdefghi', + }; + const { value } = await contractInstance.functions + .echo_struct_vector_last([ + { + foo: 1, + bar: 11337n, + baz: '123456789', + }, + { + foo: 2, + bar: 21337n, + baz: 'alphabet!', + }, + last, + ]) + .call(); + const unhexed = { + foo: value.foo, + bar: bn(value.bar).toHex(), + baz: value.baz, + }; + expect(unhexed).toStrictEqual(last); + }); }); diff --git a/packages/contract/src/__test__/coverage-contract/src/main.sw b/packages/contract/src/__test__/coverage-contract/src/main.sw index 4740cc665ca..c367e3ea072 100644 --- a/packages/contract/src/__test__/coverage-contract/src/main.sw +++ b/packages/contract/src/__test__/coverage-contract/src/main.sw @@ -4,7 +4,11 @@ use std::*; use core::*; use std::storage::*; use std::contract_id::ContractId; +use std::vec::Vec; use std::option::Option; +use std::assert::assert; +use std::logging::log; +use std::mem::addr_of; pub struct U8Struct { i: u8, @@ -23,6 +27,12 @@ pub struct BigStruct { bar: u8, } +pub struct ComplexStruct { + foo: u8, + bar: u64, + baz: str[9], +} + pub enum SmallEnum { Empty: (), } @@ -44,7 +54,9 @@ abi CoverageContract { fn get_contract_id() -> ContractId; fn get_some_option_u8() -> Option; fn get_none_option_u8() -> Option; + fn check_u8_vector(vector: Vec) -> bool; fn echo_u8(input: u8) -> u8; + fn echo_u8_addition(input_a: u8, input_b: u8, input_c: u8) -> u8; fn echo_u16(input: u16) -> u16; fn echo_u32(input: u32) -> u32; fn echo_u64(input: u64) -> u64; @@ -68,6 +80,14 @@ abi CoverageContract { fn echo_option_u8(input: Option) -> Option; fn echo_option_extract_u32(input: Option) -> u32; fn echo_option_three_u8(inputA: Option, inputB: Option, inputC: Option) -> u8; + fn echo_u8_vector(input: Vec) -> Vec; + fn echo_u8_vector_first(vector: Vec) -> u8; + fn echo_u8_option_vector_first(vector: Vec>) -> u8; + fn echo_u64_vector_last(vector: Vec) -> u64; + fn echo_u32_vector_addition_other_type(vector: Vec, input: u32) -> u32; + fn echo_u32_vector_addition(vector_1: Vec, vector_2: Vec) -> u32; + fn echo_struct_vector_first(vector: Vec) -> BigStruct; + fn echo_struct_vector_last(vector: Vec) -> ComplexStruct; } impl CoverageContract for Contract { @@ -122,9 +142,34 @@ impl CoverageContract for Contract { o } - fn echo_u8(input: u8) -> u8 { + fn check_u8_vector(vector: Vec) -> bool { + match vector.len() { + 0 => false, + length => { + assert(length == 5); + assert(vector.capacity() == 5); + assert(vector.is_empty() == false); + log("vector.buf.ptr"); + log(vector.buf.ptr); + log("vector.buf.cap"); + log(vector.buf.cap); + log("vector.len"); + log(vector.len); + log("addr_of vector"); + log(addr_of(vector)); + true + }, + } + } + + fn echo_u8(input: u8) -> u8 { input } + + fn echo_u8_addition(input_a: u8, input_b: u8, input_c: u8) -> u8 { + input_a + input_b + input_c + } + fn echo_u16(input: u16) -> u16 { input } @@ -210,4 +255,50 @@ impl CoverageContract for Contract { value1 + value2 + value3 } + fn echo_u8_vector(input: Vec) -> Vec { + input + } + + fn echo_u8_vector_first(vector: Vec) -> u8 { + match vector.get(0) { + Option::Some(val) => val, + Option::None => 0, + } + } + + fn echo_u8_option_vector_first(vector: Vec>) -> u8 { + match vector.get(0) { + Option::Some(option) => { + match option { + Option::Some(value) => value, + Option::None => 0, + } + }, + Option::None => 0, + } + } + + fn echo_u64_vector_last(vector: Vec) -> u64 { + match vector.get(vector.len() - 1) { + Option::Some(val) => val, + Option::None => 0, + } + } + + fn echo_u32_vector_addition_other_type(vector: Vec, input: u32) -> u32 { + vector.get(0).unwrap() + input + } + + fn echo_u32_vector_addition(vector_1: Vec, vector_2: Vec) -> u32 { + vector_1.get(0).unwrap() + vector_2.get(0).unwrap() + } + + fn echo_struct_vector_first(vector: Vec) -> BigStruct { + vector.get(0).unwrap() + } + + fn echo_struct_vector_last(vector: Vec) -> ComplexStruct { + vector.get(vector.len() - 1).unwrap() + } + } diff --git a/packages/contract/src/contracts/functions/base-invocation-scope.ts b/packages/contract/src/contracts/functions/base-invocation-scope.ts index a3796fb36cf..7b71ec0faac 100644 --- a/packages/contract/src/contracts/functions/base-invocation-scope.ts +++ b/packages/contract/src/contracts/functions/base-invocation-scope.ts @@ -20,8 +20,13 @@ import type Contract from '../contract'; import { InvocationCallResult, FunctionInvocationResult } from './invocation-results'; function createContractCall(funcScope: InvocationScopeLike): ContractCall { - const { contract, args, forward, func, callParameters } = funcScope.getCallConfig(); - const data = contract.interface.encodeFunctionData(func, args as Array); + const { contract, args, forward, func, callParameters, bytesOffset } = funcScope.getCallConfig(); + + const data = contract.interface.encodeFunctionData( + func, + args as Array, + contractCallScript.getScriptDataOffset() + bytesOffset + ); return { contractId: contract.id, diff --git a/packages/contract/src/contracts/functions/invocation-scope.ts b/packages/contract/src/contracts/functions/invocation-scope.ts index fd759e3d162..dc1af873bc6 100644 --- a/packages/contract/src/contracts/functions/invocation-scope.ts +++ b/packages/contract/src/contracts/functions/invocation-scope.ts @@ -33,6 +33,7 @@ export class FunctionInvocationScope< txParameters: this.txParameters, forward: this.forward, args: this.args, + bytesOffset: this.transactionRequest.bytesOffset || 0, }; } diff --git a/packages/contract/src/types.ts b/packages/contract/src/types.ts index edf574a0052..880056df4cd 100644 --- a/packages/contract/src/types.ts +++ b/packages/contract/src/types.ts @@ -27,6 +27,7 @@ export type CallConfig = { txParameters?: TxParams; forward?: CoinQuantity; args: T; + bytesOffset: number; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/providers/src/LogReader.ts b/packages/providers/src/LogReader.ts new file mode 100644 index 00000000000..771103e6ab4 --- /dev/null +++ b/packages/providers/src/LogReader.ts @@ -0,0 +1,51 @@ +import { arrayify } from '@ethersproject/bytes'; +import { StringCoder } from '@fuel-ts/abi-coder'; +import { ReceiptType } from '@fuel-ts/transactions'; + +import type { + TransactionResultLogDataReceipt, + TransactionResultLogReceipt, + TransactionResultReceipt, +} from './transaction-response'; + +type LogReceipt = TransactionResultLogReceipt | TransactionResultLogDataReceipt; +class LogReader { + logs: LogReceipt[]; + + constructor(receipts: TransactionResultReceipt[]) { + this.logs = receipts.filter( + ({ type }) => type === ReceiptType.Log || type === ReceiptType.LogData + ) as LogReceipt[]; + } + + toArray(): string[] { + return this.logs.map((log) => { + if (log.type === ReceiptType.LogData) { + const stringCoder = new StringCoder(Number(log.len)); + let value = stringCoder.decode(arrayify(log.data), 0)[0]; + value = value.replaceAll('\x00', ''); + return `${value}`; + } + + return `${log.val0}`; + }); + } + + print(): string { + return this.toArray() + .map((log, id) => `[log ${id}] ${log}`) + .join('\n'); + } + + toString(): string { + return this.print(); + } + + static debug(receipts: TransactionResultReceipt[]) { + const logReader = new LogReader(receipts); + // eslint-disable-next-line no-console + console.log(logReader.print()); + } +} + +export default LogReader; diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index 053dd3e6acc..1fadbfad973 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -7,3 +7,4 @@ export { default as Provider } from './provider'; export * from './transaction-request'; export * from './transaction-response'; export * from './util'; +export { default as LogReader } from './LogReader'; diff --git a/packages/providers/src/transaction-request/transaction-request.ts b/packages/providers/src/transaction-request/transaction-request.ts index ec02a646111..8a1cf72d02d 100644 --- a/packages/providers/src/transaction-request/transaction-request.ts +++ b/packages/providers/src/transaction-request/transaction-request.ts @@ -351,6 +351,8 @@ export class ScriptTransactionRequest extends BaseTransactionRequest { script: Uint8Array; /** Script input data (parameters) */ scriptData: Uint8Array; + /** determined bytes offset for start of script data */ + bytesOffset: number | undefined; constructor({ script, scriptData, ...rest }: ScriptTransactionRequestLike = {}) { super(rest); @@ -393,6 +395,10 @@ export class ScriptTransactionRequest extends BaseTransactionRequest { setScript(script: AbstractScript, data: T) { this.script = script.bytes; this.scriptData = script.encodeScriptData(data); + + if (this.bytesOffset === undefined) { + this.bytesOffset = this.scriptData.byteLength; + } } addVariableOutputs(numberOfVariables: number = 1) { diff --git a/packages/script/src/script.ts b/packages/script/src/script.ts index 56b6683265d..db28285a9a3 100644 --- a/packages/script/src/script.ts +++ b/packages/script/src/script.ts @@ -1,5 +1,12 @@ import type { BytesLike } from '@ethersproject/bytes'; import { arrayify } from '@ethersproject/bytes'; +import { + VM_TX_MEMORY, + TRANSACTION_SCRIPT_FIXED_SIZE, + ASSET_ID_LEN, + WORD_SIZE, + CONTRACT_ID_LEN, +} from '@fuel-ts/abi-coder'; import type { BN } from '@fuel-ts/math'; import type { CallResult, @@ -11,17 +18,10 @@ import type { TransactionResult, } from '@fuel-ts/providers'; import { ReceiptType } from '@fuel-ts/transactions'; +import { ByteArrayCoder } from '@fuel-ts/transactions/src/coders/byte-array'; import { ScriptResultDecoderError } from './errors'; -// TODO: Source these from other packages -const VM_TX_MEMORY = 10240; -const TRANSACTION_SCRIPT_FIXED_SIZE = 112; -const WORD_SIZE = 8; -const CONTRACT_ID_LEN = 32; -const ASSET_ID_LEN = 32; -const AMOUNT_LEN = 8; - export type ScriptResult = { code: BN; gasUsed: BN; @@ -85,7 +85,11 @@ export class Script { } getScriptDataOffset() { - return VM_TX_MEMORY + TRANSACTION_SCRIPT_FIXED_SIZE + this.bytes.length; + return ( + VM_TX_MEMORY + + TRANSACTION_SCRIPT_FIXED_SIZE + + new ByteArrayCoder(this.bytes.length).encodedLength + ); } /** @@ -93,7 +97,7 @@ export class Script { * Used for struct inputs */ getArgOffset() { - const callDataOffset = this.getScriptDataOffset() + ASSET_ID_LEN + AMOUNT_LEN; + const callDataOffset = this.getScriptDataOffset() + ASSET_ID_LEN + WORD_SIZE; return callDataOffset + CONTRACT_ID_LEN + WORD_SIZE + WORD_SIZE; } diff --git a/packages/transactions/src/consts.ts b/packages/transactions/src/consts.ts index b805cbdd684..09dfe23d441 100644 --- a/packages/transactions/src/consts.ts +++ b/packages/transactions/src/consts.ts @@ -3,12 +3,6 @@ import { bn } from '@fuel-ts/math'; /** Maximum contract size, in bytes. */ export const CONTRACT_MAX_SIZE = 16 * 1024; -/** Maximum number of inputs. */ -export const MAX_INPUTS = 8; - -/** Maximum number of outputs. */ -export const MAX_OUTPUTS = 8; - /** Maximum number of witnesses. */ export const MAX_WITNESSES = 16; diff --git a/tsconfig.base.json b/tsconfig.base.json index 5314744819d..c7680ac2427 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -4,7 +4,7 @@ "declaration": true, "declarationMap": true, "esModuleInterop": true, - "lib": ["ES2020"], + "lib": ["ES2021"], "module": "commonjs", "resolveJsonModule": true, "skipLibCheck": true,