diff --git a/.changeset/six-cherries-share.md b/.changeset/six-cherries-share.md new file mode 100644 index 0000000000..1e376c6813 --- /dev/null +++ b/.changeset/six-cherries-share.md @@ -0,0 +1,6 @@ +--- +"@fuel-ts/abi-coder": minor +"@fuel-ts/contract": minor +--- + +Update interface to handle array and enum contract arguments diff --git a/packages/abi-coder/src/abi-coder.test.ts b/packages/abi-coder/src/abi-coder.test.ts index 6d4b789082..984d35095f 100644 --- a/packages/abi-coder/src/abi-coder.test.ts +++ b/packages/abi-coder/src/abi-coder.test.ts @@ -40,6 +40,26 @@ describe('AbiCoder', () => { expect(decoded).toEqual([B256, B256]); }); + it('encodes and decodes arrays', () => { + const types = [ + { + name: 'a', + type: '[u64; 3]', + components: [ + { + name: '__array_element', + type: 'u64', + components: null, + }, + ], + }, + ]; + + const encoded = abiCoder.encode(types, [[1, 2, 3]]); + + expect(hexlify(encoded)).toBe('0x000000000000000100000000000000020000000000000003'); + }); + it('encodes and decodes nested reference types', () => { const types = [ { diff --git a/packages/abi-coder/src/abi-coder.ts b/packages/abi-coder/src/abi-coder.ts index fe1e9458e3..67ce68c4cd 100644 --- a/packages/abi-coder/src/abi-coder.ts +++ b/packages/abi-coder/src/abi-coder.ts @@ -18,7 +18,7 @@ import type { JsonAbiFragmentType } from './json-abi'; import { filterEmptyParams } from './utilities'; export const stringRegEx = /str\[(?[0-9]+)\]/; -export const arrayRegEx = /\[(?[\w\s]+);\s*(?[0-9]+)\]/; +export const arrayRegEx = /\[(?[\w\s\\[\]]+);\s*(?[0-9]+)\]/; export const structRegEx = /^struct (?\w+)$/; export const enumRegEx = /^enum (?\w+)$/; export const tupleRegEx = /^\((?.*)\)$/; @@ -46,13 +46,6 @@ export default class AbiCoder { default: } - const stringMatch = stringRegEx.exec(param.type)?.groups; - if (stringMatch) { - const length = parseInt(stringMatch.length, 10); - - return new StringCoder(length); - } - const arrayMatch = arrayRegEx.exec(param.type)?.groups; if (arrayMatch) { const length = parseInt(arrayMatch.length, 10); @@ -64,6 +57,13 @@ export default class AbiCoder { return new ArrayCoder(itemCoder, length); } + const stringMatch = stringRegEx.exec(param.type)?.groups; + if (stringMatch) { + const length = parseInt(stringMatch.length, 10); + + return new StringCoder(length); + } + const structMatch = structRegEx.exec(param.type)?.groups; if (structMatch && Array.isArray(param.components)) { const coders = param.components.reduce((obj, component) => { diff --git a/packages/abi-coder/src/fragments/function-fragment.ts b/packages/abi-coder/src/fragments/function-fragment.ts index 0033a01520..30aaf2a5ac 100644 --- a/packages/abi-coder/src/fragments/function-fragment.ts +++ b/packages/abi-coder/src/fragments/function-fragment.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { FormatTypes, ParamType } from '@ethersproject/abi'; -import { arrayRegEx, structRegEx } from '../abi-coder'; +import { arrayRegEx, enumRegEx, structRegEx } from '../abi-coder'; import type { JsonAbiFragment } from '../json-abi'; import { Fragment } from './fragment'; @@ -18,7 +18,12 @@ function formatOverride(this: ParamType, format?: string): string { const arrayMatch = arrayRegEx.exec(this.type)?.groups; if (arrayMatch) { - return `[${arrayMatch.item}; ${arrayMatch.length}]`; + return `a[${arrayMatch.item};${arrayMatch.length}]`; + } + + const enumMatch = enumRegEx.exec(this.type)?.groups; + if (enumMatch) { + return `e${this.format(format)}`; } } diff --git a/packages/abi-coder/src/interface.test.ts b/packages/abi-coder/src/interface.test.ts index d49689001b..4f95090264 100644 --- a/packages/abi-coder/src/interface.test.ts +++ b/packages/abi-coder/src/interface.test.ts @@ -125,8 +125,42 @@ describe('Interface', () => { expect(decoded.length).toEqual(1); expect(decoded[0]).toEqual(42n); }); + it('can calculate the correct sighash for array string values', () => { + const fnFragment = FunctionFragment.fromObject({ + type: 'function', + inputs: [ + { + name: 'arg', + type: '[s[3]; 3]', + components: [ + { + name: '__array_element', + type: 's[3]', + }, + ], + }, + ], + name: 'takes_array', + outputs: [ + { + name: '', + type: '[s[3]; 2]', + components: [ + { + name: '__array_element', + type: 's[3]', + }, + ], + }, + ], + }); + + expect(fnFragment.format()).toBe('takes_array(a[s[3];3])'); + const sighash = Interface.getSighash(fnFragment); + expect(hexlify(sighash)).toEqual('0x00000000b80a1c57'); + }); - it('can calculate the correct sighash for array values', () => { + it('can calculate the correct sighash for array of u64 values', () => { const fnFragment = FunctionFragment.fromObject({ type: 'function', inputs: [ @@ -155,8 +189,46 @@ describe('Interface', () => { }, ], }); + + expect(fnFragment.format()).toBe('takes_array(a[u16;3])'); + const sighash = Interface.getSighash(fnFragment); + expect(hexlify(sighash)).toEqual('0x00000000101cbeb5'); + }); + + it('can calculate the correct sighash for enum', () => { + const fnFragment = FunctionFragment.fromObject({ + type: 'function', + inputs: [ + { + name: 'enum_arg', + type: 'enum TestEnum', + components: [ + { + name: 'Value', + type: 'bool', + components: null, + }, + { + name: 'Data', + type: 'bool', + components: null, + }, + ], + }, + ], + name: 'take_enum', + outputs: [ + { + name: '', + type: 'bool', + components: null, + }, + ], + }); + + expect(fnFragment.format()).toBe('take_enum(e(bool,bool))'); const sighash = Interface.getSighash(fnFragment); - expect(hexlify(sighash)).toEqual('0x00000000058734b9'); + expect(hexlify(sighash)).toEqual('0x00000000424d6522'); }); it('can encode and decode function data with array values', () => { @@ -191,7 +263,7 @@ describe('Interface', () => { }, ]); expect(hexlify(functionInterface.encodeFunctionData('takes_array', [[1, 2, 3]]))).toEqual( - '0x00000000058734b90000000000000001000000000000000100000000000000020000000000000003' + '0x00000000101cbeb50000000000000001000000000000000100000000000000020000000000000003' ); }); diff --git a/packages/contract/src/__test__/call-test-contract/src/main.sw b/packages/contract/src/__test__/call-test-contract/src/main.sw index 7b2dbab079..e4a9401e35 100644 --- a/packages/contract/src/__test__/call-test-contract/src/main.sw +++ b/packages/contract/src/__test__/call-test-contract/src/main.sw @@ -4,6 +4,21 @@ use std::logging::log; use std::context::{*, call_frames::*, registers::context_gas}; use std::contract_id::ContractId; +enum TestB256Enum { + Value: b256, + Data: b256, +} + +enum TestBoolEnum { + Value: bool, + Data: bool, +} + +enum TestStringEnum { + Value: str[3], + Data: str[3], +} + struct TestStruct { a: bool, b: u64, @@ -35,6 +50,14 @@ abi TestContract { fn return_context_amount() -> u64; fn return_context_asset() -> b256; fn return_context_gas() -> u64; + fn take_array_string_shuffle(a: [str[3];3]) -> [str[3];3]; + fn take_array_string_return_single(a: [str[3];3]) -> [str[3];1]; + fn take_array_string_return_single_element(a: [str[3];3]) -> str[3]; + fn take_array_number(a: [u64;3]) -> u64; + fn take_array_boolean(a: [bool;3]) -> bool; + fn take_b256_enum(a: TestB256Enum) -> b256; + fn take_bool_enum(enum_arg: TestBoolEnum) -> bool; + fn take_string_enum(enum_arg: TestStringEnum) -> str[3]; } impl TestContract for Contract { @@ -93,4 +116,37 @@ impl TestContract for Contract { fn return_context_gas() -> u64 { context_gas() } + fn take_array_string_shuffle(a: [str[3];3]) -> [str[3];3] { + [a[2], a[0], a[1]] + } + fn take_array_string_return_single(a: [str[3];3]) -> [str[3];1] { + [a[0]] + } + fn take_array_string_return_single_element(a: [str[3];3]) -> str[3] { + a[0] + } + fn take_array_number(a: [u64;3]) -> u64 { + a[0] + } + fn take_array_boolean(a: [bool;3]) -> bool { + a[0] + } + fn take_b256_enum(enum_arg: TestB256Enum) -> b256 { + let enum_arg = match enum_arg { + TestB256Enum::Value(val) => val, TestB256Enum::Data(val) => val, + }; + enum_arg + } + fn take_bool_enum(enum_arg: TestBoolEnum) -> bool { + let enum_arg = match enum_arg { + TestBoolEnum::Value(val) => val, TestBoolEnum::Data(val) => val, + }; + enum_arg + } + fn take_string_enum(enum_arg: TestStringEnum) -> str[3] { + let enum_arg = match enum_arg { + TestStringEnum::Value(val) => val, TestStringEnum::Data(val) => val, + }; + enum_arg + } } diff --git a/packages/contract/src/__test__/contract.test.ts b/packages/contract/src/__test__/contract.test.ts index cafc2b87e0..ebdd63c3ce 100644 --- a/packages/contract/src/__test__/contract.test.ts +++ b/packages/contract/src/__test__/contract.test.ts @@ -430,4 +430,86 @@ describe('Contract', () => { .call(); }).rejects.toThrowError(`gasLimit(${gasLimit}) is lower than the required (${gasUsed})`); }); + + it('calls array functions', async () => { + const contract = await setup(); + + const { value: arrayBoolean } = await contract.functions + .take_array_boolean([true, false, false]) + .call(); + + expect(arrayBoolean).toEqual(true); + + const { value: arrayNumber } = await contract.functions.take_array_number([1, 2, 3]).call(); + + expect(arrayNumber).toEqual(1n); + + const { value: arrayReturnShuffle } = await contract.functions + .take_array_string_shuffle(['abc', 'efg', 'hij']) + .call(); + + expect(arrayReturnShuffle).toEqual(['hij', 'abc', 'efg']); + + const { value: arrayReturnSingle } = await contract.functions + .take_array_string_return_single(['abc', 'efg', 'hij']) + .call(); + + expect(arrayReturnSingle).toEqual(['abc']); + }); + + it('calls enum functions', async () => { + const contract = await setup(); + + const { value: enumB256ReturnValue } = await contract.functions + .take_b256_enum({ + Value: '0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b', + }) + .call(); + + expect(enumB256ReturnValue).toEqual( + '0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b' + ); + + const { value: enumB256ReturnData } = await contract.functions + .take_b256_enum({ + Data: '0x1111111111111111111111111111111111111111111111111111111111111111', + }) + .call(); + + expect(enumB256ReturnData).toEqual( + '0x1111111111111111111111111111111111111111111111111111111111111111' + ); + + const { value: enumBoolReturnValue } = await contract.functions + .take_bool_enum({ + Value: true, + }) + .call(); + + expect(enumBoolReturnValue).toEqual(true); + + const { value: enumBoolReturnData } = await contract.functions + .take_bool_enum({ + Data: false, + }) + .call(); + + expect(enumBoolReturnData).toEqual(false); + + const { value: enumStrReturnValue } = await contract.functions + .take_string_enum({ + Value: 'abc', + }) + .call(); + + expect(enumStrReturnValue).toEqual('abc'); + + const { value: enumStrReturnData } = await contract.functions + .take_string_enum({ + Data: 'efg', + }) + .call(); + + expect(enumStrReturnData).toEqual('efg'); + }); }); diff --git a/packages/forc-bin/package.json b/packages/forc-bin/package.json index af37025fb3..f628c57367 100644 --- a/packages/forc-bin/package.json +++ b/packages/forc-bin/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "forc-bin", - "version": "0.18.1", + "version": "0.19.0", "description": "", "author": "Fuel Labs (https://fuel.network/)", "typedocMain": "src/index.ts",