diff --git a/.changeset/gorgeous-plants-tap.md b/.changeset/gorgeous-plants-tap.md new file mode 100644 index 00000000000..e2d22a56b3d --- /dev/null +++ b/.changeset/gorgeous-plants-tap.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/abi-coder": minor +--- + +Fix vector inputs when the first item is not a vector diff --git a/packages/abi-coder/src/abi-coder.test.ts b/packages/abi-coder/src/abi-coder.test.ts index 271a3bd2397..9aabed4aaeb 100644 --- a/packages/abi-coder/src/abi-coder.test.ts +++ b/packages/abi-coder/src/abi-coder.test.ts @@ -368,4 +368,124 @@ describe('AbiCoder', () => { expect(hexlify(encoded)).toBe(expected); }); + + it('encodes inputs with [mixed params + vector second param + with offset]', () => { + const types = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + ]; + + const vector = [450, 202, 1340]; + const encoded = abiCoder.encode(types, [vector], 14440); + + const pointer = [0, 0, 0, 0, 0, 0, 56, 128]; + const capacity = [0, 0, 0, 0, 0, 0, 0, vector.length]; + const length = [0, 0, 0, 0, 0, 0, 0, vector.length]; + const data1 = [0, 0, 0, 0, 0, 0, Math.floor(vector[0] / 256), vector[0] % 256]; + const data2 = [0, 0, 0, 0, 0, 0, 0, vector[1]]; + const data3 = [0, 0, 0, 0, 0, 0, Math.floor(vector[2] / 256), vector[2] % 256]; + const inputAndVecData = concat([pointer, capacity, length, data1, data2, data3]); + + const expected = hexlify(inputAndVecData); + + expect(encoded).toStrictEqual(inputAndVecData); + expect(hexlify(encoded)).toBe(expected); + }); + + it('encodes inputs with [mixed params + vector second param + with offset]', () => { + const types = [ + { + type: 'u32', + name: 'arg1', + }, + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + ]; + + const u32 = 72; + const vector = [450, 202, 1340]; + const encoded = abiCoder.encode(types, [u32, vector], 14440); + + const encodedU32 = [0, 0, 0, 0, 0, 0, 0, u32]; + const pointer = [0, 0, 0, 0, 0, 0, 56, 136]; + const capacity = [0, 0, 0, 0, 0, 0, 0, vector.length]; + const length = [0, 0, 0, 0, 0, 0, 0, vector.length]; + const data1 = [0, 0, 0, 0, 0, 0, Math.floor(vector[0] / 256), vector[0] % 256]; + const data2 = [0, 0, 0, 0, 0, 0, 0, vector[1]]; + const data3 = [0, 0, 0, 0, 0, 0, Math.floor(vector[2] / 256), vector[2] % 256]; + const inputAndVecData = concat([encodedU32, pointer, capacity, length, data1, data2, data3]); + + const expected = hexlify(inputAndVecData); + + expect(encoded).toStrictEqual(inputAndVecData); + expect(hexlify(encoded)).toBe(expected); + }); }); diff --git a/packages/abi-coder/src/utilities.test.ts b/packages/abi-coder/src/utilities.test.ts new file mode 100644 index 00000000000..101767db051 --- /dev/null +++ b/packages/abi-coder/src/utilities.test.ts @@ -0,0 +1,646 @@ +import AbiCoder from './abi-coder'; +import VecCoder from './coders/vec'; +import { WORD_SIZE } from './constants'; +import { ParamType } from './fragments/param-type'; +import type { JsonAbiFragmentType } from './json-abi'; +import { filterEmptyParams, hasOptionTypes, getVectorAdjustments } from './utilities'; + +describe('Abi Coder Utilities', () => { + it('can filterEmptyParams', () => { + const INPUT: ParamType[] = [ + new ParamType({ + type: '()', + }), + new ParamType({ + type: 'enum Option', + }), + new ParamType({ + type: '()', + }), + ]; + const EXPECTED = [ + new ParamType({ + type: 'enum Option', + }), + ]; + + const RESULT = filterEmptyParams(INPUT); + expect(RESULT).toStrictEqual(EXPECTED); + }); + + it('can determine if types array hasOptionTypes [true]', () => { + const INPUT: ParamType[] = [ + new ParamType({ + type: 'enum Option', + }), + ]; + + const RESULT = hasOptionTypes(INPUT); + expect(RESULT).toStrictEqual(true); + }); + + it('can determine if types array hasOptionTypes [false]', () => { + const INPUT: ParamType[] = [ + new ParamType({ + type: 'struct Vec', + }), + ]; + + const RESULT = hasOptionTypes(INPUT); + expect(RESULT).toStrictEqual(false); + }); + + it('can getVectorAdjustments [no Vectors, offset = 0]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + type: 'b256', + name: 'arg0', + }, + { + type: 'b256', + name: 'arg1', + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [43]; + const EXPECTED: Uint8Array[] = []; + + const RESULT = getVectorAdjustments(CODERS, VALUES, 0); + expect(RESULT).toStrictEqual(EXPECTED); + }); + + it('can getVectorAdjustments [no Vectors, offset = 8]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + type: 'b256', + name: 'arg0', + }, + { + type: 'b256', + name: 'arg1', + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [43]; + const EXPECTED: Uint8Array[] = []; + + const RESULT = getVectorAdjustments(CODERS, VALUES, 8); + expect(RESULT).toStrictEqual(EXPECTED); + }); + + it('can getVectorAdjustments [inputs=[Vector], offset = 0]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [[1, 2, 34]]; + const EXPECTED: Uint8Array[] = [ + 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, 34]), + ]; + + const RESULT = getVectorAdjustments(CODERS, VALUES, 0); + expect(RESULT).toStrictEqual(EXPECTED); + expect(CODERS[0].offset).toStrictEqual(VecCoder.getBaseOffset()); + }); + + it('can getVectorAdjustments [inputs=[Vector], offset = 32]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [[1, 2, 34]]; + const OFFSET = 32; + const EXPECTED: Uint8Array[] = [ + 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, 34]), + ]; + + const RESULT = getVectorAdjustments(CODERS, VALUES, OFFSET); + expect(RESULT).toStrictEqual(EXPECTED); + expect(CODERS[0].offset).toStrictEqual(VecCoder.getBaseOffset() + OFFSET); + }); + + it('can getVectorAdjustments [inputs=[Vector, Vector], offset = 0]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [ + [1, 2, 34], + [71, 72, 99], + ]; + const EXPECTED: Uint8Array[] = [ + 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, 34]), + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 99]), + ]; + + const RESULT = getVectorAdjustments(CODERS, VALUES, 0); + expect(RESULT).toStrictEqual(EXPECTED); + // one base vec offset per each vector input + expect(CODERS[0].offset).toStrictEqual(VecCoder.getBaseOffset() + VecCoder.getBaseOffset()); + // one base vec offset per each vector input + the first vector's data + expect(CODERS[1].offset).toStrictEqual( + VecCoder.getBaseOffset() + VecCoder.getBaseOffset() + VALUES[0].length * WORD_SIZE + ); + }); + + it('can getVectorAdjustments [inputs=[Vector,Vector], offset = 32]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [ + [7, 2, 34], + [867, 5309, 1337], + ]; + const OFFSET = 32; + const EXPECTED: Uint8Array[] = [ + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 34]), + new Uint8Array([0, 0, 0, 0, 0, 0, 3, 99, 0, 0, 0, 0, 0, 0, 20, 189, 0, 0, 0, 0, 0, 0, 5, 57]), + ]; + + const RESULT = getVectorAdjustments(CODERS, VALUES, OFFSET); + expect(RESULT).toStrictEqual(EXPECTED); + // one base vec offset per each vector input + plus custom OFFSET + expect(CODERS[0].offset).toStrictEqual( + VecCoder.getBaseOffset() + VecCoder.getBaseOffset() + OFFSET + ); + // one base vec offset per each vector input + the first vector's data + plus custom OFFSET + expect(CODERS[1].offset).toStrictEqual( + VecCoder.getBaseOffset() + VecCoder.getBaseOffset() + VALUES[0].length * WORD_SIZE + OFFSET + ); + }); + + it('can getVectorAdjustments [inputs=[Vector,b256], offset = 8]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u8', + }, + ], + }, + { + type: 'b256', + name: 'arg1', + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [[1, 3, 3, 7], 43]; + const OFFSET = 8; + const EXPECTED: Uint8Array[] = [ + new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, + 7, + ]), + ]; + + const RESULT = getVectorAdjustments(CODERS, VALUES, OFFSET); + expect(RESULT).toStrictEqual(EXPECTED); + // one base vec offset + plus b256 data + plus custom OFFSET + expect(CODERS[0].offset).toStrictEqual(VecCoder.getBaseOffset() + 4 * WORD_SIZE + OFFSET); + }); + + it('can getVectorAdjustments [inputs=[b256,Vector], offset = 14440]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + type: 'u32', + name: 'arg1', + }, + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [33, [450, 202, 340]]; + const OFFSET = 14440; + const EXPECTED: Uint8Array[] = [ + new Uint8Array([0, 0, 0, 0, 0, 0, 1, 194, 0, 0, 0, 0, 0, 0, 0, 202, 0, 0, 0, 0, 0, 0, 1, 84]), + ]; + + const RESULT = getVectorAdjustments(CODERS, VALUES, OFFSET); + expect(RESULT).toStrictEqual(EXPECTED); + // one base vec offset + plus u32 data + plus custom OFFSET + expect(CODERS[1].offset).toStrictEqual(VecCoder.getBaseOffset() + WORD_SIZE + OFFSET); + }); + + it('can getVectorAdjustments [inputs=[b256,Vector,Vector,Vector], offset = 24]', () => { + const abiCoder = new AbiCoder(); + const NON_EMPTY_TYPES: ReadonlyArray = [ + { + type: 'u32', + name: 'arg1', + }, + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'vector', + type: 'struct Vec', + components: [ + { + name: 'buf', + type: 'struct RawVec', + components: [ + { + name: 'ptr', + type: 'raw untyped ptr', + }, + { + name: 'cap', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + { + name: 'len', + type: 'u64', + }, + ], + typeArguments: [ + { + name: '', + type: 'u64', + }, + ], + }, + ]; + const CODERS = NON_EMPTY_TYPES.map((type) => abiCoder.getCoder(type)); + const VALUES = [33, [450, 202, 340], [12, 13, 14], [11, 9]]; + const OFFSET = 24; + const EXPECTED: Uint8Array[] = [ + new Uint8Array([0, 0, 0, 0, 0, 0, 1, 194, 0, 0, 0, 0, 0, 0, 0, 202, 0, 0, 0, 0, 0, 0, 1, 84]), + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 14]), + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 9]), + ]; + + const OFFSET_PLUS_DATA_PLUS_3_OFFSETS = VecCoder.getBaseOffset() * 3 + WORD_SIZE + OFFSET; + + const RESULT = getVectorAdjustments(CODERS, VALUES, OFFSET); + expect(RESULT).toStrictEqual(EXPECTED); + // three base vec offset + plus u32 data + plus custom OFFSET + expect(CODERS[1].offset).toStrictEqual(OFFSET_PLUS_DATA_PLUS_3_OFFSETS); + // three base vec offset + plus u32 data + the first vector's data + plus custom OFFSET + expect(CODERS[2].offset).toStrictEqual(OFFSET_PLUS_DATA_PLUS_3_OFFSETS + 3 * WORD_SIZE); + // three base vec offset + plus u32 data + the first vector's data + the second vector's data + plus custom OFFSET + expect(CODERS[3].offset).toStrictEqual( + OFFSET_PLUS_DATA_PLUS_3_OFFSETS + 3 * WORD_SIZE + 3 * WORD_SIZE + ); + }); +}); diff --git a/packages/abi-coder/src/utilities.ts b/packages/abi-coder/src/utilities.ts index 8896b6721e8..5ed294e38b8 100644 --- a/packages/abi-coder/src/utilities.ts +++ b/packages/abi-coder/src/utilities.ts @@ -32,6 +32,10 @@ export function getVectorAdjustments( return { vecByteLength: data.byteLength }; }); + if (!vectorData.length) { + return vectorData; + } + const baseVectorOffset = vectorData.length * VecCoder.getBaseOffset() + offset; const offsetMap = coders.map((encoder, paramIndex) => { if (!(encoder instanceof VecCoder)) { @@ -39,20 +43,18 @@ export function getVectorAdjustments( } return byteMap.reduce((sum, byteInfo, byteIndex) => { + // non-vector data if ('byteLength' in byteInfo) { return sum + byteInfo.byteLength; } - if (byteIndex === 0 && byteIndex === paramIndex) { - return baseVectorOffset; - } - + // account for preceding vector data earlier in input list if (byteIndex < paramIndex) { - return sum + byteInfo.vecByteLength + baseVectorOffset; + return sum + byteInfo.vecByteLength; } return sum; - }, 0); + }, baseVectorOffset); }); coders.forEach((code, i) => code.setOffset(offsetMap[i])); diff --git a/packages/fuel-gauge/src/coverage-contract.test.ts b/packages/fuel-gauge/src/coverage-contract.test.ts index 1b12066bced..14a9dc7d3a8 100644 --- a/packages/fuel-gauge/src/coverage-contract.test.ts +++ b/packages/fuel-gauge/src/coverage-contract.test.ts @@ -473,4 +473,30 @@ describe('Coverage Contract', () => { expect(value.map((v: BN) => v.toNumber())).toStrictEqual([1, 2, 3]); }); + + it('should try vec_as_only_param', async () => { + const { value } = await contractInstance.functions + .vec_as_only_param([100, 450, 202, 340]) + .call(); + + expect(value.map((v: BN) => v.toHex())).toStrictEqual([ + bn(4).toHex(), + bn(100).toHex(), + bn(450).toHex(), + bn(202).toHex(), + ]); + }); + + it('should try u32_and_vec_params', async () => { + const { value } = await contractInstance.functions + .u32_and_vec_params(33, [450, 202, 340]) + .call(); + + expect(value.map((v: BN) => v.toHex())).toStrictEqual([ + bn(3).toHex(), + bn(450).toHex(), + bn(202).toHex(), + bn(340).toHex(), + ]); + }); }); diff --git a/packages/fuel-gauge/test-projects/coverage-contract/src/main.sw b/packages/fuel-gauge/test-projects/coverage-contract/src/main.sw index ae78cc38f82..759d22266c3 100644 --- a/packages/fuel-gauge/test-projects/coverage-contract/src/main.sw +++ b/packages/fuel-gauge/test-projects/coverage-contract/src/main.sw @@ -96,6 +96,8 @@ abi CoverageContract { fn get_u64_vector() -> raw_slice; fn echo_u8_vector(input: Vec) -> raw_slice; fn echo_u64_vector(input: Vec) -> raw_slice; + fn vec_as_only_param(input: Vec) -> (u64, Option, Option, Option); + fn u32_and_vec_params(foo: u32, input: Vec) -> (u64, Option, Option, Option); } impl CoverageContract for Contract { @@ -344,4 +346,22 @@ impl CoverageContract for Contract { fn echo_u64_vector(input: Vec) -> raw_slice { input.as_raw_slice() } + + fn vec_as_only_param(input: Vec) -> (u64, Option, Option, Option) { + ( + input.len(), + input.get(0), + input.get(1), + input.get(2), + ) + } + + fn u32_and_vec_params(foo: u32, input: Vec) -> (u64, Option, Option, Option) { + ( + input.len(), + input.get(0), + input.get(1), + input.get(2), + ) + } }