diff --git a/.changeset/flat-roses-lie.md b/.changeset/flat-roses-lie.md
new file mode 100644
index 00000000000..632f2c9f283
--- /dev/null
+++ b/.changeset/flat-roses-lie.md
@@ -0,0 +1,4 @@
+---
+---
+
+feat: ABI refactor
diff --git a/.changeset/nice-books-tease.md b/.changeset/nice-books-tease.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/nice-books-tease.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/.changeset/poor-years-hang.md b/.changeset/poor-years-hang.md
new file mode 100644
index 00000000000..e1335f22087
--- /dev/null
+++ b/.changeset/poor-years-hang.md
@@ -0,0 +1,6 @@
+---
+"@fuel-ts/abi": minor
+"fuels": minor
+---
+
+feat!: ABI Gen
diff --git a/.changeset/tender-tigers-fry.md b/.changeset/tender-tigers-fry.md
new file mode 100644
index 00000000000..2e270bb05e9
--- /dev/null
+++ b/.changeset/tender-tigers-fry.md
@@ -0,0 +1,7 @@
+---
+"@fuel-ts/abi": patch
+"fuels": patch
+"@fuel-ts/errors": patch
+---
+
+feat: ABI parser
diff --git a/.github/actions/test-setup/action.yaml b/.github/actions/test-setup/action.yaml
index 8d7c3d6cf0c..0c649356d13 100644
--- a/.github/actions/test-setup/action.yaml
+++ b/.github/actions/test-setup/action.yaml
@@ -39,6 +39,12 @@ runs:
with:
bun-version: ${{ inputs.bun-version }}
+ - name: Set forc and fuel-core paths
+ shell: bash
+ run: |
+ echo "$GITHUB_WORKSPACE/internal/forc/forc-binaries" >> $GITHUB_PATH
+ echo "$GITHUB_WORKSPACE/internal/fuel-core/fuel-core-binaries" >> $GITHUB_PATH
+
- name: Build
run: pnpm build
shell: bash
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index b4eebdd9f42..d35a042e492 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -92,6 +92,7 @@ jobs:
PUBLISHED_NPM_TAG: next
e2e:
+ if: false
runs-on: ubuntu-latest
timeout-minutes: 25
needs: [environments]
diff --git a/.knip.json b/.knip.json
index 5f0eb9e6e4f..46d7d8fbde3 100644
--- a/.knip.json
+++ b/.knip.json
@@ -8,7 +8,7 @@
"fuels",
"bun",
"@types/rimraf",
- "@fuel-ts/abi-typegen",
+ "@fuel-ts/abi",
"@internal/fuel-core",
"get-graphql-schema",
"events",
diff --git a/apps/demo-typegen/package.json b/apps/demo-typegen/package.json
index 520890ac0a3..9d6d61ce1b4 100644
--- a/apps/demo-typegen/package.json
+++ b/apps/demo-typegen/package.json
@@ -11,8 +11,8 @@
"forc:predicate": "pnpm fuels-forc build -p demo-predicate --release",
"build:types": "run-p types:*",
"types:contract": "pnpm fuels typegen -i demo-contract/out/release/demo-contract-abi.json -o src/contract-types",
- "types:script": "pnpm fuels typegen -i demo-script/out/release/demo-script-abi.json -o src/script-types --script",
- "types:predicate": "pnpm fuels typegen -i demo-predicate/out/release/demo-predicate-abi.json -o src/predicate-types --predicate"
+ "types:script": "pnpm fuels typegen -i demo-script/out/release/demo-script-abi.json -o src/script-types",
+ "types:predicate": "pnpm fuels typegen -i demo-predicate/out/release/demo-predicate-abi.json -o src/predicate-types"
},
"license": "Apache-2.0",
"dependencies": {
diff --git a/apps/demo-typegen/src/demo.test.ts b/apps/demo-typegen/src/demo.test.ts
index d52c7bf2383..9134931b910 100644
--- a/apps/demo-typegen/src/demo.test.ts
+++ b/apps/demo-typegen/src/demo.test.ts
@@ -2,11 +2,9 @@
import { toHex, Address, Wallet, FuelError, ErrorCode } from 'fuels';
import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils';
-import storageSlots from '../demo-contract/out/release/demo-contract-storage_slots.json';
-
import { DemoContract, DemoContractFactory } from './contract-types';
import { DemoPredicate } from './predicate-types';
-import type { DemoPredicateInputs } from './predicate-types/DemoPredicate';
+import type { DemoPredicateInputs } from './predicate-types/predicates/DemoPredicate';
import { DemoScript } from './script-types';
/**
@@ -25,7 +23,7 @@ describe('ExampleContract', () => {
// #context import { DemoContractFactory } from './sway-programs-api';
const { waitForResult } = await DemoContractFactory.deploy(wallet, {
- storageSlots,
+ storageSlots: DemoContractFactory.storageSlots,
});
const { contract } = await waitForResult();
diff --git a/apps/docs-api/index.md b/apps/docs-api/index.md
index 9e9f1c5e6bc..31a32022812 100644
--- a/apps/docs-api/index.md
+++ b/apps/docs-api/index.md
@@ -12,6 +12,9 @@
# Modules
+
+
+
- [abi-coder](https://fuels-ts-docs-api.vercel.app/modules/_fuel_ts_abi_coder.html)
- [abi-typegen](https://fuels-ts-docs-api.vercel.app/modules/_fuel_ts_abi_typegen.html)
- [account](https://fuels-ts-docs-api.vercel.app/modules/_fuel_ts_account.html)
diff --git a/apps/docs-api/typedoc.json b/apps/docs-api/typedoc.json
index 80cfb47a49a..f04aba511f4 100644
--- a/apps/docs-api/typedoc.json
+++ b/apps/docs-api/typedoc.json
@@ -2,6 +2,7 @@
"$schema": "https://typedoc.org/schema.json",
"entryPointStrategy": "packages",
"entryPoints": [
+ "../../packages/abi",
"../../packages/abi-coder",
"../../packages/abi-typegen",
"../../packages/address",
diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts
index 288308581b4..229841ebda5 100644
--- a/apps/docs/.vitepress/config.ts
+++ b/apps/docs/.vitepress/config.ts
@@ -441,6 +441,10 @@ export default defineConfig({
text: 'Optimized React Example',
link: '/guide/cookbook/optimized-react-example',
},
+ {
+ text: 'Working with the ABI',
+ link: '/guide/cookbook/working-with-the-abi',
+ },
],
},
{
diff --git a/apps/docs/spell-check-custom-words.txt b/apps/docs/spell-check-custom-words.txt
index 97456d9655b..b83448ca4cf 100644
--- a/apps/docs/spell-check-custom-words.txt
+++ b/apps/docs/spell-check-custom-words.txt
@@ -343,4 +343,5 @@ Workspaces
WSL
XOR
XORs
-YAML
\ No newline at end of file
+YAML
+matcher
\ No newline at end of file
diff --git a/apps/docs/src/guide/contracts/snippets/proxy-contracts.ts b/apps/docs/src/guide/contracts/snippets/proxy-contracts.ts
index 50db63824a0..6e1ee1faa37 100644
--- a/apps/docs/src/guide/contracts/snippets/proxy-contracts.ts
+++ b/apps/docs/src/guide/contracts/snippets/proxy-contracts.ts
@@ -1,10 +1,5 @@
// #region proxy-2
-import {
- Provider,
- Wallet,
- Src14OwnedProxy,
- Src14OwnedProxyFactory,
-} from 'fuels';
+import { Provider, Wallet, Src14OwnedProxyFactory } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env';
import {
@@ -28,7 +23,7 @@ const { contract: counterContract } = await deploy.waitForResult();
* initialize the storage slots.
*/
const storageSlots = counterContractFactory.storageSlots.concat(
- Src14OwnedProxy.storageSlots
+ Src14OwnedProxyFactory.storageSlots
);
/**
* These configurables are specific to our recommended SRC14 compliant
diff --git a/apps/docs/src/guide/contracts/snippets/storage-slots/override-storage-slots.ts b/apps/docs/src/guide/contracts/snippets/storage-slots/override-storage-slots.ts
index a4b4c289f1f..53f82cf028e 100644
--- a/apps/docs/src/guide/contracts/snippets/storage-slots/override-storage-slots.ts
+++ b/apps/docs/src/guide/contracts/snippets/storage-slots/override-storage-slots.ts
@@ -2,16 +2,13 @@
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
-import {
- StorageTestContract,
- StorageTestContractFactory,
-} from '../../../../typegend';
+import { StorageTestContractFactory } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deploymentTx = await StorageTestContractFactory.deploy(deployer, {
- storageSlots: StorageTestContract.storageSlots,
+ storageSlots: StorageTestContractFactory.storageSlots,
});
await deploymentTx.waitForResult();
diff --git a/apps/docs/src/guide/cookbook/snippets/parsing-the-abi.ts b/apps/docs/src/guide/cookbook/snippets/parsing-the-abi.ts
new file mode 100644
index 00000000000..8b54490de56
--- /dev/null
+++ b/apps/docs/src/guide/cookbook/snippets/parsing-the-abi.ts
@@ -0,0 +1,9 @@
+// #region full
+import { AbiParser } from 'fuels';
+import type { Abi, AbiSpecificationV1 } from 'fuels';
+
+import { Counter } from '../../../typegend';
+
+const parsedAbi: Abi = AbiParser.parse(Counter.abi as AbiSpecificationV1);
+// #endregion full
+console.log('Parsed ABI:', parsedAbi);
diff --git a/apps/docs/src/guide/cookbook/working-with-the-abi.md b/apps/docs/src/guide/cookbook/working-with-the-abi.md
new file mode 100644
index 00000000000..3824f20c576
--- /dev/null
+++ b/apps/docs/src/guide/cookbook/working-with-the-abi.md
@@ -0,0 +1,11 @@
+# Working with the ABI
+
+Building a Sway program with `forc build` outputs multiple files, one of which is a JSON representation of the program's ABI. Because ABI specifications can change from one `forc` version to another, working directly with the ABI is cumbersome due to having to manage all ABI specification versions in order to ensure proper functionality.
+
+
+
+
+
+To mitigate this, The SDK provides [`AbiParser`](#working-with-the-abi) which can parse all ABI specification versions and output an object that conforms to the [`Abi`](#working-with-the-abi) interface. The SDK also internally uses this `Abi` interface for implementing its encoding/decoding and TS type generation.
+
+<<< @./snippets/parsing-the-abi.ts#full{ts:line-numbers}
diff --git a/apps/docs/src/guide/encoding/encode-and-decode.md b/apps/docs/src/guide/encoding/encode-and-decode.md
index 6ed76fad5ad..561e7feeaa1 100644
--- a/apps/docs/src/guide/encoding/encode-and-decode.md
+++ b/apps/docs/src/guide/encoding/encode-and-decode.md
@@ -1,13 +1,13 @@
# Encode and Decode
-To interact with the FuelVM, types must be encoded and decoded per the [argument encoding specification](https://docs.fuel.network/docs/specs/abi/argument-encoding/). The SDK provides the `Interface` class to encode and decode data.
+To interact with the FuelVM, types must be encoded and decoded per the [argument encoding specification](https://docs.fuel.network/docs/specs/abi/argument-encoding/). The SDK provides the `AbiCoder` class to encode and decode data.
-The relevant methods of `Interface` are:
+To encode and decode types, the `AbiCoder` class provides the `getType` method which returns a `AbiCoderType` instance that provides the following methods:
-- `encodeType`
-- `decodeType`
+- `encode`
+- `decode`
-The `Interface` class requires you to pass the [ABI](https://docs.fuel.network/docs/specs/abi/json-abi-format/) on initialization. Both methods accept a `concreteTypeId`, which must exist in the ABI's `concreteTypes` array. After that, a suitable coder will be assigned to encode/decode that type.
+The `AbiCoder` class requires you to pass the [ABI](https://docs.fuel.network/docs/specs/abi/json-abi-format/) on initialization. Both methods accept a `concreteTypeId`, which must exist in the ABI's `concreteTypes` array. After that, a suitable coder will be assigned to encode/decode that type.
Imagine we are working with the following script that returns the sum of two `u32` integers:
@@ -23,7 +23,7 @@ It will produce the following ABI:
<<< @./snippets/encode-and-decode.jsonc#encode-and-decode-2{json:line-numbers}
-Now, let's prepare some data to pass to the `main` function to retrieve the combined integer. The function expects and returns a `u32` integer. So here, we will encode the `u32` to pass it to the function and receive the same `u32` back, as bytes, that we'll use for decoding. We can do both of these with the `Interface`.
+Now, let's prepare some data to pass to the `main` function to retrieve the combined integer. The function expects and returns a `u32` integer. So here, we will encode the `u32` to pass it to the function and receive the same `u32` back, as bytes, that we'll use for decoding. We can do both of these with the `AbiCoder`.
First, let's prepare the transaction:
diff --git a/apps/docs/src/guide/encoding/snippets/encode-and-decode.ts b/apps/docs/src/guide/encoding/snippets/encode-and-decode.ts
index 0565c7b16b8..3640ae93e8a 100644
--- a/apps/docs/src/guide/encoding/snippets/encode-and-decode.ts
+++ b/apps/docs/src/guide/encoding/snippets/encode-and-decode.ts
@@ -1,12 +1,15 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// #region full
-import type { JsonAbi, TransactionResultReturnDataReceipt } from 'fuels';
+import type {
+ AbiSpecification,
+ TransactionResultReturnDataReceipt,
+} from 'fuels';
import {
buildFunctionResult,
ReceiptType,
arrayify,
Script,
- Interface,
+ AbiCoder,
Provider,
Wallet,
} from 'fuels';
@@ -20,7 +23,7 @@ const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
// First we need to build out the transaction via the script that we want to encode.
// For that we'll need the ABI and the bytecode of the script
-const abi: JsonAbi = ScriptSum.abi;
+const abi: AbiSpecification = ScriptSum.abi;
const bytecode = ScriptSum.bytecode;
// Create the invocation scope for the script call, passing the initial
@@ -43,13 +46,13 @@ const argument = abi.functions
.find((f) => f.name === 'main')
?.inputs.find((i) => i.name === 'inputted_amount')?.concreteTypeId as string;
-// The `Interface` class (imported from `fuels`) is the entry point for encoding and decoding all things abi-related.
-// We will use its `encodeType` method and create the encoding required for
-// a u32 which takes 4 bytes up of property space.
+// The `AbiCoder` class (imported from `fuels`) is the entry point for encoding and decoding all things abi-related.
+// We will use its `getType` method to get the coder for the argument and then call its `encode` method to
+// create the encoding required for a u32 which takes 4 bytes up of property space.
-const abiInterface = new Interface(abi);
+const abiCoder = AbiCoder.fromAbi(abi);
const argumentToAdd = 10;
-const encodedArguments = abiInterface.encodeType(argument, [argumentToAdd]);
+const encodedArguments = abiCoder.getType(argument).encode([argumentToAdd]);
// Therefore the value of 10 will be encoded to:
// Uint8Array([0, 0, 0, 10]
@@ -91,10 +94,13 @@ const returnData = arrayify(returnDataReceipt.data);
// returnData = new Uint8Array([0, 0, 0, 20]
// And now we can decode the returned bytes in a similar fashion to how they were
-// encoded, via the `Interface`
-const [decodedReturnData] = abiInterface.decodeType(argument, returnData);
+// encoded, via the `AbiCoder`
+const decodedReturnData = abiCoder.getType(argument).decode(returnData);
// 20
const totalValue = argumentToAdd + initialValue;
// #endregion encode-and-decode-5
// #endregion full
+
+console.log('decodedReturnData should be 20', decodedReturnData === 20);
+console.log('totalValue should be 20', totalValue === 20);
diff --git a/apps/docs/src/guide/encoding/snippets/working-with-bytes.ts b/apps/docs/src/guide/encoding/snippets/working-with-bytes.ts
index 1dc89c369dd..814b7645cec 100644
--- a/apps/docs/src/guide/encoding/snippets/working-with-bytes.ts
+++ b/apps/docs/src/guide/encoding/snippets/working-with-bytes.ts
@@ -1,88 +1,115 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
// #region full
import { randomBytes } from 'crypto';
-import {
- ArrayCoder,
- B256Coder,
- B512Coder,
- BigNumberCoder,
- BooleanCoder,
- EnumCoder,
- NumberCoder,
- RawSliceCoder,
- StdStringCoder,
- StringCoder,
- StructCoder,
- TupleCoder,
- VecCoder,
- hexlify,
-} from 'fuels';
+import { encoding, hexlify } from 'fuels';
// #region working-with-bytes-1
-const u8Coder = new NumberCoder('u8');
-const encodedU8 = u8Coder.encode(255);
+const encodedU8 = encoding.u8.encode(255);
-const u16Coder = new NumberCoder('u16');
-const encodedU16 = u16Coder.encode(255);
+const encodedU16 = encoding.u16.encode(255);
-const u32Coder = new NumberCoder('u32');
-const encodedU32 = u32Coder.encode(255);
+const encodedU32 = encoding.u32.encode(255);
-const u64Coder = new BigNumberCoder('u64');
-const encodedU64 = u64Coder.encode(255);
+const encodedU64 = encoding.u64.encode(255);
-const u256Coder = new BigNumberCoder('u256');
-const encodedU256 = u256Coder.encode(255);
+const encodedU256 = encoding.u256.encode(255);
// #endregion working-with-bytes-1
// #region working-with-bytes-2
-const booleanCoder = new BooleanCoder();
-const encodedTrue = booleanCoder.encode(true);
-
-const encodedFalse = booleanCoder.encode(false);
+const encodedTrue = encoding.bool.encode(true);
+const encodedFalse = encoding.bool.encode(false);
// #endregion working-with-bytes-2
// #region working-with-bytes-3
-const stringCoder = new StringCoder(5);
-const encoded = stringCoder.encode('hello');
+const stringCoder = encoding.string(5);
+const encodedString = stringCoder.encode('hello');
// #endregion working-with-bytes-3
// #region working-with-bytes-4
-const b256Coder = new B256Coder();
-const encodedB256 = b256Coder.encode(hexlify(randomBytes(32)));
-const b512Coder = new B512Coder();
-const encodedB512 = b512Coder.encode(hexlify(randomBytes(64)));
+const encodedB256 = encoding.b256.encode(hexlify(randomBytes(32)));
+
+const encodedB512 = encoding.b512.encode(hexlify(randomBytes(64)));
// #endregion working-with-bytes-4
// #region working-with-bytes-5
-const tupleCoder = new TupleCoder([
- new NumberCoder('u8'),
- new NumberCoder('u16'),
-]);
+const tupleCoder = encoding.tuple([encoding.u8, encoding.u16]);
const encodedTuple = tupleCoder.encode([255, 255]);
-const structCoder = new StructCoder('struct', {
- a: new NumberCoder('u8'),
- b: new NumberCoder('u16'),
+const structCoder = encoding.struct({
+ a: encoding.u8,
+ b: encoding.u16,
});
const encodedStruct = structCoder.encode({ a: 255, b: 255 });
-const arrayCoder = new ArrayCoder(new NumberCoder('u8'), 4);
+const arrayCoder = encoding.array(encoding.u8, 4);
const encodedArray = arrayCoder.encode([255, 0, 255, 0]);
-const enumCoder = new EnumCoder('enum', { a: new NumberCoder('u32') });
+const enumCoder = encoding.enum({ a: encoding.u32 });
const encodedEnum = enumCoder.encode({ a: 255 });
// #endregion working-with-bytes-5
// #region working-with-bytes-6
-const vecCoder = new VecCoder(new NumberCoder('u8'));
+const vecCoder = encoding.vector(encoding.u8);
const encodedVec = vecCoder.encode([255, 0, 255]);
-const stdStringCoder = new StdStringCoder();
-const encodedStdString = stdStringCoder.encode('hello');
+const encodedStdString = encoding.stdString.encode('hello');
-const rawSliceCoder = new RawSliceCoder();
-const encodedRawSlice = rawSliceCoder.encode([1, 2, 3, 4]);
+const encodedRawSlice = encoding.rawSlice.encode([1, 2, 3, 4]);
// #endregion working-with-bytes-6
// #endregion full
+
+console.log('encodedU8 should be [255]', encodedU8.toString() === '255');
+console.log('encodedU16 should be [0, 255]', encodedU16.toString() === '0,255');
+console.log(
+ 'encodedU32 should be [0, 0, 0, 255]',
+ encodedU32.toString() === '0,0,0,255'
+);
+console.log(
+ 'encodedU64 should be [0, 0, 0, 0, 0, 0, 0, 255]',
+ encodedU64.toString() === '0,0,0,0,0,0,0,255'
+);
+console.log(
+ 'encodedU256 should be [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255]',
+ encodedU256.toString() ===
+ '0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255'
+);
+
+console.log('encodedTrue should be [1]', encodedTrue.toString() === '1');
+console.log('encodedFalse should be [0]', encodedFalse.toString() === '0');
+
+console.log(
+ 'encodedString should be [104, 101, 108, 108, 111]',
+ encodedString.toString() === '104,101,108,108,111'
+);
+
+console.log('encodedB256 should be 32', encodedB256.length === 32);
+console.log('encodedB512 should be 64', encodedB512.length === 64);
+
+console.log(
+ 'encodedTuple should be [255, 0, 255]',
+ encodedTuple.toString() === '255,0,255'
+);
+console.log(
+ 'encodedStruct should be [255, 0, 255]',
+ encodedStruct.toString() === '255,0,255'
+);
+console.log(
+ 'encodedArray should be [255, 0, 255, 0]',
+ encodedArray.toString() === '255,0,255,0'
+);
+console.log(
+ 'encodedEnum should be [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255]',
+ encodedEnum.toString() === '0,0,0,0,0,0,0,0,0,0,0,255'
+);
+console.log(
+ 'encodedVec should be [0, 0, 0, 0, 0, 0, 0, 3, 255, 0, 255]',
+ encodedVec.toString() === '0,0,0,0,0,0,0,3,255,0,255'
+);
+console.log(
+ 'encodedStdString should be [0, 0, 0, 0, 0, 0, 0, 5, 104, 101, 108, 108, 111]',
+ encodedStdString.toString() === '0,0,0,0,0,0,0,5,104,101,108,108,111'
+);
+console.log(
+ 'encodedRawSlice should be [0, 0, 0, 0, 0, 0, 0, 4, 1, 2, 3, 4]',
+ encodedRawSlice.toString() === '0,0,0,0,0,0,0,4,1,2,3,4'
+);
diff --git a/apps/docs/src/guide/encoding/working-with-bytes.md b/apps/docs/src/guide/encoding/working-with-bytes.md
index 9d1f8149cbb..e82bb7e9cea 100644
--- a/apps/docs/src/guide/encoding/working-with-bytes.md
+++ b/apps/docs/src/guide/encoding/working-with-bytes.md
@@ -6,7 +6,7 @@ This guide aims to give a high-level overview of how to work with bytes in the S
We know the sizes of all core types at compile time. They are the building blocks of the more complex types and are the most common types you will encounter.
-### Unsigned Integer (`u8` / `u16` / `u32` / `u64` / `u128` / `u256`)
+### Unsigned Integer (`u8` / `u16` / `u32` / `u64` / `u256`)
Each type will only contain the number of bits specified in the name. For example, a `u8` will contain 8 bits, and a `u256` will contain 256 bits and take up the exact property space with no additional padding.
diff --git a/apps/docs/src/guide/errors/index.md b/apps/docs/src/guide/errors/index.md
index f28a91ef8b4..136faaa0b66 100644
--- a/apps/docs/src/guide/errors/index.md
+++ b/apps/docs/src/guide/errors/index.md
@@ -18,6 +18,12 @@ When the arguments supplied to the function do not match the minimum required in
Check that the arguments supplied to the function match the required type.
+### `ABI_SPECIFICATION_INVALID`
+
+When the ABI specification provided is invalid.
+
+Check that the ABI specification is valid.
+
### `ACCOUNT_REQUIRED`
When an [`Account`](https://fuels-ts-docs-api.vercel.app/classes/_fuel_ts_account.Account.html) is required for an operation. This will usually be in the form of a [`Wallet`](../wallets/index.md).
@@ -324,6 +330,12 @@ In cases where the error hasn't been mapped yet, this code will be used.
If you believe you found a bug, please report the [issue](https://github.com/FuelLabs/fuels-ts/issues/new/choose) to the team.
+### `MATCHER_NOT_FOUND`
+
+When a matcher is not found for a given Sway type.
+
+Check that the Sway type is correct and exists in the ABI.
+
### `MAX_INPUTS_EXCEEDED`
When the number of transaction inputs exceeds the maximum limit allowed by the blockchain.
diff --git a/apps/docs/src/guide/fuels-cli/commands.md b/apps/docs/src/guide/fuels-cli/commands.md
index 97e936c676f..43bbf92925e 100644
--- a/apps/docs/src/guide/fuels-cli/commands.md
+++ b/apps/docs/src/guide/fuels-cli/commands.md
@@ -79,7 +79,7 @@ npx fuels@{{fuels}} build
```
1. Build all Sway programs under your `workspace` using `forc` [1](https://docs.fuel.network/docs/forc/commands/forc_build/)
-1. Generate types for them using `fuels-typegen` [2](#fuels-typegen)
+1. Generate types for them using `fuels typegen` [2](#fuels-typegen)
```console-vue
npx fuels@{{fuels}} build --deploy
@@ -167,9 +167,6 @@ npx fuels@{{fuels}} help typegen
Options:
-i, --inputs Input paths/globals to your Abi JSON files
-o, --output Directory path for generated files
- -c, --contract Generate types for Contracts [default]
- -s, --script Generate types for Scripts
- -p, --predicate Generate types for Predicates
-S, --silent Omit output messages
```
diff --git a/apps/docs/src/guide/fuels-cli/generating-types.md b/apps/docs/src/guide/fuels-cli/generating-types.md
index 6a42b1b530d..57523586e26 100644
--- a/apps/docs/src/guide/fuels-cli/generating-types.md
+++ b/apps/docs/src/guide/fuels-cli/generating-types.md
@@ -27,18 +27,15 @@ Generate Typescript from Sway ABI JSON files
Options:
-i, --inputs Input paths/globals to your ABI JSON files
-o, --output Directory path for generated files
- -c, --contract Generate types for Contracts [default]
- -s, --script Generate types for Scripts
- -p, --predicate Generate types for Predicates
-S, --silent Omit output messages
-h, --help Display help
```
-## Generating Types for Contracts
+## Generating Types
-You can generate types for a Sway contract using the command below:
+You can generate types for a Sway program using the command below:
-
+
```console
@@ -50,30 +47,15 @@ pnpm fuels typegen -i ./abis/*-abi.json -o ./types
-The path after the input flag `-i` should point to the file ending in `-abi.json` produced when the contract was built.
+The path after the input flag `-i` should point to the file ending in `-abi.json` produced when the Sway program was built.
-The path after the output flag `-o` will be the output directory for the generated types.
+- For scripts and predicates, you'll need the bytecode of the program to be in the same folder for the command to work.
+- For contracts, the command will work without the bytecode but the corresponding `ContractFactory` file won't be generated as factories need the bytecode to operate.
-You can omit the `--contract` option here since it's the default.
+The path after the output flag `-o` will be the output directory for the generated types.
-## Generating Types for Scripts
-
-To generate types for a Sway script, use the `--script` flag:
-
-```console
-pnpm fuels typegen -i ./abis/*-abi.json -o ./types --script
-```
-
-## Generating Types for Predicates
-
-To generate types for a Sway predicate, use the `--predicate` flag:
-
-```console
-pnpm fuels typegen -i ./abis/*-abi.json -o ./types --predicate
-```
-
---
See also:
diff --git a/apps/docs/src/guide/fuels-cli/using-generated-types.md b/apps/docs/src/guide/fuels-cli/using-generated-types.md
index 4702c2e0394..7e38ecf61c3 100644
--- a/apps/docs/src/guide/fuels-cli/using-generated-types.md
+++ b/apps/docs/src/guide/fuels-cli/using-generated-types.md
@@ -29,7 +29,7 @@ Typegen tries to resolve, auto-load, and embed the [Storage Slots](../contracts/
After generating types via:
```console
-pnpm fuels typegen -i ./abis/*-abi.json -o ./types --script
+pnpm fuels typegen -i ./abis/*-abi.json -o ./types
```
We can use these files like so:
@@ -41,7 +41,7 @@ We can use these files like so:
After generating types via:
```console
-pnpm fuels typegen -i ./abis/*-abi.json -o ./types --predicate
+pnpm fuels typegen -i ./abis/*-abi.json -o ./types
```
We can use these files like so:
diff --git a/apps/docs/src/guide/scripts/snippets/initialising-scripts.ts b/apps/docs/src/guide/scripts/snippets/initialising-scripts.ts
index 6516ba10849..bae140341c4 100644
--- a/apps/docs/src/guide/scripts/snippets/initialising-scripts.ts
+++ b/apps/docs/src/guide/scripts/snippets/initialising-scripts.ts
@@ -29,10 +29,9 @@ const scriptRequest = new ScriptRequest(
throw new Error('fail');
}
- const [decodedResult] = script.interface.functions.main.decodeOutput(
+ return script.interface.functions.main.decodeOutput(
scriptResult.returnReceipt.data
);
- return decodedResult;
}
);
// #endregion script-init
diff --git a/apps/docs/src/guide/types/snippets/enums/using-enums-of-enums-1.ts b/apps/docs/src/guide/types/snippets/enums/using-enums-of-enums-1.ts
index d3bc38d837d..2194c4fdc16 100644
--- a/apps/docs/src/guide/types/snippets/enums/using-enums-of-enums-1.ts
+++ b/apps/docs/src/guide/types/snippets/enums/using-enums-of-enums-1.ts
@@ -2,7 +2,7 @@ import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { EchoEnumFactory } from '../../../../typegend';
-import { UserErrorInput } from '../../../../typegend/contracts/EchoEnum';
+import { UserError } from '../../../../typegend/contracts/EchoEnum';
const provider = new Provider(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
@@ -10,7 +10,7 @@ const deploy = await EchoEnumFactory.deploy(wallet);
const { contract } = await deploy.waitForResult();
// #region snippet-1
-const enumParam = { UserError: UserErrorInput.InsufficientPermissions };
+const enumParam = { UserError: UserError.InsufficientPermissions };
const { value } = await contract.functions.echo_error_enum(enumParam).get();
diff --git a/apps/docs/src/guide/types/snippets/enums/using-enums-of-enums-2.ts b/apps/docs/src/guide/types/snippets/enums/using-enums-of-enums-2.ts
index d38469bd533..2c6318b0696 100644
--- a/apps/docs/src/guide/types/snippets/enums/using-enums-of-enums-2.ts
+++ b/apps/docs/src/guide/types/snippets/enums/using-enums-of-enums-2.ts
@@ -2,7 +2,7 @@ import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { EchoEnumFactory } from '../../../../typegend';
-import { StateErrorInput } from '../../../../typegend/contracts/EchoEnum';
+import { StateError } from '../../../../typegend/contracts/EchoEnum';
const provider = new Provider(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
@@ -10,7 +10,7 @@ const deploy = await EchoEnumFactory.deploy(wallet);
const { contract } = await deploy.waitForResult();
// #region snippet-1
-const enumParam = { StateError: StateErrorInput.Completed };
+const enumParam = { StateError: StateError.Completed };
const { value } = await contract.functions.echo_error_enum(enumParam).get();
diff --git a/apps/docs/src/guide/types/snippets/enums/using-sway-enums.ts b/apps/docs/src/guide/types/snippets/enums/using-sway-enums.ts
index 4c3b0233ace..93c7fc3d3c7 100644
--- a/apps/docs/src/guide/types/snippets/enums/using-sway-enums.ts
+++ b/apps/docs/src/guide/types/snippets/enums/using-sway-enums.ts
@@ -3,14 +3,14 @@ import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { EchoEnumFactory } from '../../../../typegend';
-import { StateErrorInput } from '../../../../typegend/contracts/EchoEnum';
+import { StateError } from '../../../../typegend/contracts/EchoEnum';
const provider = new Provider(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deploy = await EchoEnumFactory.deploy(wallet);
const { contract } = await deploy.waitForResult();
-const enumParam = StateErrorInput.Completed;
+const enumParam = StateError.Completed;
const { value } = await contract.functions
.echo_state_error_enum(enumParam)
diff --git a/internal/check-imports/package.json b/internal/check-imports/package.json
index 1b8d66bf044..a34d3cf4852 100644
--- a/internal/check-imports/package.json
+++ b/internal/check-imports/package.json
@@ -9,6 +9,7 @@
},
"license": "Apache-2.0",
"dependencies": {
+ "@fuel-ts/abi": "workspace:*",
"@fuel-ts/abi-coder": "workspace:*",
"@fuel-ts/abi-typegen": "workspace:*",
"@fuel-ts/address": "workspace:*",
diff --git a/internal/check-imports/src/imports.ts b/internal/check-imports/src/imports.ts
index 570ede6e6a6..3cd0aa73f9f 100644
--- a/internal/check-imports/src/imports.ts
+++ b/internal/check-imports/src/imports.ts
@@ -1,3 +1,4 @@
+import * as abi from '@fuel-ts/abi';
import * as abiCoder from '@fuel-ts/abi-coder';
import * as abiTypegen from '@fuel-ts/abi-typegen';
import * as account from '@fuel-ts/account';
@@ -20,6 +21,7 @@ import * as fuels from 'fuels';
const { log } = console;
log([
+ abi,
abiCoder,
abiTypegen,
address,
diff --git a/internal/check-imports/src/references.ts b/internal/check-imports/src/references.ts
index aa4fbbe098a..508c8f40655 100644
--- a/internal/check-imports/src/references.ts
+++ b/internal/check-imports/src/references.ts
@@ -1,3 +1,4 @@
+import { AbiCoder, encoding } from '@fuel-ts/abi';
import { Interface, StringCoder } from '@fuel-ts/abi-coder';
import { AbiTypeGen } from '@fuel-ts/abi-typegen';
import { runCliAction } from '@fuel-ts/abi-typegen/cli';
@@ -37,10 +38,20 @@ import {
arrayify,
hexlify,
createConfig,
+ AbiParser,
} from 'fuels';
const { log } = console;
+/**
+ * abi
+ */
+log(AbiCoder);
+log(encoding);
+log(encoding.v1.string);
+log(encoding.v1.string(8));
+log(AbiParser);
+
/**
* abi-coder
*/
diff --git a/packages/abi-coder/src/FunctionFragment.ts b/packages/abi-coder/src/FunctionFragment.ts
index 32087c21d19..9aabd92f021 100644
--- a/packages/abi-coder/src/FunctionFragment.ts
+++ b/packages/abi-coder/src/FunctionFragment.ts
@@ -75,7 +75,7 @@ export class FunctionFragment {
return new TupleCoder(coders).encode(argumentValues);
}
- decodeArguments(data: BytesLike) {
+ decodeArguments(data: BytesLike): unknown[] | undefined {
const bytes = arrayify(data);
const nonVoidInputs = findNonVoidInputs(this.jsonAbiOld, this.jsonFnOld.inputs);
diff --git a/packages/abi-coder/src/encoding/coders/B256Coder.test.ts b/packages/abi-coder/src/encoding/coders/B256Coder.test.ts
index b2c19ff78df..64df40ac176 100644
--- a/packages/abi-coder/src/encoding/coders/B256Coder.test.ts
+++ b/packages/abi-coder/src/encoding/coders/B256Coder.test.ts
@@ -18,7 +18,7 @@ describe('B256Coder', () => {
const coder = new B256Coder();
- it('should encode zero as a 256 bit hash string', () => {
+ it('should encode [zero] as a 256 bit hash string', () => {
const expected = B256_ZERO_ENCODED;
const actual = coder.encode(B256_ZERO_DECODED);
diff --git a/packages/abi-coder/src/encoding/coders/OptionCoder.test.ts b/packages/abi-coder/src/encoding/coders/OptionCoder.test.ts
index e43ca579211..e930ef911fe 100644
--- a/packages/abi-coder/src/encoding/coders/OptionCoder.test.ts
+++ b/packages/abi-coder/src/encoding/coders/OptionCoder.test.ts
@@ -8,36 +8,36 @@ import { VoidCoder } from './VoidCoder';
*/
describe('OptionCoder', () => {
const coder = new OptionCoder('std::option::Option', {
- Some: new NumberCoder('u8'),
None: new VoidCoder(),
+ Some: new NumberCoder('u8'),
});
describe('encode', () => {
it('should encode a Some value', () => {
const encoded = coder.encode(100);
- const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0, 100]);
+ const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1, 100]);
expect(encoded).toEqual(expected);
});
it('should encode a None value', () => {
const encoded = coder.encode(undefined);
- const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1]);
+ const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
expect(encoded).toEqual(expected);
});
it('should encode a None value [optional]', () => {
const encoded = coder.encode();
- const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1]);
+ const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
expect(encoded).toEqual(expected);
});
});
describe('decode', () => {
it('should decode a Some value', () => {
- const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0, 100]);
+ const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1, 100]);
const expected = [100, 9];
const decoded = coder.decode(input, 0);
@@ -46,7 +46,7 @@ describe('OptionCoder', () => {
});
it('should decode a None value', () => {
- const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1]);
+ const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
const expected = [undefined, 8];
const decoded = coder.decode(input, 0);
diff --git a/packages/abi-typegen/package.json b/packages/abi-typegen/package.json
index a0c7a220487..d4695347303 100644
--- a/packages/abi-typegen/package.json
+++ b/packages/abi-typegen/package.json
@@ -3,9 +3,6 @@
"version": "0.98.0",
"description": "Generates Typescript definitions from Sway ABI Json files",
"author": "Fuel Labs (https://fuel.network/)",
- "bin": {
- "fuels-typegen": "typegen.js"
- },
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
diff --git a/packages/abi-typegen/src/templates/contract/main.hbs b/packages/abi-typegen/src/templates/contract/main.hbs
index eef7efd30a2..ba606c1c975 100644
--- a/packages/abi-typegen/src/templates/contract/main.hbs
+++ b/packages/abi-typegen/src/templates/contract/main.hbs
@@ -1,12 +1,13 @@
{{header}}
-import { Contract, Interface } from "fuels";
+import { Contract, AbiCoder } from "fuels";
{{#if imports}}
import type {
Provider,
Account,
StorageSlot,
Address,
+ AbiSpecification,
{{#each imports}}
{{this}},
{{/each}}
@@ -53,18 +54,14 @@ export type {{capitalizedName}}Configurables = Partial<{
}>;
{{/if}}
-const abi = {{abiJsonString}};
+const abi: AbiSpecification = {{abiJsonString}};
const storageSlots: StorageSlot[] = {{storageSlotsJsonString}};
-export class {{capitalizedName}}Interface extends Interface {
- constructor() {
- super(abi);
- }
-
+export class {{capitalizedName}}AbiCoder extends AbiCoder {
declare functions: {
{{#each functionsFragments}}
- {{this}}: FunctionFragment;
+ {{this}}: AbiCoderFunction;
{{/each}}
};
}
@@ -73,7 +70,7 @@ export class {{capitalizedName}} extends Contract {
static readonly abi = abi;
static readonly storageSlots = storageSlots;
- declare interface: {{capitalizedName}}Interface;
+ declare interface: {{capitalizedName}}AbiCoder;
declare functions: {
{{#each functionsTypedefs}}
{{this}};
diff --git a/packages/abi-typegen/src/templates/contract/main.ts b/packages/abi-typegen/src/templates/contract/main.ts
index 759f3624f78..258296f8061 100644
--- a/packages/abi-typegen/src/templates/contract/main.ts
+++ b/packages/abi-typegen/src/templates/contract/main.ts
@@ -32,7 +32,7 @@ export function renderMainTemplate(params: { abi: Abi; versions: BinaryVersions
const { structs } = formatStructs({ types });
const { imports } = formatImports({
types,
- baseMembers: ['FunctionFragment', 'InvokeFunction'],
+ baseMembers: ['AbiCoderFunction', 'InvokeFunction'],
});
const { rawContents, storageSlotsContents } = params.abi;
diff --git a/packages/abi-typegen/src/templates/predicate/main.hbs b/packages/abi-typegen/src/templates/predicate/main.hbs
index 03cbaf76dd6..56f68ed8a79 100644
--- a/packages/abi-typegen/src/templates/predicate/main.hbs
+++ b/packages/abi-typegen/src/templates/predicate/main.hbs
@@ -57,7 +57,7 @@ export type {{capitalizedName}}Parameters = Omit<
'abi' | 'bytecode'
>;
-const abi = {{abiJsonString}};
+const abi: AbiSpecification = {{abiJsonString}};
const bytecode = decompressBytecode('{{compressedBytecode}}');
diff --git a/packages/abi-typegen/src/templates/predicate/main.ts b/packages/abi-typegen/src/templates/predicate/main.ts
index a2c928ecf94..01e0fee0134 100644
--- a/packages/abi-typegen/src/templates/predicate/main.ts
+++ b/packages/abi-typegen/src/templates/predicate/main.ts
@@ -34,7 +34,14 @@ export function renderMainTemplate(params: { abi: Abi; versions: BinaryVersions
const { structs } = formatStructs({ types });
const { imports } = formatImports({
types,
- baseMembers: ['Predicate', 'Provider', 'InputValue', 'PredicateParams', 'decompressBytecode'],
+ baseMembers: [
+ 'Predicate',
+ 'Provider',
+ 'InputValue',
+ 'PredicateParams',
+ 'decompressBytecode',
+ 'AbiSpecification',
+ ],
});
const { prefixedInputs: inputs, output } = func.attributes;
diff --git a/packages/abi-typegen/src/templates/script/main.hbs b/packages/abi-typegen/src/templates/script/main.hbs
index 225d540e9ca..403aec80ba3 100644
--- a/packages/abi-typegen/src/templates/script/main.hbs
+++ b/packages/abi-typegen/src/templates/script/main.hbs
@@ -51,7 +51,7 @@ export type {{capitalizedName}}Configurables = Partial<{
}>;
{{/if}}
-const abi = {{abiJsonString}};
+const abi: AbiSpecification = {{abiJsonString}};
const bytecode = decompressBytecode('{{compressedBytecode}}');
diff --git a/packages/abi-typegen/src/templates/script/main.ts b/packages/abi-typegen/src/templates/script/main.ts
index 36b76d2ce74..3ce3e15615a 100644
--- a/packages/abi-typegen/src/templates/script/main.ts
+++ b/packages/abi-typegen/src/templates/script/main.ts
@@ -34,7 +34,7 @@ export function renderMainTemplate(params: { abi: Abi; versions: BinaryVersions
const { structs } = formatStructs({ types });
const { imports } = formatImports({
types,
- baseMembers: ['Script', 'Account', 'decompressBytecode'],
+ baseMembers: ['Script', 'Account', 'decompressBytecode', 'AbiSpecification'],
});
const { prefixedInputs: inputs, output } = func.attributes;
diff --git a/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw b/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw
index 500f8639108..74af47c4e97 100644
--- a/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw
+++ b/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw
@@ -81,15 +81,15 @@ abi MyContract {
fn types_vector_geo(x: Vec) -> Vec;
fn types_vector_option(x: Vec) -> Vec;
fn types_option(x: Option) -> Option;
- fn types_option_geo(x: Option) -> Option;
+ fn types_option_struct(x: Option) -> Option;
fn types_evm_address(x: EvmAddress) -> EvmAddress;
fn types_bytes(x: Bytes) -> Bytes;
fn types_raw_slice(x: raw_slice) -> raw_slice;
fn types_str_slice(x: str) -> str;
fn types_std_string(x: String) -> String;
fn types_result(x: Result) -> Result;
- fn type_address(x: Address) -> Address;
- fn type_contract_id(x: ContractId) -> ContractId;
+ fn types_address(x: Address) -> Address;
+ fn types_contract_id(x: ContractId) -> ContractId;
fn type_identity(x: Identity) -> Identity;
fn type_external_struct(x: ExternalStruct) -> ExternalStruct;
fn type_external_enum(x: ExternalEnum) -> ExternalEnum;
@@ -182,7 +182,7 @@ impl MyContract for Contract {
fn types_option(x: Option) -> Option {
x
}
- fn types_option_geo(x: Option) -> Option {
+ fn types_option_struct(x: Option) -> Option {
x
}
fn types_evm_address(x: EvmAddress) -> EvmAddress {
@@ -211,10 +211,10 @@ impl MyContract for Contract {
Err(MyContractError::DivisionByZero) => Err(__to_str_array("DivisError")),
}
}
- fn type_address(x: Address) -> Address {
+ fn types_address(x: Address) -> Address {
x
}
- fn type_contract_id(x: ContractId) -> ContractId {
+ fn types_contract_id(x: ContractId) -> ContractId {
x
}
fn type_identity(x: Identity) -> Identity {
diff --git a/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs b/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs
index 992052611ff..9e76887d9fe 100644
--- a/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs
+++ b/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs
@@ -10,14 +10,15 @@
Fuel-Core version: 33.33.33
*/
-import { Contract, Interface } from "fuels";
+import { Contract, AbiCoder } from "fuels";
import type {
Provider,
Account,
StorageSlot,
Address,
+ AbiSpecification,
+ AbiCoderFunction,
BigNumberish,
- FunctionFragment,
InvokeFunction,
} from 'fuels';
@@ -32,7 +33,7 @@ export type MyContractConfigurables = Partial<{
A_GENERIC_STRUCT: GenericStructInput, BigNumberish>;
}>;
-const abi = {
+const abi: AbiSpecification = {
"programType": "contract",
"specVersion": "1",
"encodingVersion": "1",
@@ -177,13 +178,9 @@ const abi = {
const storageSlots: StorageSlot[] = [];
-export class MyContractInterface extends Interface {
- constructor() {
- super(abi);
- }
-
+export class MyContractAbiCoder extends AbiCoder {
declare functions: {
- main: FunctionFragment;
+ main: AbiCoderFunction;
};
}
@@ -191,7 +188,7 @@ export class MyContract extends Contract {
static readonly abi = abi;
static readonly storageSlots = storageSlots;
- declare interface: MyContractInterface;
+ declare interface: MyContractAbiCoder;
declare functions: {
main: InvokeFunction<[x: string, y: string], boolean>;
};
diff --git a/packages/abi-typegen/test/fixtures/templates/contract/main.hbs b/packages/abi-typegen/test/fixtures/templates/contract/main.hbs
index a0350d966f7..cacdab706d7 100644
--- a/packages/abi-typegen/test/fixtures/templates/contract/main.hbs
+++ b/packages/abi-typegen/test/fixtures/templates/contract/main.hbs
@@ -10,17 +10,18 @@
Fuel-Core version: 33.33.33
*/
-import { Contract, Interface } from "fuels";
+import { Contract, AbiCoder } from "fuels";
import type {
Provider,
Account,
StorageSlot,
Address,
+ AbiSpecification,
+ AbiCoderFunction,
BigNumberish,
BN,
Bytes,
EvmAddress,
- FunctionFragment,
InvokeFunction,
RawSlice,
StdString,
@@ -55,7 +56,7 @@ export type MyStructOutput = { x: number, y: number, state: MyEnumOutput };
export type StructWithMultiOptionInput = { multiple: [Option, Option, Option, Option, Option] };
export type StructWithMultiOptionOutput = { multiple: [Option, Option, Option, Option, Option] };
-const abi = {
+const abi: AbiSpecification = {
"programType": "contract",
"specVersion": "1",
"encodingVersion": "1",
@@ -685,28 +686,6 @@ const abi = {
"output": "a95e1fcceb1451b8a76471f593f66c4a52ca04bde3c227c746ad7aaf988de5c6",
"attributes": null
},
- {
- "inputs": [
- {
- "name": "x",
- "concreteTypeId": "f597b637c3b0f588fb8d7086c6f4735caa3122b85f0423b82e489f9bb58e2308"
- }
- ],
- "name": "type_address",
- "output": "f597b637c3b0f588fb8d7086c6f4735caa3122b85f0423b82e489f9bb58e2308",
- "attributes": null
- },
- {
- "inputs": [
- {
- "name": "x",
- "concreteTypeId": "29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54"
- }
- ],
- "name": "type_contract_id",
- "output": "29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54",
- "attributes": null
- },
{
"inputs": [
{
@@ -740,6 +719,17 @@ const abi = {
"output": "ab7cd04e05be58e3fc15d424c2c4a57f824a2a2d97d67252440a3925ebdc1335",
"attributes": null
},
+ {
+ "inputs": [
+ {
+ "name": "x",
+ "concreteTypeId": "f597b637c3b0f588fb8d7086c6f4735caa3122b85f0423b82e489f9bb58e2308"
+ }
+ ],
+ "name": "types_address",
+ "output": "f597b637c3b0f588fb8d7086c6f4735caa3122b85f0423b82e489f9bb58e2308",
+ "attributes": null
+ },
{
"inputs": [
{
@@ -806,6 +796,17 @@ const abi = {
"output": "cdd87b7d12fe505416570c294c884bca819364863efe3bf539245fa18515fbbb",
"attributes": null
},
+ {
+ "inputs": [
+ {
+ "name": "x",
+ "concreteTypeId": "29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54"
+ }
+ ],
+ "name": "types_contract_id",
+ "output": "29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54",
+ "attributes": null
+ },
{
"inputs": [
{
@@ -905,7 +906,7 @@ const abi = {
"concreteTypeId": "3597e0782bd4dbaf5c8025b40ff3a325845ee34caa713a6d664bda034a31d02a"
}
],
- "name": "types_option_geo",
+ "name": "types_option_struct",
"output": "3597e0782bd4dbaf5c8025b40ff3a325845ee34caa713a6d664bda034a31d02a",
"attributes": null
},
@@ -1150,52 +1151,48 @@ const abi = {
const storageSlots: StorageSlot[] = [];
-export class MyContractInterface extends Interface {
- constructor() {
- super(abi);
- }
-
+export class MyContractAbiCoder extends AbiCoder {
declare functions: {
- alias_types_tuple_with_native_types: FunctionFragment;
- type_address: FunctionFragment;
- type_contract_id: FunctionFragment;
- type_external_enum: FunctionFragment;
- type_external_struct: FunctionFragment;
- type_identity: FunctionFragment;
- types_array: FunctionFragment;
- types_asset_id: FunctionFragment;
- types_b256: FunctionFragment;
- types_b512: FunctionFragment;
- types_bool: FunctionFragment;
- types_bytes: FunctionFragment;
- types_empty: FunctionFragment;
- types_empty_then_value: FunctionFragment;
- types_enum: FunctionFragment;
- types_enum_with_vector: FunctionFragment;
- types_evm_address: FunctionFragment;
- types_generic_enum: FunctionFragment;
- types_generic_struct: FunctionFragment;
- types_option: FunctionFragment;
- types_option_geo: FunctionFragment;
- types_raw_slice: FunctionFragment;
- types_result: FunctionFragment;
- types_std_string: FunctionFragment;
- types_str: FunctionFragment;
- types_str_slice: FunctionFragment;
- types_struct: FunctionFragment;
- types_tuple: FunctionFragment;
- types_tuple_with_native_types: FunctionFragment;
- types_u16: FunctionFragment;
- types_u256: FunctionFragment;
- types_u32: FunctionFragment;
- types_u64: FunctionFragment;
- types_u8: FunctionFragment;
- types_value_then_empty: FunctionFragment;
- types_value_then_empty_then_value: FunctionFragment;
- types_value_then_value_then_empty_then_empty: FunctionFragment;
- types_vector_geo: FunctionFragment;
- types_vector_option: FunctionFragment;
- types_vector_u8: FunctionFragment;
+ alias_types_tuple_with_native_types: AbiCoderFunction;
+ type_external_enum: AbiCoderFunction;
+ type_external_struct: AbiCoderFunction;
+ type_identity: AbiCoderFunction;
+ types_address: AbiCoderFunction;
+ types_array: AbiCoderFunction;
+ types_asset_id: AbiCoderFunction;
+ types_b256: AbiCoderFunction;
+ types_b512: AbiCoderFunction;
+ types_bool: AbiCoderFunction;
+ types_bytes: AbiCoderFunction;
+ types_contract_id: AbiCoderFunction;
+ types_empty: AbiCoderFunction;
+ types_empty_then_value: AbiCoderFunction;
+ types_enum: AbiCoderFunction;
+ types_enum_with_vector: AbiCoderFunction;
+ types_evm_address: AbiCoderFunction;
+ types_generic_enum: AbiCoderFunction;
+ types_generic_struct: AbiCoderFunction;
+ types_option: AbiCoderFunction;
+ types_option_struct: AbiCoderFunction;
+ types_raw_slice: AbiCoderFunction;
+ types_result: AbiCoderFunction;
+ types_std_string: AbiCoderFunction;
+ types_str: AbiCoderFunction;
+ types_str_slice: AbiCoderFunction;
+ types_struct: AbiCoderFunction;
+ types_tuple: AbiCoderFunction;
+ types_tuple_with_native_types: AbiCoderFunction;
+ types_u16: AbiCoderFunction;
+ types_u256: AbiCoderFunction;
+ types_u32: AbiCoderFunction;
+ types_u64: AbiCoderFunction;
+ types_u8: AbiCoderFunction;
+ types_value_then_empty: AbiCoderFunction;
+ types_value_then_empty_then_value: AbiCoderFunction;
+ types_value_then_value_then_empty_then_empty: AbiCoderFunction;
+ types_vector_geo: AbiCoderFunction;
+ types_vector_option: AbiCoderFunction;
+ types_vector_u8: AbiCoderFunction;
};
}
@@ -1203,20 +1200,20 @@ export class MyContract extends Contract {
static readonly abi = abi;
static readonly storageSlots = storageSlots;
- declare interface: MyContractInterface;
+ declare interface: MyContractAbiCoder;
declare functions: {
alias_types_tuple_with_native_types: InvokeFunction<[x: [AssetIdInput, AssetIdInput, boolean]], [AssetIdOutput, AssetIdOutput, boolean]>;
- type_address: InvokeFunction<[x: AddressInput], AddressOutput>;
- type_contract_id: InvokeFunction<[x: ContractIdInput], ContractIdOutput>;
type_external_enum: InvokeFunction<[x: ExternalEnumInput], ExternalEnumOutput>;
type_external_struct: InvokeFunction<[x: ExternalStructInput], ExternalStructOutput>;
type_identity: InvokeFunction<[x: IdentityInput], IdentityOutput>;
+ types_address: InvokeFunction<[x: AddressInput], AddressOutput>;
types_array: InvokeFunction<[x: [BigNumberish, BigNumberish, BigNumberish]], [number, number, number]>;
types_asset_id: InvokeFunction<[x: AssetIdInput], AssetIdOutput>;
types_b256: InvokeFunction<[x: string], string>;
types_b512: InvokeFunction<[x: string], string>;
types_bool: InvokeFunction<[x: boolean], boolean>;
types_bytes: InvokeFunction<[x: Bytes], Bytes>;
+ types_contract_id: InvokeFunction<[x: ContractIdInput], ContractIdOutput>;
types_empty: InvokeFunction<[x?: undefined], void>;
types_empty_then_value: InvokeFunction<[x: undefined, y: BigNumberish], void>;
types_enum: InvokeFunction<[x: MyEnumInput], MyEnumOutput>;
@@ -1225,7 +1222,7 @@ export class MyContract extends Contract {
types_generic_enum: InvokeFunction<[x: GenericEnumInput], GenericEnumOutput>;
types_generic_struct: InvokeFunction<[x: GenericStructWithEnumInput], GenericStructWithEnumOutput>;
types_option: InvokeFunction<[x?: Option], Option>;
- types_option_geo: InvokeFunction<[x?: Option], Option>;
+ types_option_struct: InvokeFunction<[x?: Option], Option>;
types_raw_slice: InvokeFunction<[x: RawSlice], RawSlice>;
types_result: InvokeFunction<[x: Result], Result>;
types_std_string: InvokeFunction<[x: StdString], StdString>;
diff --git a/packages/abi-typegen/test/fixtures/templates/predicate-with-configurable/main.hbs b/packages/abi-typegen/test/fixtures/templates/predicate-with-configurable/main.hbs
index c40afd5a99d..4fb4f6f1738 100644
--- a/packages/abi-typegen/test/fixtures/templates/predicate-with-configurable/main.hbs
+++ b/packages/abi-typegen/test/fixtures/templates/predicate-with-configurable/main.hbs
@@ -11,6 +11,7 @@
*/
import {
+ AbiSpecification,
BigNumberish,
decompressBytecode,
InputValue,
@@ -31,7 +32,7 @@ export type MyPredicateParameters = Omit<
'abi' | 'bytecode'
>;
-const abi = {
+const abi: AbiSpecification = {
"programType": "predicate",
"specVersion": "1",
"encodingVersion": "1",
diff --git a/packages/abi-typegen/test/fixtures/templates/predicate/main.hbs b/packages/abi-typegen/test/fixtures/templates/predicate/main.hbs
index 8eaed3fbf41..fe83ed8aba1 100644
--- a/packages/abi-typegen/test/fixtures/templates/predicate/main.hbs
+++ b/packages/abi-typegen/test/fixtures/templates/predicate/main.hbs
@@ -11,6 +11,7 @@
*/
import {
+ AbiSpecification,
BigNumberish,
BN,
decompressBytecode,
@@ -39,7 +40,7 @@ export type MyPredicateParameters = Omit<
'abi' | 'bytecode'
>;
-const abi = {
+const abi: AbiSpecification = {
"programType": "predicate",
"specVersion": "1",
"encodingVersion": "1",
diff --git a/packages/abi-typegen/test/fixtures/templates/script-with-configurable/main.hbs b/packages/abi-typegen/test/fixtures/templates/script-with-configurable/main.hbs
index 34c9a4f1722..4dd791da2b7 100644
--- a/packages/abi-typegen/test/fixtures/templates/script-with-configurable/main.hbs
+++ b/packages/abi-typegen/test/fixtures/templates/script-with-configurable/main.hbs
@@ -11,6 +11,7 @@
*/
import {
+ AbiSpecification,
Account,
BigNumberish,
decompressBytecode,
@@ -27,7 +28,7 @@ export type MyScriptConfigurables = Partial<{
SHOULD_RETURN: boolean;
}>;
-const abi = {
+const abi: AbiSpecification = {
"programType": "script",
"specVersion": "1",
"encodingVersion": "1",
diff --git a/packages/abi-typegen/test/fixtures/templates/script/main.hbs b/packages/abi-typegen/test/fixtures/templates/script/main.hbs
index aa93e951140..0d08fdbfabd 100644
--- a/packages/abi-typegen/test/fixtures/templates/script/main.hbs
+++ b/packages/abi-typegen/test/fixtures/templates/script/main.hbs
@@ -11,6 +11,7 @@
*/
import {
+ AbiSpecification,
Account,
BigNumberish,
BN,
@@ -31,7 +32,7 @@ export type ScoreOutput = { user: number, points: number };
export type MyScriptInputs = [vec: Vec, enm: MyGenericEnumInput, opt: Option, res: Result, BigNumberish>];
export type MyScriptOutput = boolean;
-const abi = {
+const abi: AbiSpecification = {
"programType": "script",
"specVersion": "1",
"encodingVersion": "1",
diff --git a/packages/abi/LICENSE b/packages/abi/LICENSE
new file mode 100644
index 00000000000..261eeb9e9f8
--- /dev/null
+++ b/packages/abi/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/abi/README.md b/packages/abi/README.md
new file mode 100644
index 00000000000..ced0388cf00
--- /dev/null
+++ b/packages/abi/README.md
@@ -0,0 +1,3 @@
+## @fuel-ts/abi
+
+TODO write up README
diff --git a/packages/abi/package.json b/packages/abi/package.json
new file mode 100644
index 00000000000..077818f53cd
--- /dev/null
+++ b/packages/abi/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@fuel-ts/abi",
+ "private": true,
+ "version": "0.0.0",
+ "description": "",
+ "author": "Fuel Labs (https://fuel.network/)",
+ "license": "Apache-2.0",
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "engines": {
+ "node": "^18.20.3 || ^20.0.0 || ^22.0.0"
+ },
+ "bin": {
+ "fuels-typegen": "typegen.js"
+ },
+ "exports": {
+ ".": {
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs",
+ "types": "./dist/index.d.ts"
+ },
+ "./cli": {
+ "types": "./dist/cli.d.ts",
+ "require": "./dist/cli.js",
+ "import": "./dist/cli.mjs"
+ }
+ },
+ "typesVersions": {
+ "*": {
+ "cli": [
+ "./dist/cli.d.ts"
+ ]
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup",
+ "postbuild": "tsx ../../scripts/postbuild.ts"
+ },
+ "dependencies": {
+ "@fuel-ts/crypto": "workspace:*",
+ "@fuel-ts/errors": "workspace:*",
+ "@fuel-ts/hasher": "workspace:*",
+ "@fuel-ts/math": "workspace:*",
+ "@fuel-ts/utils": "workspace:*",
+ "type-fest": "4.26.1",
+ "@fuel-ts/versions": "workspace:*",
+ "handlebars": "4.7.8",
+ "commander": "12.1.0",
+ "glob": "10.4.5"
+ },
+ "devDependencies": {}
+}
diff --git a/packages/abi/src/bin.ts b/packages/abi/src/bin.ts
new file mode 100755
index 00000000000..4d4c3bed880
--- /dev/null
+++ b/packages/abi/src/bin.ts
@@ -0,0 +1,15 @@
+import { getBinaryVersions } from '@fuel-ts/versions/cli';
+import { Command } from 'commander';
+
+import { configureTypegenCliOptions } from './cli';
+
+const program = new Command();
+
+program.name('fuels-typegen');
+program.version(getBinaryVersions().FUELS);
+program.usage(`-i ../out/*-abi.json -o ./generated/`);
+program.option('-S, --silent', 'Omit output messages', false);
+
+configureTypegenCliOptions(program);
+
+program.parse(process.argv);
diff --git a/packages/abi/src/cli.ts b/packages/abi/src/cli.ts
new file mode 100644
index 00000000000..c37108bd746
--- /dev/null
+++ b/packages/abi/src/cli.ts
@@ -0,0 +1 @@
+export * from './gen/cli';
diff --git a/packages/abi/src/coder/AbiCoder.ts b/packages/abi/src/coder/AbiCoder.ts
new file mode 100644
index 00000000000..5073916bd4a
--- /dev/null
+++ b/packages/abi/src/coder/AbiCoder.ts
@@ -0,0 +1,148 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import type { Abi, AbiSpecification } from '../parser';
+import { AbiParser } from '../parser';
+
+import type {
+ AbiCoderConfigurable,
+ AbiCoderFunction,
+ AbiCoderLog,
+ AbiCoderType,
+} from './abi-coder-types';
+import type { AbiEncoding } from './encoding';
+import { encoding } from './encoding';
+import { createConfigurable } from './utils/createConfigurable';
+import { createFunction } from './utils/createFunction';
+import { createLog } from './utils/createLog';
+import { createType } from './utils/createType';
+
+export class AbiCoder {
+ public readonly abi: Abi;
+ public readonly specification: AbiSpecification;
+
+ private readonly encoding: AbiEncoding;
+ private readonly functionLookup: Map;
+ public readonly functions: Record;
+ public readonly configurables: Record;
+ public readonly logs: Record;
+
+ /**
+ * Create an instance of `AbiCoder` from an ABI specification
+ *
+ * @param specification - The ABI specification
+ */
+ public constructor(specification: AbiSpecification) {
+ this.abi = AbiParser.parse(specification);
+ this.specification = specification;
+ this.encoding = encoding.fromVersion(this.abi.encodingVersion);
+
+ const { functions, configurables, loggedTypes } = this.abi;
+ this.functionLookup = new Map();
+ this.functions = Object.fromEntries(
+ functions.map((fn) => {
+ const func = createFunction(fn, this.encoding);
+ this.functionLookup.set(fn.name, func);
+ this.functionLookup.set(func.signature, func);
+ this.functionLookup.set(func.selector, func);
+ return [fn.name, func];
+ })
+ );
+ this.configurables = Object.fromEntries(
+ configurables.map((conf) => [conf.name, createConfigurable(conf, this.encoding)])
+ );
+ this.logs = Object.fromEntries(
+ loggedTypes.map((log) => [log.logId, createLog(log, this.encoding)])
+ );
+ }
+
+ /**
+ * Create an instance of `AbiCoder` from an ABI specification
+ *
+ * @param abi - The ABI specification
+ * @returns The `AbiCoder` instance
+ */
+ static fromAbi(abi: AbiSpecification): AbiCoder {
+ return new AbiCoder(abi);
+ }
+
+ /**
+ * Get a function by its name, signature or selector
+ *
+ * @param nameOrSignatureOrSelector - The function name, signature or selector
+ * @returns The function
+ *
+ * @throws FuelError - when the function is not found
+ */
+ public getFunction(nameOrSignatureOrSelector: string): AbiCoderFunction {
+ const fn = this.functionLookup.get(nameOrSignatureOrSelector);
+
+ if (fn === undefined) {
+ throw new FuelError(
+ FuelError.CODES.FUNCTION_NOT_FOUND,
+ `Unable to find function with the name or signature or selector of "${nameOrSignatureOrSelector}".`
+ );
+ }
+
+ return fn;
+ }
+
+ /**
+ * Get a configurable by its name
+ *
+ * @param name - The configurable name
+ * @returns The configurable
+ *
+ * @throws FuelError - when the configurable is not found
+ */
+ public getConfigurable(name: string): AbiCoderConfigurable {
+ const configurable = this.configurables[name];
+ if (configurable === undefined) {
+ throw new FuelError(
+ FuelError.CODES.CONFIGURABLE_NOT_FOUND,
+ `Configurable with name '${name}' doesn't exist in the ABI.`
+ );
+ }
+
+ return configurable;
+ }
+
+ /**
+ * Get a log by its log ID
+ *
+ * @param logId - The log ID
+ * @returns The log
+ *
+ * @throws FuelError - when the log is not found
+ */
+ public getLog(logId: string): AbiCoderLog {
+ const log = this.logs[logId];
+ if (log === undefined) {
+ throw new FuelError(
+ FuelError.CODES.LOG_TYPE_NOT_FOUND,
+ `Log type with logId '${logId}' doesn't exist in the ABI.`
+ );
+ }
+
+ return log;
+ }
+
+ /**
+ * Get a type by its concrete type ID
+ *
+ * @param concreteTypeId - The concrete type ID
+ * @returns The type
+ *
+ * @throws FuelError - when the type is not found
+ */
+ public getType(concreteTypeId: string): AbiCoderType {
+ const type = this.abi.concreteTypes.find((t) => t.concreteTypeId === concreteTypeId);
+ if (type === undefined) {
+ throw new FuelError(
+ FuelError.CODES.TYPE_NOT_FOUND,
+ `Type with concreteTypeId '${concreteTypeId}' doesn't exist in the ABI.`
+ );
+ }
+
+ return createType(type, this.encoding);
+ }
+}
diff --git a/packages/abi/src/coder/abi-coder-types.ts b/packages/abi/src/coder/abi-coder-types.ts
new file mode 100644
index 00000000000..9c5bb1e24e6
--- /dev/null
+++ b/packages/abi/src/coder/abi-coder-types.ts
@@ -0,0 +1,37 @@
+import type { BytesLike } from '@fuel-ts/utils';
+
+import type { AbiConfigurable, AbiFunction, AbiLoggedType } from '../parser';
+
+import type { DecodedValue, InputValue } from './encoding/encoding-types';
+
+export interface AbiCoderFunction {
+ name: AbiFunction['name'];
+ inputs: AbiFunction['inputs'];
+ signature: string;
+ selector: string;
+ selectorBytes: Uint8Array;
+ attributes: AbiFunction['attributes'];
+ isReadOnly: () => boolean;
+ encodeArguments: (values: InputValue[]) => Uint8Array;
+ decodeArguments: (data: BytesLike) => DecodedValue[];
+ encodeOutput: (value: InputValue) => Uint8Array;
+ decodeOutput: (data: BytesLike) => DecodedValue;
+}
+
+export interface AbiCoderConfigurable {
+ name: AbiConfigurable['name'];
+ offset: AbiConfigurable['offset'];
+ encode: (values: InputValue) => Uint8Array;
+ decode: (data: BytesLike) => DecodedValue;
+}
+
+export interface AbiCoderLog {
+ logId: AbiLoggedType['logId'];
+ encode: (values: InputValue) => Uint8Array;
+ decode: (data: BytesLike) => DecodedValue;
+}
+
+export interface AbiCoderType {
+ encode: (value: InputValue) => Uint8Array;
+ decode: (data: BytesLike) => DecodedValue;
+}
diff --git a/packages/abi/src/coder/constants.ts b/packages/abi/src/coder/constants.ts
new file mode 100644
index 00000000000..96eb8e36ac6
--- /dev/null
+++ b/packages/abi/src/coder/constants.ts
@@ -0,0 +1,28 @@
+/**
+ * Property space and config constants
+ */
+export const WORD_SIZE = 8;
+export const BYTES_32 = 32;
+export const UTXO_ID_LEN = BYTES_32 + 2;
+export const ASSET_ID_LEN = BYTES_32;
+export const CONTRACT_ID_LEN = BYTES_32;
+export const MAX_BYTES = 2 ** 32 - 1; // Max u32
+
+export const calculateVmTxMemory = ({ maxInputs }: { maxInputs: number }) =>
+ BYTES_32 + // Tx ID
+ ASSET_ID_LEN + // Base asset ID
+ // Asset ID/Balance coin input pairs
+ maxInputs * (ASSET_ID_LEN + WORD_SIZE) +
+ WORD_SIZE; // Tx size
+
+// SCRIPT_FIXED_SIZE = 104
+export const SCRIPT_FIXED_SIZE =
+ WORD_SIZE + // Identifier
+ WORD_SIZE + // Gas limit
+ WORD_SIZE + // Script size
+ WORD_SIZE + // Script data size
+ WORD_SIZE + // Policies
+ WORD_SIZE + // Inputs size
+ WORD_SIZE + // Outputs size
+ WORD_SIZE + // Witnesses size
+ BYTES_32; // Receipts root
diff --git a/packages/abi/src/coder/encoding/encoding-constants.ts b/packages/abi/src/coder/encoding/encoding-constants.ts
new file mode 100644
index 00000000000..2fa4621b5fe
--- /dev/null
+++ b/packages/abi/src/coder/encoding/encoding-constants.ts
@@ -0,0 +1,43 @@
+export const BOOL_TYPE = 'bool';
+export const U8_TYPE = 'u8';
+export const U16_TYPE = 'u16';
+export const U32_TYPE = 'u32';
+export const U64_TYPE = 'u64';
+export const U256_TYPE = 'u256';
+export const B256_TYPE = 'b256';
+export const B512_TYPE = 'b512';
+export const VOID_TYPE = 'void';
+export const BYTES_TYPE = 'bytes';
+export const RAW_SLICE_TYPE = 'raw slice';
+export const STD_STRING_TYPE = 'std string';
+export const STR_SLICE_TYPE = 'str';
+export const VECTOR_TYPE = 'vector';
+export const STRING_TYPE = 'string';
+export const STRUCT_TYPE = 'struct';
+export const ARRAY_TYPE = 'array';
+export const ENUM_TYPE = 'enum';
+export const OPTION_TYPE = 'option';
+export const TUPLE_TYPE = 'tuple';
+
+export const ENCODING_TYPES = [
+ U8_TYPE,
+ U16_TYPE,
+ U32_TYPE,
+ U64_TYPE,
+ U256_TYPE,
+ BOOL_TYPE,
+ B256_TYPE,
+ B512_TYPE,
+ VOID_TYPE,
+ BYTES_TYPE,
+ RAW_SLICE_TYPE,
+ STD_STRING_TYPE,
+ STR_SLICE_TYPE,
+ VECTOR_TYPE,
+ STRING_TYPE,
+ STRUCT_TYPE,
+ ARRAY_TYPE,
+ ENUM_TYPE,
+ OPTION_TYPE,
+ TUPLE_TYPE,
+] as const;
diff --git a/packages/abi/src/coder/encoding/encoding-types.ts b/packages/abi/src/coder/encoding/encoding-types.ts
new file mode 100644
index 00000000000..5a9e6a1512a
--- /dev/null
+++ b/packages/abi/src/coder/encoding/encoding-types.ts
@@ -0,0 +1,112 @@
+import type { BN } from '@fuel-ts/math';
+import type { BytesLike } from '@fuel-ts/utils';
+
+import type { AbiConcreteType, AbiTypeComponent } from '../../parser';
+
+import type { coders as v1 } from './v1';
+
+export type SupportedCodersV1 = typeof v1;
+
+/**
+ * All the supported coders, across all versions.
+ */
+export type SupportedCoders = SupportedCodersV1;
+export type SupportedCoder = SupportedCoders[keyof SupportedCoders];
+
+/**
+ * A primitive type value
+ */
+export type Primitive = string | number | boolean;
+
+/**
+ * An option type value
+ */
+export type Option = T | undefined;
+
+export type Bytes = Uint8Array | number[];
+export type RawSlice = Uint8Array | number[];
+export type StdString = string;
+export type StrSlice = string;
+
+/**
+ * The type of value you can provide to `Coder.encode`
+ */
+export type InputValue =
+ | Primitive
+ | BN
+ | Option
+ | BytesLike
+ | InputValue[]
+ | { [key: string]: InputValue }
+ | Record;
+
+/**
+ * The type of value you can get from `Coder.decode`
+ */
+export type DecodedValue =
+ | Primitive
+ | DecodedValue[]
+ | { [key: string]: DecodedValue }
+ | Record;
+
+/**
+ * Used to infer the encoded and decoded values of a coder.
+ */
+export type TypesOfCoder =
+ TCoder extends Coder ? { Input: TInput; Decoded: TDecoded } : never;
+
+export abstract class Coder {
+ /**
+ * The type of the coder
+ */
+ abstract type: string;
+
+ /**
+ * Encode a value.
+ *
+ * @param value - The value to encode.
+ * @returns The encoded value.
+ *
+ * @throws FuelError - with a ENCODE_ERROR code
+ */
+ abstract encode(value: TEncoded): Uint8Array;
+
+ /**
+ * Decode a value.
+ *
+ * @param value - The value to decode.
+ * @param offset - The offset to start decoding from.
+ * @returns A tuple with the [decoded value, offset of the final decoded]
+ *
+ * @throws FuelError - with a DECODE_ERROR code
+ */
+ abstract decode(value: Uint8Array, offset?: number): [TDecoded, number];
+}
+
+export type CoderFactoryParameters = {
+ name?: string;
+ type: AbiConcreteType | AbiTypeComponent['type'];
+};
+export type CoderFactory = (
+ params: CoderFactoryParameters,
+ factory: CoderFactory
+) => TCoder;
+
+type CurrentEncoding = typeof v1;
+
+export type Encoding = CurrentEncoding & {
+ v1: typeof v1;
+ fromVersion: (version: string) => AbiEncoding;
+};
+
+export type AbiEncoding = {
+ coders: SupportedCoders;
+
+ /**
+ * Get a coder for a given type and name
+ *
+ * @param opts - The options object
+ * @returns A coder
+ */
+ getCoder: (params: CoderFactoryParameters) => Coder;
+};
diff --git a/packages/abi/src/coder/encoding/encoding.ts b/packages/abi/src/coder/encoding/encoding.ts
new file mode 100644
index 00000000000..de0090c25f5
--- /dev/null
+++ b/packages/abi/src/coder/encoding/encoding.ts
@@ -0,0 +1,74 @@
+import { FuelError } from '@fuel-ts/errors';
+import { assertUnreachable } from '@fuel-ts/utils';
+
+import type { Matcher } from '../../matchers/sway-type-matchers';
+
+import type {
+ AbiEncoding,
+ Coder,
+ CoderFactoryParameters,
+ Encoding,
+ SupportedCoder,
+ SupportedCoders,
+} from './encoding-types';
+import * as v1 from './v1';
+
+interface SupportedEncoding {
+ coders: SupportedCoders;
+ matcher: Matcher;
+}
+
+const currentEncoding = v1.coders;
+const supportedEncodings: Record = {
+ '1': { coders: v1.coders, matcher: v1.matcher },
+};
+
+export const encoding: Encoding = {
+ ...currentEncoding,
+ v1: v1.coders,
+ fromVersion: (version: string): AbiEncoding => {
+ const supportedEncoding = supportedEncodings[version as keyof typeof supportedEncodings];
+ if (!supportedEncoding) {
+ throw new FuelError(
+ FuelError.CODES.UNSUPPORTED_ENCODING_VERSION,
+ `Unsupported encoding version "${version}"`
+ );
+ }
+
+ const getCoder = (opts: CoderFactoryParameters) => {
+ const { type, name } = opts;
+ let coder: SupportedCoder | undefined;
+ try {
+ coder = supportedEncoding.matcher(type);
+ } catch (error) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `Unsupported coder type "${type.swayType}" for element "${name}"`
+ );
+ }
+
+ if (!coder) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `Unsupported coder type "${type.swayType}" for element "${name}"`
+ );
+ }
+
+ if (typeof coder === 'object') {
+ return coder as Coder;
+ }
+
+ if (typeof coder === 'function') {
+ return coder.factory(opts, getCoder);
+ }
+
+ // Coders should always be either an object or function
+ return assertUnreachable(coder as never);
+ };
+
+ return {
+ coders: supportedEncoding.coders,
+ getCoder,
+ };
+ },
+};
diff --git a/packages/abi/src/coder/encoding/index.ts b/packages/abi/src/coder/encoding/index.ts
new file mode 100644
index 00000000000..9fc5c4794dd
--- /dev/null
+++ b/packages/abi/src/coder/encoding/index.ts
@@ -0,0 +1,2 @@
+export { encoding } from './encoding';
+export * from './encoding-types';
diff --git a/packages/abi/src/coder/encoding/v1/array.ts b/packages/abi/src/coder/encoding/v1/array.ts
new file mode 100644
index 00000000000..f90d53ced99
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/array.ts
@@ -0,0 +1,46 @@
+import { concat } from '@fuel-ts/utils';
+
+import { ARRAY_TYPE } from '../encoding-constants';
+import type { Coder, TypesOfCoder } from '../encoding-types';
+import {
+ assertValueIsArray,
+ assertDataLengthLessThanMax,
+ assertValueLengthEqualsExpected,
+} from '../validation';
+
+/**
+ * `array` coder
+ */
+export type ArrayEncodeValue = Array['Input']>;
+export type ArrayDecodeData = Array['Decoded']>;
+export type ArrayCoder = Coder<
+ ArrayEncodeValue,
+ ArrayDecodeData
+>;
+
+export const arrayCoder = (
+ coder: TCoder,
+ size: number
+): ArrayCoder => ({
+ type: ARRAY_TYPE,
+ encode: (value: ArrayEncodeValue): Uint8Array => {
+ assertValueIsArray(value, ARRAY_TYPE);
+ assertValueLengthEqualsExpected(value, size, ARRAY_TYPE);
+ const encodedValues = value.map((elementValue) => coder.encode(elementValue));
+ return concat(encodedValues);
+ },
+ decode: (data: Uint8Array, initialOffset: number = 0): [ArrayDecodeData, number] => {
+ assertDataLengthLessThanMax(data, ARRAY_TYPE);
+
+ let decoded;
+ let offset = initialOffset;
+
+ const decodedValue = Array(size)
+ .fill(0)
+ .map(() => {
+ [decoded, offset] = coder.decode(data, offset);
+ return decoded;
+ });
+ return [decodedValue as ArrayDecodeData, offset];
+ },
+});
diff --git a/packages/abi/src/coder/encoding/v1/enum.ts b/packages/abi/src/coder/encoding/v1/enum.ts
new file mode 100644
index 00000000000..01171d1d88c
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/enum.ts
@@ -0,0 +1,90 @@
+import { concat } from '@fuel-ts/utils';
+import type { RequireExactlyOne } from 'type-fest';
+
+import { ENUM_TYPE, VOID_TYPE } from '../encoding-constants';
+import type { Coder, TypesOfCoder } from '../encoding-types';
+import { errors } from '../validation';
+
+import { u64 } from './fixed';
+
+export type EnumEncodeValue> = RequireExactlyOne<{
+ [P in keyof TCoders]: TypesOfCoder['Input'];
+}>;
+export type EnumDecodeValue> = RequireExactlyOne<{
+ [P in keyof TCoders]: TypesOfCoder['Decoded'];
+}>;
+export type EnumCoder = Record> = Coder<
+ EnumEncodeValue,
+ EnumDecodeValue
+>;
+
+const isNativeCoder = (coder: Coder) => coder.type === VOID_TYPE;
+const extractCaseKeysFromValue = >(
+ value: EnumEncodeValue
+): string[] => (typeof value === 'string' ? [value] : Object.keys(value));
+const extractCaseValueFromValue = >(
+ value: EnumEncodeValue
+) => (typeof value === 'string' ? undefined : Object.values(value)[0]);
+const extractCaseIndexFromData = (data: Uint8Array, initialOffset: number) => {
+ const [decoded, offset] = u64.decode(data, initialOffset);
+ return [decoded.toNumber(), offset];
+};
+
+const createCaseValueCoder = (key: string, index: number, coder: TCoder) => {
+ const isNative = isNativeCoder(coder);
+ return {
+ type: ENUM_TYPE,
+ encode: (value: TypesOfCoder['Input']): Uint8Array => {
+ const encodedIndex = u64.encode(index);
+ const encodedValue = coder.encode(value);
+ return concat([encodedIndex, encodedValue]);
+ },
+ decode: (data: Uint8Array, initialOffset = 0): [TypesOfCoder['Decoded'], number] => {
+ const [decoded, offset] = coder.decode(data, initialOffset);
+ return [isNative ? key : { [key]: decoded }, offset];
+ },
+ };
+};
+
+export const enumCoder = >(
+ initialCoders: TCoders,
+ type: string = ENUM_TYPE
+) => {
+ const coders = Object.fromEntries(
+ Object.entries(initialCoders).map(([key, coder], index) => [
+ key,
+ createCaseValueCoder(key, index, coder),
+ ])
+ );
+ const validKeys = Object.keys(coders);
+
+ return {
+ type,
+ encode: (value: EnumEncodeValue) => {
+ const [caseKey, ...shouldBeEmpty] = extractCaseKeysFromValue(value);
+ if (!caseKey) {
+ throw errors.invalidEnumValueCaseKeyMissing(type, value, validKeys);
+ }
+ if (!validKeys.includes(caseKey)) {
+ throw errors.invalidEnumValueCaseKey(type, value, caseKey, validKeys);
+ }
+ if (shouldBeEmpty.length > 0) {
+ throw errors.invalidEnumValueMultipleCaseKeys(type, value, validKeys);
+ }
+ const caseValue = extractCaseValueFromValue(value);
+ return coders[caseKey].encode(caseValue);
+ },
+ decode: (data: Uint8Array, initialOffset = 0): [EnumDecodeValue, number] => {
+ if (data.length < initialOffset + 8) {
+ throw errors.invalidEnumDataCaseKey(type, validKeys);
+ }
+ const [caseIndex, indexOffset] = extractCaseIndexFromData(data, initialOffset);
+ if (caseIndex < 0 || caseIndex >= validKeys.length) {
+ throw errors.invalidEnumDataCaseKey(type, validKeys);
+ }
+ const caseKey = validKeys[caseIndex];
+ const [decoded, offset] = coders[caseKey].decode(data, indexOffset);
+ return [decoded as EnumDecodeValue, offset];
+ },
+ };
+};
diff --git a/packages/abi/src/coder/encoding/v1/factories/array.ts b/packages/abi/src/coder/encoding/v1/factories/array.ts
new file mode 100644
index 00000000000..2a3df18d745
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/factories/array.ts
@@ -0,0 +1,34 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import type { AbiTypeComponent } from '../../../..';
+import { ARRAY_REGEX } from '../../../../matchers/sway-type-matchers';
+import { ARRAY_TYPE } from '../../encoding-constants';
+import type { CoderFactory, CoderFactoryParameters } from '../../encoding-types';
+import type { ArrayCoder } from '../array';
+import { arrayCoder } from '../array';
+
+export const arrayCoderFactory: CoderFactory = (
+ { type: { swayType, components } }: CoderFactoryParameters,
+ factory: CoderFactory
+) => {
+ const arrayMatch = ARRAY_REGEX.exec(swayType)?.groups;
+ if (!arrayMatch) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `The provided ${ARRAY_TYPE} type is missing ABI components.`,
+ { swayType, components }
+ );
+ }
+
+ const arraySize = parseInt(arrayMatch.length, 10);
+ const arrayElement: AbiTypeComponent | undefined = components?.[0];
+ if (!arrayElement) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `The provided ${ARRAY_TYPE} type is missing a ABI component.`
+ );
+ }
+
+ const arrayElementCoder = factory(arrayElement, factory);
+ return arrayCoder(arrayElementCoder, arraySize);
+};
diff --git a/packages/abi/src/coder/encoding/v1/factories/enum.ts b/packages/abi/src/coder/encoding/v1/factories/enum.ts
new file mode 100644
index 00000000000..795a4e948ff
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/factories/enum.ts
@@ -0,0 +1,29 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import type { AbiTypeComponent } from '../../../../parser';
+import { ENUM_TYPE } from '../../encoding-constants';
+import type { Coder, CoderFactory, CoderFactoryParameters } from '../../encoding-types';
+import { enumCoder } from '../enum';
+import type { EnumCoder } from '../enum';
+
+export const enumCoderFactory: CoderFactory = (
+ { type: { swayType, components } }: CoderFactoryParameters,
+ factory: CoderFactory
+) => {
+ if (!components) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `The provided ${ENUM_TYPE} type is missing ABI components.`,
+ { swayType, components }
+ );
+ }
+
+ const coders = components.reduce((obj, component: AbiTypeComponent) => {
+ const o: Record = obj;
+
+ o[component.name] = factory(component, factory);
+ return o;
+ }, {});
+
+ return enumCoder(coders);
+};
diff --git a/packages/abi/src/coder/encoding/v1/factories/index.ts b/packages/abi/src/coder/encoding/v1/factories/index.ts
new file mode 100644
index 00000000000..4bb7663a80c
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/factories/index.ts
@@ -0,0 +1,17 @@
+import { arrayCoderFactory } from './array';
+import { enumCoderFactory } from './enum';
+import { optionFactory } from './option';
+import { stringFactory } from './string';
+import { structCoderFactory } from './struct';
+import { tupleCoderFactory } from './tuple';
+import { vectorFactory } from './vector';
+
+export const factories = {
+ array: arrayCoderFactory,
+ enum: enumCoderFactory,
+ tuple: tupleCoderFactory,
+ struct: structCoderFactory,
+ vector: vectorFactory,
+ string: stringFactory,
+ option: optionFactory,
+};
diff --git a/packages/abi/src/coder/encoding/v1/factories/option.ts b/packages/abi/src/coder/encoding/v1/factories/option.ts
new file mode 100644
index 00000000000..d1da469eeae
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/factories/option.ts
@@ -0,0 +1,28 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import type { AbiTypeComponent } from '../../../../parser';
+import { OPTION_TYPE } from '../../encoding-constants';
+import type { CoderFactory, CoderFactoryParameters, Coder } from '../../encoding-types';
+import { option } from '../option';
+
+export const optionFactory = (
+ { type: { swayType, components } }: CoderFactoryParameters,
+ factory: CoderFactory
+) => {
+ if (!components) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `The provided ${OPTION_TYPE} type is missing ABI components.`,
+ { swayType, components }
+ );
+ }
+
+ const coders = components.reduce((obj, component: AbiTypeComponent) => {
+ const o: Record = obj;
+
+ o[component.name] = factory(component, factory);
+ return o;
+ }, {});
+
+ return option(coders);
+};
diff --git a/packages/abi/src/coder/encoding/v1/factories/string.ts b/packages/abi/src/coder/encoding/v1/factories/string.ts
new file mode 100644
index 00000000000..4cf47dd60e7
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/factories/string.ts
@@ -0,0 +1,22 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import { STRING_REGEX } from '../../../../matchers/sway-type-matchers';
+import { STRING_TYPE } from '../../encoding-constants';
+import type { CoderFactory, CoderFactoryParameters } from '../../encoding-types';
+import { string } from '../string';
+
+export const stringFactory = (
+ { type: { swayType } }: CoderFactoryParameters,
+ _factory: CoderFactory
+) => {
+ const match = STRING_REGEX.exec(swayType)?.groups;
+ if (!match) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `Unable to find ${STRING_TYPE} coder for the provided type "${swayType}".`,
+ { swayType }
+ );
+ }
+ const encodedLength = parseInt(match.length, 10);
+ return string(encodedLength);
+};
diff --git a/packages/abi/src/coder/encoding/v1/factories/struct.ts b/packages/abi/src/coder/encoding/v1/factories/struct.ts
new file mode 100644
index 00000000000..87964cacc5e
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/factories/struct.ts
@@ -0,0 +1,27 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import type { AbiTypeComponent } from '../../../../parser';
+import { STRUCT_TYPE } from '../../encoding-constants';
+import type { Coder, CoderFactory, CoderFactoryParameters } from '../../encoding-types';
+import { struct } from '../struct';
+
+export const structCoderFactory = (
+ { type: { swayType, components } }: CoderFactoryParameters,
+ factory: CoderFactory
+) => {
+ if (!components) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `The provided ${STRUCT_TYPE} type is missing ABI components.`,
+ { swayType, components }
+ );
+ }
+
+ const coders = components.reduce((obj, component: AbiTypeComponent) => {
+ const o: Record = obj;
+
+ o[component.name] = factory(component, factory);
+ return o;
+ }, {});
+ return struct(coders);
+};
diff --git a/packages/abi/src/coder/encoding/v1/factories/tuple.ts b/packages/abi/src/coder/encoding/v1/factories/tuple.ts
new file mode 100644
index 00000000000..ca4e0495cd7
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/factories/tuple.ts
@@ -0,0 +1,23 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import type { AbiTypeComponent } from '../../../../parser';
+import { TUPLE_TYPE } from '../../encoding-constants';
+import type { CoderFactory, CoderFactoryParameters } from '../../encoding-types';
+import { tuple } from '../tuple';
+import type { TupleCoder } from '../tuple';
+
+export const tupleCoderFactory: CoderFactory = (
+ { type: { swayType, components } }: CoderFactoryParameters,
+ factory: CoderFactory
+) => {
+ if (!components) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `The provided ${TUPLE_TYPE} type is missing ABI components.`,
+ { swayType, components }
+ );
+ }
+
+ const coders = components.map((component: AbiTypeComponent) => factory(component, factory));
+ return tuple(coders);
+};
diff --git a/packages/abi/src/coder/encoding/v1/factories/vector.ts b/packages/abi/src/coder/encoding/v1/factories/vector.ts
new file mode 100644
index 00000000000..8310b7e2af4
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/factories/vector.ts
@@ -0,0 +1,21 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import { VECTOR_TYPE } from '../../encoding-constants';
+import type { CoderFactory, CoderFactoryParameters } from '../../encoding-types';
+import { vector } from '../heap';
+
+export const vectorFactory = (
+ { type: { swayType, components } }: CoderFactoryParameters,
+ factory: CoderFactory
+) => {
+ if (!components) {
+ throw new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ `The provided ${VECTOR_TYPE} type is missing ABI components.`,
+ { swayType, components }
+ );
+ }
+
+ const vecElementCoder = factory(components[0], factory);
+ return vector(vecElementCoder);
+};
diff --git a/packages/abi/src/coder/encoding/v1/fixed.ts b/packages/abi/src/coder/encoding/v1/fixed.ts
new file mode 100644
index 00000000000..73d3fe33b0c
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/fixed.ts
@@ -0,0 +1,194 @@
+import type { BN, BNInput } from '@fuel-ts/math';
+import { toNumber, toBytes, bn, toHex } from '@fuel-ts/math';
+import { arrayify } from '@fuel-ts/utils';
+
+import {
+ B256_TYPE,
+ B512_TYPE,
+ BOOL_TYPE,
+ U16_TYPE,
+ U256_TYPE,
+ U32_TYPE,
+ U64_TYPE,
+ U8_TYPE,
+ VOID_TYPE,
+} from '../encoding-constants';
+import type { Coder } from '../encoding-types';
+import {
+ assertBnValueByteLengthLessThan,
+ assertBnValueNonNegative,
+ assertBooleanValue,
+ assertDataLengthMoreThanExpected,
+ assertEncode,
+ assertEncodedLengthEquals,
+ assertSafeNumberValue,
+ errors,
+} from '../validation';
+
+/**
+ * A number coder (u8, u16, u32)
+ *
+ * @param encodedLength - The number of bytes to encode the value.
+ * @param type - The type of the coder.
+ * @returns A number based coder.
+ */
+const createNumberCoder = (encodedLength: number, type: string): Coder => ({
+ type,
+ /**
+ * Encode a number value.
+ *
+ * @param value - The number value to encode.
+ * @returns The encoded number value.
+ */
+ encode: (value: number): Uint8Array => {
+ const bnValue = bn(value);
+ assertBnValueNonNegative(bnValue, type);
+ assertBnValueByteLengthLessThan(bnValue, encodedLength, type);
+ return toBytes(bnValue, encodedLength);
+ },
+ decode: (data: Uint8Array, offset: number = 0): [number, number] => {
+ const elementData = data.slice(offset, offset + encodedLength);
+ assertEncodedLengthEquals(elementData, encodedLength, type);
+ return [toNumber(elementData), offset + encodedLength];
+ },
+});
+
+/**
+ * A big number coder (u64, u256)
+ *
+ * @param encodedLength - The number of bytes to encode the value.
+ * @param type - The type of the coder.
+ * @returns A big number based coder.
+ */
+const createBigNumberCoder = (encodedLength: number, type: string): Coder => ({
+ type,
+ /**
+ * Encode a big number value.
+ *
+ * @param value - The big number value to encode.
+ * @returns The encoded big number value.
+ */
+ encode: (value: BN | BNInput): Uint8Array => {
+ assertSafeNumberValue(value, type);
+ const bnValue = assertEncode(bn, value, errors.invalidBnValue(type, value));
+ assertBnValueNonNegative(bnValue, type);
+ assertBnValueByteLengthLessThan(bnValue, encodedLength, type);
+ return toBytes(bnValue, encodedLength);
+ },
+ /**
+ * Decode a big number value.
+ *
+ * @param data - The encoded data to decode.
+ * @param offset - The offset to start decoding from.
+ * @returns A tuple with the [decoded value, offset of the final decoded]
+ */
+ decode: (data: Uint8Array, offset: number = 0): [BN, number] => {
+ const elementData = data.slice(offset, offset + encodedLength);
+ assertEncodedLengthEquals(elementData, encodedLength, type);
+ return [bn(elementData), offset + encodedLength];
+ },
+});
+
+/**
+ * A hex coder (b256, b512)
+ *
+ * @param encodedLength - The number of bytes to encode the value.
+ * @param type - The type of the coder.
+ * @returns A hex based coder.
+ */
+const createHexCoder = (encodedLength: number, type: string): Coder => ({
+ type,
+ /**
+ * Encode a hex value.
+ *
+ * @param value - The hex value to encode.
+ * @returns The encoded hex value.
+ *
+ * @throws {@link FuelError} - when a malformed hex value is provided
+ * @throws {@link FuelError} - when the encoded value's byte length does not match the expected length
+ */
+ encode: (value: string): Uint8Array => {
+ let encodedValue;
+ try {
+ encodedValue = arrayify(value);
+ } catch (error) {
+ throw errors.malformedHexValue(type, value, encodedLength);
+ }
+
+ assertEncodedLengthEquals(encodedValue, encodedLength, type);
+ return encodedValue;
+ },
+ /**
+ * Decode a hex value.
+ *
+ * @param data - The encoded data to decode.
+ * @param offset - The offset to start decoding from.
+ * @returns A tuple with the [decoded value, offset of the final decoded]
+ *
+ * @throws {@link FuelError} - when the encoded data length does not match the expected length
+ */
+ decode: (data: Uint8Array, offset: number = 0): [string, number] => {
+ let bytes = data.slice(offset, offset + encodedLength);
+ assertEncodedLengthEquals(bytes, encodedLength, type);
+
+ if (bn(bytes).isZero()) {
+ bytes = new Uint8Array(encodedLength);
+ }
+ return [toHex(bytes, encodedLength), offset + encodedLength];
+ },
+});
+
+export const u8: Coder = createNumberCoder(1, U8_TYPE);
+export const u16: Coder = createNumberCoder(2, U16_TYPE);
+export const u32: Coder = createNumberCoder(4, U32_TYPE);
+export const u64: Coder = createBigNumberCoder(8, U64_TYPE);
+export const u256: Coder = createBigNumberCoder(32, U256_TYPE);
+export const b256: Coder = createHexCoder(32, B256_TYPE);
+export const b512: Coder = createHexCoder(64, B512_TYPE);
+export const voidCoder: Coder = {
+ type: VOID_TYPE,
+ /**
+ * Encode a void value.
+ *
+ * @returns The encoded void value.
+ */
+ encode: (): Uint8Array => new Uint8Array(),
+ /**
+ * Decode a void value.
+ *
+ * @param data - The encoded data to decode.
+ * @param offset - The offset to start decoding from.
+ * @returns A tuple with the [decoded value, offset of the final decoded]
+ */
+ decode: (_data: Uint8Array, offset: number = 0): [undefined, number] => [undefined, offset],
+};
+
+export const bool: Coder = {
+ type: BOOL_TYPE,
+ /**
+ * Encode a boolean value.
+ *
+ * @param value - The boolean value to encode.
+ * @returns The encoded boolean value.
+ */
+ encode: (value: boolean): Uint8Array => {
+ assertBooleanValue(value);
+ return toBytes(value ? 1 : 0);
+ },
+ /**
+ * Decode a boolean value.
+ *
+ * @param data - The encoded data to decode.
+ * @param offset - The offset to start decoding from.
+ * @returns A tuple with the [decoded value, offset of the final decoded]
+ */
+ decode: (data: Uint8Array, initialOffset: number = 0): [boolean, number] => {
+ assertDataLengthMoreThanExpected(data, 1, BOOL_TYPE);
+
+ const [value, offset] = u8.decode(data, initialOffset);
+ if (value !== 0 && value !== 1) {
+ throw errors.invalidBooleanData(BOOL_TYPE, data, value);
+ }
+ return [Boolean(value), offset];
+ },
+};
diff --git a/packages/abi/src/coder/encoding/v1/heap.ts b/packages/abi/src/coder/encoding/v1/heap.ts
new file mode 100644
index 00000000000..37b2ccce751
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/heap.ts
@@ -0,0 +1,138 @@
+import { FuelError } from '@fuel-ts/errors';
+import { concat, toUtf8Bytes, toUtf8String } from '@fuel-ts/utils';
+
+import {
+ BYTES_TYPE,
+ RAW_SLICE_TYPE,
+ STD_STRING_TYPE,
+ STR_SLICE_TYPE,
+ VECTOR_TYPE,
+} from '../encoding-constants';
+import type { Coder } from '../encoding-types';
+import { assertDataLengthLessThanMax, assertEncodedLengthEquals, errors } from '../validation';
+
+import { u64 } from './fixed';
+
+const createHeapType = ({
+ type,
+ encode,
+ decode,
+}: Coder): Coder => ({
+ type,
+ encode: (value: TEncoded) => {
+ try {
+ // Encode the length of the bytes
+ const dataLengthBytes = u64.encode(value.length);
+
+ // Encode the value
+ const valueBytes = encode(value);
+
+ // Concatenate the encoded length with the bytes
+ return concat([dataLengthBytes, valueBytes]);
+ } catch (error) {
+ throw errors.invalidValue(type, value);
+ }
+ },
+ decode: (data: Uint8Array, initialOffset = 0): [TDecoded, number] => {
+ try {
+ // Obtain the length of the bytes
+ const [dataLengthBn, dataLowerOffset] = u64.decode(data, initialOffset);
+ const dataLength = dataLengthBn.toNumber();
+
+ // Obtain the data bytes
+ const dataUpperOffset = dataLowerOffset + dataLength;
+ const dataBytes = data.slice(dataLowerOffset, dataUpperOffset);
+ assertEncodedLengthEquals(dataBytes, dataLength, type);
+
+ return decode(dataBytes, dataUpperOffset);
+ } catch (error) {
+ throw errors.malformedBytes(type, data);
+ }
+ },
+});
+
+const bytesTransformer: Coder = {
+ type: BYTES_TYPE,
+ encode: (value: Uint8Array | number[]) => (Array.isArray(value) ? new Uint8Array(value) : value),
+ decode: (data: Uint8Array, offset: number): [Uint8Array, number] => [data, offset],
+};
+export const bytes: Coder = createHeapType(bytesTransformer);
+
+const rawSliceTransformer: Coder = {
+ type: RAW_SLICE_TYPE,
+ encode: (value: number[]) => new Uint8Array(value),
+ decode: (data: Uint8Array, offset: number): [number[], number] => [Array.from(data), offset],
+};
+export const rawSlice: Coder = createHeapType(rawSliceTransformer);
+
+const createStringCoder = (type: string): Coder =>
+ createHeapType({
+ type,
+ encode: (value: string) => toUtf8Bytes(value),
+ decode: (data: Uint8Array, offset: number): [string, number] => [toUtf8String(data), offset],
+ });
+
+export const stdString = createStringCoder(STD_STRING_TYPE);
+export const str = createStringCoder(STR_SLICE_TYPE);
+
+export type VecEncodedValue =
+ | Array[0]>
+ | Uint8Array;
+export type VecDecodedValue =
+ | Array[0]>
+ | Uint8Array;
+
+const isUint8Array = (value: unknown): value is Uint8Array => value instanceof Uint8Array;
+
+export const vector = (coder: TCoder): Coder> => ({
+ type: VECTOR_TYPE,
+ encode: (value: VecEncodedValue): Uint8Array => {
+ if (!Array.isArray(value) && !isUint8Array(value)) {
+ throw new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${VECTOR_TYPE} value - expected array value, or a Uint8Array.`,
+ { value }
+ );
+ }
+
+ const valueArray = Array.isArray(value) ? value : Array.from(value);
+
+ // Encode the length of the bytes
+ const dataLengthBytes = u64.encode(valueArray.length);
+
+ // Encode the value
+ const valueBytes = valueArray.map((elementValue) => coder.encode(elementValue));
+
+ // Concatenate the encoded length with the bytes
+ return concat([dataLengthBytes, ...valueBytes]);
+ },
+ decode: (data: Uint8Array, initialOffset = 0): [VecDecodedValue, number] => {
+ assertDataLengthLessThanMax(data, VECTOR_TYPE);
+
+ let dataLengthBn;
+ let dataLowerOffset;
+
+ try {
+ // Obtain the length of the bytes
+ [dataLengthBn, dataLowerOffset] = u64.decode(data, initialOffset);
+ } catch (error) {
+ throw new FuelError(
+ FuelError.CODES.DECODE_ERROR,
+ `Invalid ${VECTOR_TYPE} data - malformed bytes.`,
+ { data }
+ );
+ }
+
+ // Obtain the data bytes
+ const elements: unknown[] = [];
+ const dataLength = dataLengthBn.toNumber();
+ let offset = dataLowerOffset;
+ for (let i = 0; i < dataLength; i++) {
+ const [decodedElement, elementOffset] = coder.decode(data, offset);
+ offset = elementOffset;
+ elements.push(decodedElement);
+ }
+
+ return [elements as VecDecodedValue, offset];
+ },
+});
diff --git a/packages/abi/src/coder/encoding/v1/index.ts b/packages/abi/src/coder/encoding/v1/index.ts
new file mode 100644
index 00000000000..53c0cf4c78c
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/index.ts
@@ -0,0 +1,66 @@
+import { createMatcher } from '../../../matchers/sway-type-matchers';
+import type { SupportedCoder } from '../encoding-types';
+
+import { arrayCoder } from './array';
+import { enumCoder } from './enum';
+import { factories } from './factories';
+import { voidCoder, u16, u32, u8, u64, u256, b256, b512, bool } from './fixed';
+import { bytes, rawSlice, stdString, str, vector } from './heap';
+import { option } from './option';
+import { string } from './string';
+import { struct } from './struct';
+import { tuple } from './tuple';
+
+export const coders = {
+ u8,
+ u16,
+ u32,
+ u64,
+ u256,
+ b256,
+ b512,
+ bool,
+ void: voidCoder,
+ bytes,
+ rawSlice,
+ str,
+ stdString,
+ array: Object.assign(arrayCoder, { factory: factories.array }),
+ tuple: Object.assign(tuple, { factory: factories.tuple }),
+ struct: Object.assign(struct, { factory: factories.struct }),
+ enum: Object.assign(enumCoder, { factory: factories.enum }),
+ vector: Object.assign(vector, { factory: factories.vector }),
+ string: Object.assign(string, { factory: factories.string }),
+ option: Object.assign(option, { factory: factories.option }),
+} as const;
+
+export const matcher = createMatcher({
+ u8: coders.u8,
+ u16: coders.u16,
+ u32: coders.u32,
+ u64: coders.u64,
+ u256: coders.u256,
+ rawUntypedSlice: coders.rawSlice,
+ b256: coders.b256,
+ b512: coders.b512,
+ bool: coders.bool,
+ void: coders.void,
+
+ string: coders.string,
+ array: coders.array,
+ tuple: coders.tuple,
+ vector: coders.vector,
+ struct: coders.struct,
+ bytes: coders.bytes,
+ stdString: coders.stdString,
+ str: coders.str,
+ enum: coders.enum,
+ option: coders.option,
+
+ assetId: coders.struct,
+ evmAddress: coders.struct,
+ result: coders.enum,
+
+ // Unmatchable
+ generic: undefined,
+});
diff --git a/packages/abi/src/coder/encoding/v1/option.ts b/packages/abi/src/coder/encoding/v1/option.ts
new file mode 100644
index 00000000000..643df9f7d12
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/option.ts
@@ -0,0 +1,45 @@
+import { OPTION_TYPE } from '../encoding-constants';
+import type { Coder, Option } from '../encoding-types';
+import { assertDecode, assertEncode, errors } from '../validation';
+
+import { enumCoder } from './enum';
+import type { EnumDecodeValue, EnumEncodeValue } from './enum';
+
+type SwayOption = 'None' | { Some: T };
+
+export type OptionEncodeValue> =
+ | EnumEncodeValue['Some']
+ | undefined;
+export type OptionDecodeData> =
+ EnumDecodeValue[keyof TCoders];
+export type OptionCoder> = Coder<
+ OptionEncodeValue,
+ OptionDecodeData
+>;
+
+export const option = >(
+ coders: TCoders
+): OptionCoder => {
+ const valueCoder = enumCoder(coders, OPTION_TYPE);
+ return {
+ type: OPTION_TYPE,
+ encode: (value: Option): Uint8Array => {
+ const input: SwayOption = value === undefined ? 'None' : { Some: value };
+ return assertEncode(
+ valueCoder.encode,
+ input as unknown as EnumEncodeValue,
+ errors.invalidValue(OPTION_TYPE, input)
+ );
+ },
+ decode: (data: Uint8Array, initialOffset = 0): [OptionDecodeData, number] => {
+ const [decoded, offset] = assertDecode(
+ valueCoder.decode,
+ data,
+ initialOffset,
+ errors.malformedBytes(OPTION_TYPE, data)
+ );
+ const optionValue = typeof decoded === 'string' ? undefined : decoded.Some;
+ return [optionValue as OptionDecodeData, offset];
+ },
+ };
+};
diff --git a/packages/abi/src/coder/encoding/v1/string.ts b/packages/abi/src/coder/encoding/v1/string.ts
new file mode 100644
index 00000000000..9211c0393b0
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/string.ts
@@ -0,0 +1,25 @@
+import { toUtf8Bytes, toUtf8String } from '@fuel-ts/utils';
+
+import { STRING_TYPE } from '../encoding-constants';
+import type { Coder } from '../encoding-types';
+import { assertEncodedLengthEquals, assertValueLengthEqualsExpected } from '../validation';
+
+/**
+ * `string` coder
+ *
+ * @param encodedLength - The length of the encoded array.
+ * @returns
+ */
+export const string = (encodedLength: number): Coder => ({
+ type: 'string',
+ encode: (value: string): Uint8Array => {
+ assertValueLengthEqualsExpected(value, encodedLength, STRING_TYPE);
+ return toUtf8Bytes(value);
+ },
+ decode: (data: Uint8Array, offset: number = 0): [string, number] => {
+ const upperOffset = offset + encodedLength;
+ const bytes = data.slice(offset, upperOffset);
+ assertEncodedLengthEquals(bytes, encodedLength, STRING_TYPE);
+ return [toUtf8String(bytes), upperOffset];
+ },
+});
diff --git a/packages/abi/src/coder/encoding/v1/struct.ts b/packages/abi/src/coder/encoding/v1/struct.ts
new file mode 100644
index 00000000000..42eec900507
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/struct.ts
@@ -0,0 +1,44 @@
+import { concatBytes } from '@fuel-ts/utils';
+
+import { STRUCT_TYPE } from '../encoding-constants';
+import type { Coder, TypesOfCoder } from '../encoding-types';
+import { assertObjectKeysAllPresent } from '../validation';
+
+/**
+ * `struct` coder
+ */
+export type StructEncodeValue> = {
+ [P in keyof TCoders]: TypesOfCoder['Input'];
+};
+export type StructDecodeData> = {
+ [P in keyof TCoders]: TypesOfCoder['Decoded'];
+};
+export type StructCoder = Record> = Coder<
+ StructEncodeValue,
+ StructDecodeData
+>;
+
+export const struct = >(
+ coders: TCoders
+): StructCoder => ({
+ type: STRUCT_TYPE,
+ encode: (value: StructEncodeValue): Uint8Array => {
+ assertObjectKeysAllPresent(value, coders, STRUCT_TYPE);
+
+ // Encode each value
+ const encodedValues = Object.entries(coders).map(([key, coder]) => coder.encode(value[key]));
+
+ return concatBytes(encodedValues);
+ },
+ decode: (data: Uint8Array, initialOffset = 0): [StructDecodeData, number] => {
+ let offset = initialOffset;
+ let decoded;
+
+ const decodedValue = Object.entries(coders).reduce((acc, [caseKey, fieldCoder]) => {
+ [decoded, offset] = fieldCoder.decode(data, offset);
+ acc[caseKey as keyof StructDecodeData] = decoded;
+ return acc;
+ }, {} as StructDecodeData);
+ return [decodedValue, offset];
+ },
+});
diff --git a/packages/abi/src/coder/encoding/v1/tuple.ts b/packages/abi/src/coder/encoding/v1/tuple.ts
new file mode 100644
index 00000000000..430dbe79fcb
--- /dev/null
+++ b/packages/abi/src/coder/encoding/v1/tuple.ts
@@ -0,0 +1,45 @@
+import { concat } from '@fuel-ts/utils';
+
+import { TUPLE_TYPE } from '../encoding-constants';
+import type { Coder, TypesOfCoder } from '../encoding-types';
+import { assertValueLengthEqualsExpected, errors } from '../validation';
+
+/**
+ * Tuple coder
+ */
+export type TupleEncodeValue = {
+ [P in keyof TCoders]: TypesOfCoder['Input'];
+};
+export type TupleDecodeValue = {
+ [P in keyof TCoders]: TypesOfCoder['Decoded'];
+};
+export type TupleCoder = Coder<
+ TupleEncodeValue,
+ TupleDecodeValue
+>;
+
+export const tuple = (coders: TCoders): TupleCoder => ({
+ type: TUPLE_TYPE,
+ encode: (value: TupleEncodeValue): Uint8Array => {
+ assertValueLengthEqualsExpected(value, coders.length, TUPLE_TYPE);
+ const encodedValues = coders.map((elementCoder, elementIndex) => {
+ const elementValue = value[elementIndex];
+ return elementCoder.encode(elementValue);
+ });
+ return concat(encodedValues);
+ },
+ decode: (data: Uint8Array, initialOffset = 0): [TupleDecodeValue, number] => {
+ try {
+ let offset = initialOffset;
+ let decoded;
+
+ const decodedValue = coders.map((coder) => {
+ [decoded, offset] = coder.decode(data, offset);
+ return decoded;
+ });
+ return [decodedValue as TupleDecodeValue, offset];
+ } catch (error) {
+ throw errors.malformedData(TUPLE_TYPE, data, initialOffset);
+ }
+ },
+});
diff --git a/packages/abi/src/coder/encoding/validation.ts b/packages/abi/src/coder/encoding/validation.ts
new file mode 100644
index 00000000000..1cbb55d1ab7
--- /dev/null
+++ b/packages/abi/src/coder/encoding/validation.ts
@@ -0,0 +1,240 @@
+import { FuelError } from '@fuel-ts/errors';
+import type { BN, BNInput } from '@fuel-ts/math';
+
+import type { Coder } from '../..';
+import { MAX_BYTES } from '../constants';
+
+export const errors = {
+ /**
+ * Encode errors
+ */
+ invalidValue: (type: string, value: unknown) =>
+ new FuelError(FuelError.CODES.ENCODE_ERROR, `Invalid ${type} value - malformed value.`, {
+ value,
+ }),
+ invalidBnValue: (type: string, value: BNInput) =>
+ new FuelError(FuelError.CODES.ENCODE_ERROR, `Invalid ${type} value - expected a BNInput.`, {
+ value,
+ }),
+ malformedHexValue: (type: string, value: string, expectedLength: number) =>
+ new FuelError(FuelError.CODES.ENCODE_ERROR, `Invalid ${type} value - malformed hex value.`, {
+ value,
+ expectedLength,
+ }),
+ invalidEnumValueCaseKeyMissing: (type: string, value: unknown, validKeys: string[]) =>
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${type} value - a valid case key must be provided.`,
+ {
+ value,
+ validKeys,
+ }
+ ),
+ invalidEnumValueCaseKey: (type: string, value: unknown, caseKey: string, validKeys: string[]) =>
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${type} value - invalid case key "${caseKey}".`,
+ { value, validKeys }
+ ),
+ invalidEnumValueMultipleCaseKeys: (type: string, value: unknown, validKeys: string[]) =>
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${type} value - only one field must be provided.`,
+ { value, validKeys }
+ ),
+ /**
+ * Decode errors
+ */
+ malformedData: (type: string, data: Uint8Array, expectedLength: number) =>
+ new FuelError(FuelError.CODES.DECODE_ERROR, `Invalid ${type} data - malformed data.`, {
+ data,
+ expectedLength,
+ }),
+ malformedBytes: (type: string, data: Uint8Array) =>
+ new FuelError(FuelError.CODES.DECODE_ERROR, `Invalid ${type} data - malformed bytes.`, {
+ data,
+ }),
+ invalidEnumDataCaseKey: (type: string, validKeys: string[]) =>
+ new FuelError(FuelError.CODES.DECODE_ERROR, `Invalid ${type} data - invalid case key.`, {
+ validKeys,
+ }),
+ invalidBooleanData: (type: string, data: Uint8Array, decoded: unknown) =>
+ new FuelError(FuelError.CODES.DECODE_ERROR, `Invalid ${type} value - invalid boolean value.`, {
+ data,
+ type,
+ value: decoded,
+ }),
+};
+
+export const assertEncode = (
+ encode: (value: TValue) => TOutput,
+ value: TValue,
+ thrownError: FuelError
+) => {
+ try {
+ return encode(value);
+ } catch (error) {
+ throw thrownError;
+ }
+};
+
+/**
+ * Asserts that a value is a boolean.
+ *
+ * @throws {@link FuelError} - with a ENCODE_ERROR code if the value is not a boolean
+ */
+export const assertBooleanValue = (value: unknown) => {
+ if (typeof value !== 'boolean') {
+ throw new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid boolean value.', { value });
+ }
+};
+
+export const assertValueIsArray = (value: unknown, type: string) => {
+ if (!Array.isArray(value)) {
+ throw new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${type} value - expected array value.`,
+ {
+ value,
+ type,
+ }
+ );
+ }
+};
+
+/**
+ * Asserts that a number value is less than the max safe integer.
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER#description
+ *
+ * @throws {@link FuelError} - with a ENCODE_ERROR code if the value is more than the max safe integer
+ */
+export const assertSafeNumberValue = (value: unknown, type: string) => {
+ if (typeof value === 'number' && value > Number.MAX_SAFE_INTEGER) {
+ throw new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${type} type - number value is too large. Number can only safely handle up to 53 bits.`
+ );
+ }
+};
+
+/**
+ * Asserts that a BN value is non-negative.
+ *
+ * @throws {@link FuelError} - with a ENCODE_ERROR code if the value is less than zero
+ */
+export const assertBnValueNonNegative = (value: BN, type: string) => {
+ if (value.isNeg()) {
+ throw new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${type} value - value is less than zero.`,
+ { value: value.toString(), type }
+ );
+ }
+};
+
+/**
+ * Asserts that a BN value's byte length is less than a maximum.
+ *
+ * @throws {@link FuelError} - with a ENCODE_ERROR code if the value's byte length exceeds the maximum
+ */
+export const assertBnValueByteLengthLessThan = (value: BN, max: number, type: string) => {
+ if (value.byteLength() > max) {
+ throw new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${type} value - value exceeds maximum.`,
+ { value: value.toString(), type }
+ );
+ }
+};
+
+export function assertValueLengthEqualsExpected(
+ value: ArrayLike,
+ expectedLength: number,
+ type: string
+) {
+ if (value.length !== expectedLength) {
+ throw new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid ${type} value - unexpected length.`,
+ { value, expectedLength }
+ );
+ }
+}
+
+export function assertObjectKeysAllPresent>(
+ value: Record,
+ coders: TCoders,
+ type: string
+) {
+ const actualKeys = Object.keys(value);
+ const expectedKeys = Object.keys(coders);
+
+ // Check if there are any missing keys or extra keys
+ const missingKeys = expectedKeys.filter((key) => !actualKeys.includes(key));
+ if (missingKeys.length > 0) {
+ const paths = missingKeys.map((key) => ({ path: key, error: 'Field not present.' }));
+ throw new FuelError(FuelError.CODES.ENCODE_ERROR, `Invalid ${type} value - malformed object.`, {
+ value,
+ paths,
+ });
+ }
+}
+
+export const assertDecode = (
+ decode: (data: Uint8Array, offset: number) => [T, number],
+ data: Uint8Array,
+ offset: number,
+ thrownError: FuelError
+) => {
+ try {
+ return decode(data, offset);
+ } catch (error) {
+ throw thrownError;
+ }
+};
+
+/**
+ * Asserts that the encoded data length matches the expected length
+ *
+ * @throws {@link FuelError} - with a DECODE_ERROR code if the encoded data length does not match the expected length
+ */
+export const assertEncodedLengthEquals = (
+ data: Uint8Array,
+ expectedLength: number,
+ type: string
+) => {
+ if (data.length !== expectedLength) {
+ throw new FuelError(FuelError.CODES.DECODE_ERROR, `Invalid ${type} data - unexpected length.`, {
+ data,
+ type,
+ expectedLength,
+ });
+ }
+};
+
+export function assertDataLengthLessThanMax(data: Uint8Array, type: string) {
+ if (data.length > MAX_BYTES) {
+ throw new FuelError(
+ FuelError.CODES.DECODE_ERROR,
+ `Invalid ${type} data - exceeds maximum bytes.`,
+ {
+ data,
+ length: data.length,
+ maxLength: MAX_BYTES,
+ }
+ );
+ }
+}
+
+export function assertDataLengthMoreThanExpected(
+ data: Uint8Array,
+ expectedLength: number,
+ type: string
+) {
+ if (data.length < expectedLength) {
+ throw new FuelError(FuelError.CODES.DECODE_ERROR, `Invalid ${type} data - not enough data.`, {
+ data,
+ expectedLength,
+ });
+ }
+}
diff --git a/packages/abi/src/coder/index.ts b/packages/abi/src/coder/index.ts
new file mode 100644
index 00000000000..1c4e5254600
--- /dev/null
+++ b/packages/abi/src/coder/index.ts
@@ -0,0 +1,4 @@
+export { AbiCoder } from './AbiCoder';
+export * from './abi-coder-types';
+export * from './constants';
+export * from './encoding';
diff --git a/packages/abi/src/coder/utils/createConfigurable.ts b/packages/abi/src/coder/utils/createConfigurable.ts
new file mode 100644
index 00000000000..49129e7c065
--- /dev/null
+++ b/packages/abi/src/coder/utils/createConfigurable.ts
@@ -0,0 +1,23 @@
+import type { BytesLike } from '@fuel-ts/utils';
+import { arrayify } from '@fuel-ts/utils';
+
+import type { AbiConfigurable } from '../../parser';
+import type { AbiCoderConfigurable } from '../abi-coder-types';
+import type { AbiEncoding, DecodedValue } from '../encoding';
+
+export const createConfigurable = (
+ configurable: AbiConfigurable,
+ encoding: AbiEncoding
+): AbiCoderConfigurable => {
+ const configurableCoder = encoding.getCoder(configurable);
+ return {
+ name: configurable.name,
+ offset: configurable.offset,
+ encode: configurableCoder.encode,
+ decode: (data: BytesLike): DecodedValue => {
+ const bytes = arrayify(data);
+ const [decodedValue] = configurableCoder.decode(bytes);
+ return decodedValue as DecodedValue;
+ },
+ };
+};
diff --git a/packages/abi/src/coder/utils/createFunction.ts b/packages/abi/src/coder/utils/createFunction.ts
new file mode 100644
index 00000000000..00cd02c8ce8
--- /dev/null
+++ b/packages/abi/src/coder/utils/createFunction.ts
@@ -0,0 +1,59 @@
+import { FuelError } from '@fuel-ts/errors';
+import type { BytesLike } from '@fuel-ts/utils';
+import { arrayify } from '@fuel-ts/utils';
+
+import type { AbiFunction } from '../../parser';
+import type { AbiCoderFunction } from '../abi-coder-types';
+import type { AbiEncoding, DecodedValue, InputValue } from '../encoding';
+
+import { createFunctionSelector } from './createFunctionSelector';
+import { createFunctionSignature } from './createFunctionSignature';
+import { getFunctionInputs } from './getFunctionInputs';
+import { padValuesWithUndefined } from './padValuesWithUndefined';
+
+export const createFunction = (fn: AbiFunction, encoding: AbiEncoding): AbiCoderFunction => {
+ const signature = createFunctionSignature(fn);
+ const argumentCoder = encoding.coders.tuple(fn.inputs.map((input) => encoding.getCoder(input)));
+ const outputCoder = encoding.getCoder({ type: fn.output });
+
+ const storageAttribute = fn.attributes?.find((attr) => attr.name === 'storage');
+ const isReadOnly = !storageAttribute?.arguments?.includes('write');
+
+ const functionInputs = getFunctionInputs({ inputs: fn.inputs });
+ const mandatoryInputLength = functionInputs.filter((i) => !i.isOptional).length;
+
+ return {
+ // Function fields
+ name: fn.name,
+ inputs: fn.inputs,
+ signature,
+ selector: createFunctionSelector(signature),
+ selectorBytes: encoding.coders.stdString.encode(fn.name),
+ attributes: fn.attributes ?? [],
+ isReadOnly: () => isReadOnly,
+
+ // Coders
+ encodeArguments: (values: InputValue[]): Uint8Array => {
+ if (values.length < mandatoryInputLength) {
+ throw new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ `Invalid number of arguments. Expected a minimum of ${mandatoryInputLength} arguments, received ${values.length}`
+ );
+ }
+
+ const paddedValues = padValuesWithUndefined(values, functionInputs);
+ return argumentCoder.encode(paddedValues);
+ },
+ decodeArguments: (data: BytesLike): DecodedValue[] => {
+ const bytes = arrayify(data);
+ const [decoded] = argumentCoder.decode(bytes);
+ return decoded as DecodedValue[];
+ },
+ encodeOutput: (value: InputValue): Uint8Array => outputCoder.encode(value),
+ decodeOutput: (data: BytesLike): DecodedValue => {
+ const bytes = arrayify(data);
+ const [decoded] = outputCoder.decode(bytes);
+ return decoded as DecodedValue;
+ },
+ };
+};
diff --git a/packages/abi/src/coder/utils/createFunctionSelector.ts b/packages/abi/src/coder/utils/createFunctionSelector.ts
new file mode 100644
index 00000000000..febcdbf73cf
--- /dev/null
+++ b/packages/abi/src/coder/utils/createFunctionSelector.ts
@@ -0,0 +1,18 @@
+import { bufferFromString } from '@fuel-ts/crypto';
+import { sha256 } from '@fuel-ts/hasher';
+import { bn } from '@fuel-ts/math';
+
+/**
+ * Creates a function selector from a function signature. This acts as a unique identifier for the function
+ * that can be used to identify the function in the contract. Previously, this was used in contract calls adhering
+ * to the v0 specification. This has since been superseded to be a StdString representation of a function name.
+ * However this selector still exists as a unique identifier for the function.
+ *
+ * @param functionSignature - The function signature to create a selector from.
+ * @returns The function selector.
+ */
+export const createFunctionSelector = (functionSignature: string): string => {
+ const hashedFunctionSignature = sha256(bufferFromString(functionSignature, 'utf-8'));
+ // get first 4 bytes of signature + 0x prefix. then left-pad it to 8 bytes using toHex(8)
+ return bn(hashedFunctionSignature.slice(0, 10)).toHex(8);
+};
diff --git a/packages/abi/src/coder/utils/createFunctionSignature.ts b/packages/abi/src/coder/utils/createFunctionSignature.ts
new file mode 100644
index 00000000000..6a492692b00
--- /dev/null
+++ b/packages/abi/src/coder/utils/createFunctionSignature.ts
@@ -0,0 +1,88 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+import {
+ ARRAY_REGEX,
+ ENUM_REGEX,
+ STRING_REGEX,
+ STRUCT_REGEX,
+ swayTypeMatchers,
+} from '../../matchers/sway-type-matchers';
+import type { AbiFunction, AbiConcreteType, AbiTypeComponent } from '../../parser';
+
+const HEAP_TYPE_SIGNATURE = '(rawptr,u64),u64';
+
+const createSignaturePrefix = ({
+ type,
+}: {
+ type: AbiConcreteType | AbiTypeComponent['type'];
+}): string => {
+ switch (true) {
+ case STRUCT_REGEX.test(type.swayType):
+ return 's';
+
+ case ARRAY_REGEX.test(type.swayType):
+ return 'a';
+
+ case ENUM_REGEX.test(type.swayType):
+ return 'e';
+
+ default:
+ return '';
+ }
+};
+
+const createSignatureContents = ({
+ type,
+}: {
+ type: AbiConcreteType | AbiTypeComponent['type'];
+}): string => {
+ const { swayType, components, metadata } = type;
+
+ if (swayTypeMatchers.rawUntypedSlice(type)) {
+ return 'rawslice';
+ }
+
+ const strMatch = STRING_REGEX.exec(swayType)?.groups;
+ if (strMatch) {
+ return `str[${strMatch.length}]`;
+ }
+
+ if (swayTypeMatchers.bytes(type)) {
+ return `(s${HEAP_TYPE_SIGNATURE})`;
+ }
+
+ if (components === undefined) {
+ return swayType;
+ }
+
+ const arrayMatch = ARRAY_REGEX.exec(swayType)?.groups;
+ if (arrayMatch) {
+ const arrayElementSignature = createSignatureForType(components[0]);
+ return `[${arrayElementSignature};${arrayMatch.length}]`;
+ }
+
+ const typeArgumentsSignature = metadata?.typeArguments
+ ? `<${metadata.typeArguments
+ ?.map((typeArgument) => createSignatureForType({ type: typeArgument }))
+ .join(',')}>`
+ : '';
+
+ const componentsSignature = swayTypeMatchers.vector(type)
+ ? `s${typeArgumentsSignature}${HEAP_TYPE_SIGNATURE}`
+ : components.map(createSignatureForType).join(',');
+
+ return `${typeArgumentsSignature}(${componentsSignature})`;
+};
+
+const createSignatureForType = (input: {
+ type: AbiConcreteType | AbiTypeComponent['type'];
+}): string => {
+ const prefix = createSignaturePrefix(input);
+ const contents = createSignatureContents(input);
+
+ return `${prefix}${contents}`;
+};
+
+export const createFunctionSignature = (fn: AbiFunction): string => {
+ const functionInputsSignature = fn.inputs.map(createSignatureForType).join(',');
+ return `${fn.name}(${functionInputsSignature})`;
+};
diff --git a/packages/abi/src/coder/utils/createLog.ts b/packages/abi/src/coder/utils/createLog.ts
new file mode 100644
index 00000000000..371825c766c
--- /dev/null
+++ b/packages/abi/src/coder/utils/createLog.ts
@@ -0,0 +1,20 @@
+import type { BytesLike } from '@fuel-ts/utils';
+import { arrayify } from '@fuel-ts/utils';
+
+import type { AbiLoggedType } from '../../parser';
+import type { AbiCoderLog } from '../abi-coder-types';
+import type { AbiEncoding } from '../encoding';
+import type { DecodedValue } from '../encoding/encoding-types';
+
+export const createLog = (loggedType: AbiLoggedType, encoding: AbiEncoding): AbiCoderLog => {
+ const loggedTypeCoder = encoding.getCoder(loggedType);
+ return {
+ logId: loggedType.logId,
+ encode: loggedTypeCoder.encode,
+ decode: (data: BytesLike): DecodedValue => {
+ const bytes = arrayify(data);
+ const [decoded] = loggedTypeCoder.decode(bytes);
+ return decoded as DecodedValue;
+ },
+ };
+};
diff --git a/packages/abi/src/coder/utils/createType.ts b/packages/abi/src/coder/utils/createType.ts
new file mode 100644
index 00000000000..fa395031489
--- /dev/null
+++ b/packages/abi/src/coder/utils/createType.ts
@@ -0,0 +1,22 @@
+import type { BytesLike } from '@fuel-ts/utils';
+import { arrayify } from '@fuel-ts/utils';
+
+import type { AbiConcreteType, AbiTypeComponent } from '../../parser';
+import type { AbiCoderType } from '../abi-coder-types';
+import type { AbiEncoding } from '../encoding';
+import type { DecodedValue } from '../encoding/encoding-types';
+
+export function createType(
+ type: AbiConcreteType | AbiTypeComponent['type'],
+ encoding: AbiEncoding
+): AbiCoderType {
+ const coder = encoding.getCoder({ type });
+ return {
+ encode: coder.encode,
+ decode: (data: BytesLike): DecodedValue => {
+ const bytes = arrayify(data);
+ const [decoded] = coder.decode(bytes);
+ return decoded as DecodedValue;
+ },
+ };
+}
diff --git a/packages/abi/src/coder/utils/getFunctionInputs.ts b/packages/abi/src/coder/utils/getFunctionInputs.ts
new file mode 100644
index 00000000000..a71c8d96296
--- /dev/null
+++ b/packages/abi/src/coder/utils/getFunctionInputs.ts
@@ -0,0 +1,33 @@
+import { swayTypeMatchers } from '../../matchers/sway-type-matchers';
+import type { AbiFunctionInput } from '../../parser';
+
+export type FunctionInput = TArg & {
+ isOptional: boolean;
+};
+
+const isMandatoryInput = (input: AbiFunctionInput) =>
+ !swayTypeMatchers.void(input.type) && !swayTypeMatchers.option(input.type);
+
+/**
+ * Get the function inputs with the `isOptional` flag set to true for optional inputs.
+ *
+ * Optional parameters are ones which don't require inputs (void or Option)
+ * Given a function input of (u8, Option), the second input will be marked as optional as it could take a value or not.
+ *
+ * @param params - The function inputs.
+ * @returns an array of function inputs with the `isOptional` flag set to true for optional inputs.
+ */
+export const getFunctionInputs = (params: {
+ inputs: readonly AbiFunctionInput[];
+}): Array => {
+ const { inputs } = params;
+ let isMandatory = false;
+
+ /**
+ * We iterate over the inputs in reverse order to ensure that the first optional input is marked as such.
+ */
+ return inputs.reduceRight((result, input) => {
+ isMandatory = isMandatory || isMandatoryInput(input);
+ return [{ ...input, isOptional: !isMandatory }, ...result];
+ }, [] as FunctionInput[]);
+};
diff --git a/packages/abi/src/coder/utils/padValuesWithUndefined.ts b/packages/abi/src/coder/utils/padValuesWithUndefined.ts
new file mode 100644
index 00000000000..179c265037a
--- /dev/null
+++ b/packages/abi/src/coder/utils/padValuesWithUndefined.ts
@@ -0,0 +1,9 @@
+import type { InputValue } from '../encoding/encoding-types';
+
+export const padValuesWithUndefined = (values: InputValue[], inputs: ArrayLike) => {
+ if (values.length >= inputs.length) {
+ return values;
+ }
+
+ return [...values, ...Array(inputs.length - values.length).fill(undefined)];
+};
diff --git a/packages/abi/src/gen/abi-gen-types.ts b/packages/abi/src/gen/abi-gen-types.ts
new file mode 100644
index 00000000000..032c27a243f
--- /dev/null
+++ b/packages/abi/src/gen/abi-gen-types.ts
@@ -0,0 +1,55 @@
+import type { BinaryVersions } from '@fuel-ts/versions';
+
+import type { Abi } from '../parser';
+
+export interface AbiGenInput {
+ /**
+ * The details of the program to generate the files for.
+ */
+ programDetails: ProgramDetails[];
+ /**
+ * The versions of the binaries used to generate the files.
+ */
+ versions: BinaryVersions;
+ /**
+ * The mode to generate the files in.
+ * Defaults to 'ts' which generates typescript files.
+ */
+ mode?: 'ts';
+}
+
+export interface AbiGenResult {
+ /**
+ * The filename of the generated file.
+ */
+ filename: string;
+ /**
+ * The content of the generated file.
+ */
+ content: string;
+}
+
+export interface ProgramDetails {
+ /**
+ * The name of the program to generate files for.
+ * This will be used to name the generated files,
+ * as well as throughout the generated code.
+ */
+ name: string;
+ /**
+ * The compressed bytecode of the program in base64 format.
+ */
+ binCompressed?: string;
+ /**
+ * The abi of the program in the format returned via `AbiParser`.
+ */
+ abi: Abi;
+ /**
+ * The original abi contents in string format.
+ */
+ abiContents: string;
+ /**
+ * The storage slots, if working with a contract.
+ */
+ storageSlots?: string;
+}
diff --git a/packages/abi/src/gen/abi-gen.ts b/packages/abi/src/gen/abi-gen.ts
new file mode 100644
index 00000000000..db6b2d9f46a
--- /dev/null
+++ b/packages/abi/src/gen/abi-gen.ts
@@ -0,0 +1,19 @@
+import type { AbiGenInput, AbiGenResult } from './abi-gen-types';
+import { getRenderer } from './renderers/getRenderer';
+
+/**
+ * The main class to generate files for given sway programs.
+ * These contents of these generated files make it easier to interact
+ * with the sway programs, because type definitions are added,
+ * as well as some automatic loading is done for the user.
+ */
+export class AbiGen {
+ /**
+ * @returns an array of generated files for the given program details.
+ * They can be saved to disk as-is or further processed.
+ */
+ public static generate({ programDetails, mode, versions }: AbiGenInput): AbiGenResult[] {
+ const render = getRenderer(mode);
+ return render(programDetails, versions);
+ }
+}
diff --git a/packages/abi/src/gen/cli/index.ts b/packages/abi/src/gen/cli/index.ts
new file mode 100644
index 00000000000..939508c0f1b
--- /dev/null
+++ b/packages/abi/src/gen/cli/index.ts
@@ -0,0 +1 @@
+export { configureTypegenCliOptions, runTypegen } from './run';
diff --git a/packages/abi/src/gen/cli/run.ts b/packages/abi/src/gen/cli/run.ts
new file mode 100644
index 00000000000..ab87e9c61d4
--- /dev/null
+++ b/packages/abi/src/gen/cli/run.ts
@@ -0,0 +1,52 @@
+import { getBinaryVersions } from '@fuel-ts/versions/cli';
+import type { Command } from 'commander';
+import { mkdirSync, writeFileSync } from 'fs';
+import { join } from 'path';
+
+import { AbiGen } from '../abi-gen';
+
+import { getProgramDetails, loggingConfig } from './utils';
+
+export interface RunTypegenOptions {
+ inputs: string[];
+ output: string;
+ silent?: boolean;
+}
+
+export function runTypegen(options: RunTypegenOptions) {
+ const { inputs, output, silent } = options;
+
+ loggingConfig.silent = !!silent;
+
+ const programDetails = getProgramDetails(inputs);
+
+ const results = AbiGen.generate({ programDetails, versions: getBinaryVersions() });
+
+ mkdirSync(output, { recursive: true });
+
+ const subDirectories = new Set();
+
+ results.forEach((r) => {
+ const dir = r.filename.split('/').slice(0, -1).join('/');
+ if (dir !== '') {
+ subDirectories.add(dir);
+ }
+ });
+
+ subDirectories.forEach((dir) => {
+ mkdirSync(join(output, dir), { recursive: true });
+ });
+
+ results.forEach((r) => {
+ const outputPath = join(output, r.filename);
+ writeFileSync(outputPath, r.content);
+ });
+}
+
+export function configureTypegenCliOptions(program: Command) {
+ return program
+ .description(`Generate Typescript from forc build outputs`)
+ .requiredOption('-i, --inputs ', 'Input paths/globals to your ABI JSON files')
+ .requiredOption('-o, --output ', 'Directory path for generated files')
+ .action(runTypegen);
+}
diff --git a/packages/abi/src/gen/cli/utils.test.ts b/packages/abi/src/gen/cli/utils.test.ts
new file mode 100644
index 00000000000..394b52180f9
--- /dev/null
+++ b/packages/abi/src/gen/cli/utils.test.ts
@@ -0,0 +1,38 @@
+import { ErrorCode, FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { normalizeProjectName } from './utils';
+
+/**
+ * @group node
+ */
+describe('normalizeProjectName', () => {
+ test('should normalize project name', () => {
+ expect(normalizeProjectName('DsToken')).toEqual('DsToken');
+ expect(normalizeProjectName('test')).toEqual('Test');
+ expect(normalizeProjectName('ds-token')).toEqual('DsToken');
+ expect(normalizeProjectName('ds_token')).toEqual('DsToken');
+ expect(normalizeProjectName('Aaa_bbb_CCDD-EEE')).toEqual('AaaBbbCCDDEEE');
+ expect(normalizeProjectName('ds token')).toEqual('DsToken');
+ expect(normalizeProjectName('name.abi')).toEqual('NameAbi');
+ expect(normalizeProjectName('1234name.abi')).toEqual('NameAbi');
+ expect(normalizeProjectName('ERC20.abi')).toEqual('ERC20Abi');
+ expect(normalizeProjectName('my-contract')).toEqual('MyContract');
+ expect(normalizeProjectName('my contract')).toEqual('MyContract');
+ expect(normalizeProjectName('my.contract')).toEqual('MyContract');
+ expect(normalizeProjectName('still-my.contract')).toEqual('StillMyContract');
+ expect(normalizeProjectName('also my.contract')).toEqual('AlsoMyContract');
+ });
+
+ test('throws if name cannot be normalized', async () => {
+ await expectToThrowFuelError(
+ () => normalizeProjectName(''),
+ new FuelError(
+ ErrorCode.PARSE_FAILED,
+ `The provided string '' results in an empty output after`.concat(
+ ` normalization, therefore, it can't normalize string.`
+ )
+ )
+ );
+ });
+});
diff --git a/packages/abi/src/gen/cli/utils.ts b/packages/abi/src/gen/cli/utils.ts
new file mode 100644
index 00000000000..dd2f987b3cc
--- /dev/null
+++ b/packages/abi/src/gen/cli/utils.ts
@@ -0,0 +1,109 @@
+import { ErrorCode, FuelError } from '@fuel-ts/errors';
+import { assertUnreachable, compressBytecode, hexlify } from '@fuel-ts/utils';
+import { readFileSync } from 'fs';
+import { globSync } from 'glob';
+
+import type { AbiSpecification } from '../../parser';
+import { AbiParser } from '../../parser';
+import type { ProgramDetails } from '../abi-gen-types';
+
+export const loggingConfig = {
+ silent: false,
+};
+
+export function log(...args: Parameters) {
+ if (!loggingConfig.silent) {
+ // eslint-disable-next-line no-console
+ console.log(...args);
+ }
+}
+
+/**
+ * Converts `some.string-value` into `SomeStringValue`.
+ *
+ * Examples:
+ * my-simple.test —— MySimpleTest
+ * myFile.ts —— MyFileTs
+ * my-abi.json —— MyAbiJson
+ */
+export function normalizeProjectName(str: string): string {
+ const transformations: ((s: string) => string)[] = [
+ (s) => s.replace(/\s+/g, '-'), // spaces to -
+ (s) => s.replace(/\./g, '-'), // dots to -
+ (s) => s.replace(/_/g, '-'), // underscore to -
+ (s) => s.replace(/-[a-z]/g, (match) => match.slice(-1).toUpperCase()), // delete '-' and capitalize the letter after them
+ (s) => s.replace(/-/g, ''), // delete any '-' left
+ (s) => s.replace(/^\d+/, ''), // removes leading digits
+ ];
+
+ const output = transformations.reduce((s, t) => t(s), str);
+
+ if (output === '') {
+ const errMsg = `The provided string '${str}' results in an empty output after`.concat(
+ ` normalization, therefore, it can't normalize string.`
+ );
+ throw new FuelError(ErrorCode.PARSE_FAILED, errMsg);
+ }
+
+ return output[0].toUpperCase() + output.slice(1); // capitalize first letter
+}
+
+function handleMissingBinary(path: string, abi: AbiSpecification) {
+ const programType = abi.programType as 'predicate' | 'script' | 'contract' | 'library';
+ switch (programType) {
+ case 'predicate':
+ throw new FuelError(
+ ErrorCode.BIN_FILE_NOT_FOUND,
+ `For predicates, the bytecode is required. No bytecode found for predicate at ${path}.`
+ );
+ case 'script':
+ throw new FuelError(
+ ErrorCode.BIN_FILE_NOT_FOUND,
+ `For scripts, the bytecode is required. No bytecode found for script at ${path}.`
+ );
+ case 'contract':
+ log(`No bytecode found for contract at ${path}, will not generate ContractFactory for it.`);
+ break;
+ case 'library':
+ // ignore;
+ break;
+ default:
+ assertUnreachable(programType);
+ }
+}
+
+export function getProgramDetails(paths: string[]) {
+ const details: ProgramDetails[] = [];
+ paths.forEach((path) => {
+ const abiPath = path.match(/.+-abi\.json/) ? path : globSync(`${path}/*-abi.json`)[0];
+ if (abiPath === undefined) {
+ log(`No abi file found in ${path}, skipping this path.`);
+ return;
+ }
+
+ const dir = abiPath.match(/.*\//)?.[0] as string;
+ const projectName = abiPath.match(/([^/])+(?=-abi\.json)/)?.[0] as string;
+ const abiContentsStr = readFileSync(abiPath).toString();
+ const abi = JSON.parse(abiContentsStr) as AbiSpecification;
+
+ const [storageSlotsPath] = globSync(`${dir}/*-storage_slots.json`);
+ const storageSlots = storageSlotsPath ? readFileSync(storageSlotsPath).toString() : undefined;
+
+ const [binPath] = globSync(`${dir}/*.bin`);
+ if (binPath === undefined) {
+ handleMissingBinary(path, abi);
+ }
+
+ const binCompressed = binPath && compressBytecode(hexlify(readFileSync(binPath)));
+
+ details.push({
+ name: normalizeProjectName(projectName),
+ abi: AbiParser.parse(JSON.parse(abiContentsStr) as AbiSpecification),
+ binCompressed,
+ abiContents: abiContentsStr,
+ storageSlots,
+ });
+ });
+
+ return details;
+}
diff --git a/packages/abi/src/gen/hbs.d.ts b/packages/abi/src/gen/hbs.d.ts
new file mode 100644
index 00000000000..cefe640967a
--- /dev/null
+++ b/packages/abi/src/gen/hbs.d.ts
@@ -0,0 +1,5 @@
+// informs TS about Handlebar `.hbs` templates extension
+declare module '*.hbs' {
+ const value: string;
+ export default value;
+}
diff --git a/packages/abi/src/gen/index.ts b/packages/abi/src/gen/index.ts
new file mode 100644
index 00000000000..29b42ee4313
--- /dev/null
+++ b/packages/abi/src/gen/index.ts
@@ -0,0 +1,2 @@
+export { AbiGen } from './abi-gen';
+export * from './abi-gen-types';
diff --git a/packages/abi/src/gen/renderers/getRenderer.ts b/packages/abi/src/gen/renderers/getRenderer.ts
new file mode 100644
index 00000000000..bf43bb5d832
--- /dev/null
+++ b/packages/abi/src/gen/renderers/getRenderer.ts
@@ -0,0 +1,16 @@
+import { assertUnreachable } from '@fuel-ts/utils';
+
+import type { AbiGenInput } from '../abi-gen-types';
+
+import { renderTs } from './ts/render-ts';
+import type { Renderer } from './types';
+
+export function getRenderer(mode: AbiGenInput['mode']): Renderer {
+ switch (mode) {
+ case 'ts':
+ case undefined:
+ return renderTs;
+ default:
+ return assertUnreachable(mode);
+ }
+}
diff --git a/packages/abi/src/gen/renderers/ts/render-ts.ts b/packages/abi/src/gen/renderers/ts/render-ts.ts
new file mode 100644
index 00000000000..6654b4c6902
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/render-ts.ts
@@ -0,0 +1,17 @@
+import type { AbiGenResult, ProgramDetails } from '../../abi-gen-types';
+import type { Renderer } from '../types';
+
+import { renderPrograms } from './renderers/render-programs';
+import { templateRenderer } from './renderers/template-renderer';
+import commonTemplate from './templates/common.hbs';
+
+export const renderTs: Renderer = (details: ProgramDetails[], versions): AbiGenResult[] => {
+ const results = renderPrograms(details, versions);
+
+ results.push({
+ filename: 'common.ts',
+ content: templateRenderer({ template: commonTemplate, versions }),
+ });
+
+ return results;
+};
diff --git a/packages/abi/src/gen/renderers/ts/renderers/get-parent-dir-wrapper.ts b/packages/abi/src/gen/renderers/ts/renderers/get-parent-dir-wrapper.ts
new file mode 100644
index 00000000000..a8239ef814e
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/renderers/get-parent-dir-wrapper.ts
@@ -0,0 +1,32 @@
+import { assertUnreachable } from '@fuel-ts/utils';
+
+import type { Abi } from '../../../../parser';
+
+export function getParentDirWrapper(programType: Abi['programType']): {
+ parentDir: string;
+ withParentDir: (file: string) => string;
+ removeParentDir: (file: string) => string;
+} {
+ let parentDir: string = '';
+ switch (programType) {
+ case 'contract':
+ parentDir = 'contracts';
+ break;
+ case 'predicate':
+ parentDir = 'predicates';
+ break;
+ case 'script':
+ parentDir = 'scripts';
+ break;
+ case 'library':
+ break;
+ default:
+ assertUnreachable(programType);
+ }
+
+ return {
+ parentDir,
+ withParentDir: (file) => `${parentDir}/${file}`,
+ removeParentDir: (file) => file.split(`${parentDir}/`)[1],
+ };
+}
diff --git a/packages/abi/src/gen/renderers/ts/renderers/render-index-files.ts b/packages/abi/src/gen/renderers/ts/renderers/render-index-files.ts
new file mode 100644
index 00000000000..12c9c479871
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/renderers/render-index-files.ts
@@ -0,0 +1,73 @@
+import type { BinaryVersions } from '@fuel-ts/versions';
+
+import type { Abi } from '../../../../parser';
+import type { AbiGenResult } from '../../../abi-gen-types';
+import indexTemplate from '../templates/index.hbs';
+
+import { getParentDirWrapper } from './get-parent-dir-wrapper';
+import { templateRenderer } from './template-renderer';
+
+export type IndexContents = Map<
+ Abi['programType'],
+ { filename: string; exportedContent: string[] }[]
+>;
+/**
+ * @returns an array of index files
+ * that includes the root index.ts and the index.ts for each provided program type.
+ */
+export function renderIndexFiles(
+ indexContents: IndexContents,
+ versions: BinaryVersions
+): AbiGenResult[] {
+ const results: AbiGenResult[] = [];
+
+ indexContents.forEach((files, programType) => {
+ const { withParentDir, removeParentDir } = getParentDirWrapper(programType);
+
+ // from index.ts to e.g. contracts/index.ts
+ const indexFilename = withParentDir('index.ts');
+
+ const exports = files.map(({ filename, exportedContent }) => {
+ // from e.g. contracts/AbiContract.ts to AbiContract.ts
+ const relativePathToFile = removeParentDir(filename);
+ // remove .ts extension
+ return {
+ path: relativePathToFile.split('.')[0],
+ exportedContent: `{ ${exportedContent.join(', ')} }`,
+ };
+ });
+
+ const content = templateRenderer({
+ versions,
+ template: indexTemplate,
+ data: {
+ exports,
+ },
+ });
+
+ results.push({
+ filename: indexFilename,
+ content,
+ });
+ });
+
+ const mainIndexFileExports = [...indexContents.keys()]
+ .sort()
+ .map((programType) => getParentDirWrapper(programType).parentDir)
+ .map((path) => ({ path, exportedContent: '*' }));
+
+ const mainIndexFile: AbiGenResult = {
+ filename: 'index.ts',
+ content: templateRenderer({
+ versions,
+ template: indexTemplate,
+ data: {
+ exports: mainIndexFileExports,
+ },
+ }),
+ };
+
+ results.push(mainIndexFile);
+
+ return results;
+}
diff --git a/packages/abi/src/gen/renderers/ts/renderers/render-program.ts b/packages/abi/src/gen/renderers/ts/renderers/render-program.ts
new file mode 100644
index 00000000000..7bcb0c6a017
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/renderers/render-program.ts
@@ -0,0 +1,113 @@
+import { assertUnreachable } from '@fuel-ts/utils';
+import type { BinaryVersions } from '@fuel-ts/versions';
+
+import type { ProgramDetails } from '../../../abi-gen-types';
+import type { TsAbiGenResult } from '../../types';
+import abiTemplate from '../templates/abi.hbs';
+import bytecodeTemplate from '../templates/bytecode.hbs';
+import contractFactoryTemplate from '../templates/contract-factory.hbs';
+import contractTemplate from '../templates/contract.hbs';
+import predicateTemplate from '../templates/predicate.hbs';
+import scriptTemplate from '../templates/script.hbs';
+import storageSlotsTemplate from '../templates/storage-slots.hbs';
+
+import { getParentDirWrapper } from './get-parent-dir-wrapper';
+import { renderTypes } from './render-types';
+import { templateRenderer } from './template-renderer';
+
+/**
+ * Renders program-related files based on the program type.
+ * @returns An array of results containing filenames and their corresponding content.
+ * The files returned are all related to the program except the types.
+ * This includes the abi, bytecode, and the program-related classes.
+ */
+export function renderProgram(details: ProgramDetails, versions: BinaryVersions): TsAbiGenResult[] {
+ const { abi, binCompressed, name, abiContents, storageSlots } = details;
+
+ const results: TsAbiGenResult[] = [
+ {
+ filename: `${name}Types.ts`,
+ content: renderTypes(details, versions),
+ },
+ {
+ filename: `${name}-abi.ts`,
+ content: templateRenderer({ template: abiTemplate, versions, data: { abiContents } }),
+ },
+ ];
+
+ if (binCompressed) {
+ results.push({
+ filename: `${name}-bytecode.ts`,
+ content: templateRenderer({ template: bytecodeTemplate, versions, data: { binCompressed } }),
+ });
+ }
+
+ switch (abi.programType) {
+ case 'contract':
+ results.push(
+ {
+ filename: `${name}.ts`,
+ content: templateRenderer({
+ template: contractTemplate,
+ versions,
+ data: { name },
+ }),
+ exportInIndexFile: [name],
+ },
+
+ {
+ filename: `${name}-storage-slots.ts`,
+ content: templateRenderer({
+ template: storageSlotsTemplate,
+ versions,
+ data: { storageSlots },
+ }),
+ }
+ );
+
+ if (binCompressed) {
+ results.push({
+ filename: `${name}Factory.ts`,
+ content: templateRenderer({
+ template: contractFactoryTemplate,
+ versions,
+ data: { name },
+ }),
+ exportInIndexFile: [`${name}Factory`],
+ });
+ }
+ break;
+ case 'predicate':
+ results.push({
+ filename: `${name}.ts`,
+ content: templateRenderer({
+ template: predicateTemplate,
+ versions,
+ data: { name },
+ }),
+ exportInIndexFile: [name],
+ });
+ break;
+ case 'script':
+ results.push({
+ filename: `${name}.ts`,
+ content: templateRenderer({
+ template: scriptTemplate,
+ versions,
+ data: { name },
+ }),
+ exportInIndexFile: [name],
+ });
+ break;
+ case 'library':
+ // we do nothing for library
+ break;
+ default:
+ assertUnreachable(abi.programType);
+ break;
+ }
+
+ const { withParentDir } = getParentDirWrapper(abi.programType);
+
+ return results.map((r) => ({ ...r, filename: withParentDir(r.filename) }));
+}
diff --git a/packages/abi/src/gen/renderers/ts/renderers/render-programs.ts b/packages/abi/src/gen/renderers/ts/renderers/render-programs.ts
new file mode 100644
index 00000000000..269e90f8676
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/renderers/render-programs.ts
@@ -0,0 +1,37 @@
+import type { BinaryVersions } from '@fuel-ts/versions';
+
+import type { AbiGenResult, ProgramDetails } from '../../../abi-gen-types';
+
+import type { IndexContents } from './render-index-files';
+import { renderIndexFiles } from './render-index-files';
+import { renderProgram } from './render-program';
+
+/**
+ * For the given program details, render all program-related files.
+ * That includes the abi, bytecode, program-related classes,
+ * type files and the index files.
+ */
+export function renderPrograms(details: ProgramDetails[], versions: BinaryVersions) {
+ const results: AbiGenResult[] = [];
+ const indexContents: IndexContents = new Map();
+
+ for (const d of details) {
+ const files = renderProgram(d, versions);
+
+ results.push(...files);
+
+ files.forEach((file) => {
+ if (!file.exportInIndexFile?.length) {
+ return;
+ }
+
+ const contents = indexContents.get(d.abi.programType) ?? [];
+ contents.push({ filename: file.filename, exportedContent: file.exportInIndexFile });
+ indexContents.set(d.abi.programType, contents);
+ });
+ }
+
+ results.push(...renderIndexFiles(indexContents, versions));
+
+ return results;
+}
diff --git a/packages/abi/src/gen/renderers/ts/renderers/render-types.ts b/packages/abi/src/gen/renderers/ts/renderers/render-types.ts
new file mode 100644
index 00000000000..d8a88e2a362
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/renderers/render-types.ts
@@ -0,0 +1,120 @@
+import type { BinaryVersions } from '@fuel-ts/versions';
+
+import { createMatcher } from '../../../../matchers/sway-type-matchers';
+import type { Abi } from '../../../../parser';
+import { evaluateFunctionInputsOptionality } from '../../../../utils/evaluate-function-inputs-optionality';
+import type { ProgramDetails } from '../../../abi-gen-types';
+import typesTemplate from '../templates/types.hbs';
+import { generateTsType } from '../typers/generate-ts-type';
+import { flattenImports } from '../typers/helpers';
+import type { TyperReturn } from '../typers/types';
+
+import { templateRenderer } from './template-renderer';
+
+const metadataTypeFilter = createMatcher({
+ enum: true,
+ struct: true,
+ assetId: true,
+ string: false,
+ void: false,
+ bool: false,
+ u8: false,
+ u16: false,
+ u32: false,
+ u64: false,
+ u256: false,
+ b256: false,
+ generic: false,
+ stdString: false,
+ option: false,
+ result: false,
+ str: false,
+ b512: false,
+ bytes: false,
+ vector: false,
+ tuple: false,
+ array: false,
+ evmAddress: false,
+ rawUntypedSlice: false,
+});
+
+export function sortAlphabetically(a: TyperReturn, b: TyperReturn) {
+ if (a.input < b.input) {
+ return -1;
+ }
+ if (a.input > b.input) {
+ return 1;
+ }
+ return 0;
+}
+
+function mergeTypeImports(mTypes: TyperReturn[], cTypesMap: Record) {
+ const cTypes = Object.values(cTypesMap);
+
+ const imports = flattenImports(mTypes.concat(cTypes));
+
+ const fuelsTypeImports = [...new Set(imports.fuelsTypeImports)].sort().join(', ');
+
+ const commonTypeImports = [...new Set(imports.commonTypeImports)].sort().join(', ');
+
+ return { fuelsTypeImports, commonTypeImports };
+}
+
+function mapFunctions(abi: Abi, cTypes: Record) {
+ return abi.functions.map((fn) => {
+ const inputs = evaluateFunctionInputsOptionality(fn);
+
+ return {
+ name: fn.name,
+ inputs: `[${inputs.map((i) => `${i.name}${i.isOptional ? '?' : ''}: ${cTypes[i.type.concreteTypeId].input}`).join(', ')}]`,
+ output: cTypes[fn.output.concreteTypeId].output,
+ };
+ });
+}
+
+function mapConfigurables(abi: Abi, cTypes: Record) {
+ return abi.configurables.length > 0
+ ? abi.configurables.map(({ name, type }) => ({
+ name,
+ input: cTypes[type.concreteTypeId].input,
+ }))
+ : undefined;
+}
+
+/**
+ * Renders the types file for a program.
+ * @returns An object containing the filename and the content of the types file.
+ * The type rendering logic is the same for all program types.
+ */
+export function renderTypes(
+ { name: programName, abi }: ProgramDetails,
+ versions: BinaryVersions
+): string {
+ const mTypes = abi.metadataTypes
+ .filter(metadataTypeFilter)
+ .map((abiType) => generateTsType({ abiType }));
+
+ const cTypes = abi.concreteTypes.reduce>((res, abiType) => {
+ res[abiType.concreteTypeId] = generateTsType({ abiType, asReference: true });
+ return res;
+ }, {});
+
+ const { fuelsTypeImports, commonTypeImports } = mergeTypeImports(mTypes, cTypes);
+
+ const content = templateRenderer({
+ template: typesTemplate,
+ versions,
+ data: {
+ isContract: abi.programType === 'contract',
+ name: programName,
+ fuelsTypeImports,
+ commonTypeImports,
+ enums: mTypes.filter(({ tsType }) => tsType === 'enum').sort(sortAlphabetically),
+ types: mTypes.filter(({ tsType }) => tsType === 'type').sort(sortAlphabetically),
+ functions: mapFunctions(abi, cTypes),
+ configurables: mapConfigurables(abi, cTypes),
+ },
+ });
+
+ return content;
+}
diff --git a/packages/abi/src/gen/renderers/ts/renderers/template-renderer.ts b/packages/abi/src/gen/renderers/ts/renderers/template-renderer.ts
new file mode 100644
index 00000000000..f1a4ceaf7ea
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/renderers/template-renderer.ts
@@ -0,0 +1,29 @@
+import type { BinaryVersions } from '@fuel-ts/versions';
+import Handlebars from 'handlebars';
+
+import headerTemplate from '../templates/header.hbs';
+
+/*
+ Renders the given template w/ the given data, while injecting common
+ header for disabling lint rules and annotating Fuel component's versions.
+*/
+export function templateRenderer(params: {
+ template: string;
+ data?: Record;
+ versions: BinaryVersions;
+}) {
+ const { data, template, versions } = params;
+
+ const options = {
+ strict: true,
+ noEscape: true,
+ };
+
+ const renderHeaderTemplate = Handlebars.compile(headerTemplate, options);
+ const renderTemplate = Handlebars.compile(template, options);
+
+ return renderTemplate({
+ header: renderHeaderTemplate(versions),
+ ...(data ?? {}),
+ });
+}
diff --git a/packages/abi/src/gen/renderers/ts/templates/abi.hbs b/packages/abi/src/gen/renderers/ts/templates/abi.hbs
new file mode 100644
index 00000000000..0f1e0f35b16
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/abi.hbs
@@ -0,0 +1,5 @@
+{{header}}
+
+import { AbiSpecification } from 'fuels';
+
+export const abi: AbiSpecification = {{abiContents}};
diff --git a/packages/abi/src/gen/renderers/ts/templates/bytecode.hbs b/packages/abi/src/gen/renderers/ts/templates/bytecode.hbs
new file mode 100644
index 00000000000..5d859be0dd2
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/bytecode.hbs
@@ -0,0 +1,5 @@
+{{header}}
+
+import { decompressBytecode } from "fuels";
+
+export const bytecode = decompressBytecode("{{binCompressed}}");
\ No newline at end of file
diff --git a/packages/abi/src/gen/renderers/ts/templates/common.hbs b/packages/abi/src/gen/renderers/ts/templates/common.hbs
new file mode 100644
index 00000000000..bf8285b0218
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/common.hbs
@@ -0,0 +1,44 @@
+{{header}}
+
+import type { AbiCoderFunction, InvokeFunction } from 'fuels';
+
+/**
+ * Mimics Sway Enum.
+ * Requires one and only one Key-Value pair and raises error if more are provided.
+ */
+export type Enum = {
+ [K in keyof T]: Pick & { [P in Exclude]?: never };
+}[keyof T];
+
+/**
+ * Mimics Sway Option type.
+ */
+export type Option = T | undefined;
+
+/**
+ * Mimics Sway Result enum type.
+ * Ok represents the success case, while Err represents the error case.
+ */
+export type Result = Enum<{ Ok: T; Err: E }>;
+
+/**
+ * Mimics Sway array type. For example, [u64; 10] is converted to ArrayOfLength.
+ */
+export type ArrayOfLength<
+ T,
+ Length extends number,
+ Arr extends unknown[] = [],
+> = Arr['length'] extends Length ? Arr : ArrayOfLength;
+
+interface Types {
+ functions: Record;
+ configurables: Partial>;
+}
+
+export type ProgramFunctionMapper = {
+ [K in keyof T]: InvokeFunction;
+};
+
+export type InterfaceFunctionMapper = {
+ [K in keyof T]: AbiCoderFunction;
+};
\ No newline at end of file
diff --git a/packages/abi/src/gen/renderers/ts/templates/contract-factory.hbs b/packages/abi/src/gen/renderers/ts/templates/contract-factory.hbs
new file mode 100644
index 00000000000..b57f0ea8e33
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/contract-factory.hbs
@@ -0,0 +1,26 @@
+{{header}}
+
+import { ContractFactory } from 'fuels';
+import type { Account, Provider, DeployContractOptions } from 'fuels';
+import { {{name}} } from './{{name}}';
+import { bytecode } from './{{name}}-bytecode';
+import { abi } from './{{name}}-abi';
+import { storageSlots } from './{{name}}-storage-slots';
+
+export class {{name}}Factory extends ContractFactory<{{name}}> {
+
+ static readonly bytecode = bytecode;
+ static readonly storageSlots = storageSlots;
+
+ constructor(accountOrProvider: Account | Provider) {
+ super(bytecode, abi, accountOrProvider, {{name}}Factory.storageSlots);
+ }
+
+ static deploy (
+ wallet: Account,
+ options: DeployContractOptions = {}
+ ) {
+ const factory = new {{name}}Factory(wallet);
+ return factory.deploy(options);
+ }
+}
diff --git a/packages/abi/src/gen/renderers/ts/templates/contract.hbs b/packages/abi/src/gen/renderers/ts/templates/contract.hbs
new file mode 100644
index 00000000000..16de85e0f4c
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/contract.hbs
@@ -0,0 +1,33 @@
+{{header}}
+
+import { Contract, AbiCoder } from "fuels";
+import type { Address, Account, Provider } from 'fuels';
+import type { {{name}}Types as Types } from './{{name}}Types';
+import type { InterfaceFunctionMapper, ProgramFunctionMapper } from '../common';
+import { abi } from './{{name}}-abi';
+
+export * from './{{name}}Types';
+
+export type {{name}}Configurables = Types['configurables'];
+
+export class {{name}}AbiCoder extends AbiCoder {
+ declare functions: InterfaceFunctionMapper;
+
+ constructor() {
+ super(abi);
+ }
+}
+
+export class {{name}} extends Contract {
+ declare interface: {{name}}AbiCoder;
+ declare functions: ProgramFunctionMapper;
+
+ public static readonly abi = abi;
+
+ constructor(
+ id: string | Address,
+ accountOrProvider: Account | Provider,
+ ) {
+ super(id, abi, accountOrProvider);
+ }
+}
\ No newline at end of file
diff --git a/packages/abi/src/gen/renderers/ts/templates/header.hbs b/packages/abi/src/gen/renderers/ts/templates/header.hbs
new file mode 100644
index 00000000000..a8ec5578f8b
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/header.hbs
@@ -0,0 +1,9 @@
+/* Autogenerated file. Do not edit manually. */
+
+/* eslint-disable eslint-comments/no-unlimited-disable */
+/* eslint-disable */
+
+/*
+ Fuels version: {{FUELS}}
+ Forc version: {{FORC}}
+*/
diff --git a/packages/abi/src/gen/renderers/ts/templates/index.hbs b/packages/abi/src/gen/renderers/ts/templates/index.hbs
new file mode 100644
index 00000000000..f323c7973da
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/index.hbs
@@ -0,0 +1,5 @@
+{{header}}
+
+{{#each exports}}
+export {{exportedContent}} from './{{path}}';
+{{/each}}
\ No newline at end of file
diff --git a/packages/abi/src/gen/renderers/ts/templates/predicate.hbs b/packages/abi/src/gen/renderers/ts/templates/predicate.hbs
new file mode 100644
index 00000000000..2925628d4e5
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/predicate.hbs
@@ -0,0 +1,33 @@
+{{header}}
+
+import { Predicate } from 'fuels';
+import type { PredicateParams } from 'fuels';
+import { abi } from './{{name}}-abi';
+import { bytecode } from './{{name}}-bytecode';
+import type { {{name}}Types as Types } from './{{name}}Types';
+
+export * from './{{name}}Types';
+
+export type {{name}}Inputs = Types['inputs'];
+export type {{name}}Output = Types['output'];
+export type {{name}}Configurables = Types['configurables'];
+
+export type {{name}}Parameters = Omit<
+ PredicateParams<
+ Types['inputs'],
+ Types['configurables']
+ >,
+ 'abi' | 'bytecode'
+>;
+
+export class {{name}} extends Predicate<
+ Types['inputs'],
+ Types['configurables']
+> {
+ public static readonly abi = abi;
+ public static readonly bytecode = bytecode;
+
+ constructor(params: {{name}}Parameters) {
+ super({ abi, bytecode, ...params });
+ }
+}
\ No newline at end of file
diff --git a/packages/abi/src/gen/renderers/ts/templates/script.hbs b/packages/abi/src/gen/renderers/ts/templates/script.hbs
new file mode 100644
index 00000000000..b304d851616
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/script.hbs
@@ -0,0 +1,25 @@
+{{header}}
+
+import { Script } from 'fuels';
+import type { Account } from 'fuels';
+import { abi } from './{{name}}-abi';
+import { bytecode } from './{{name}}-bytecode';
+import type { {{name}}Types as Types } from './{{name}}Types';
+
+export * from './{{name}}Types';
+
+export type {{name}}Inputs = Types['inputs'];
+export type {{name}}Output = Types['output'];
+export type {{name}}Configurables = Types['configurables'];
+
+export class {{name}} extends Script<
+ Types['inputs'],
+ Types['output']
+> {
+ public static readonly abi = abi;
+ public static readonly bytecode = bytecode;
+
+ constructor(wallet: Account) {
+ super(bytecode, abi, wallet);
+ }
+}
\ No newline at end of file
diff --git a/packages/abi/src/gen/renderers/ts/templates/storage-slots.hbs b/packages/abi/src/gen/renderers/ts/templates/storage-slots.hbs
new file mode 100644
index 00000000000..a877acccda5
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/storage-slots.hbs
@@ -0,0 +1,5 @@
+{{header}}
+
+import type { StorageSlot } from 'fuels';
+
+export const storageSlots: StorageSlot[] = {{storageSlots}};
\ No newline at end of file
diff --git a/packages/abi/src/gen/renderers/ts/templates/types.hbs b/packages/abi/src/gen/renderers/ts/templates/types.hbs
new file mode 100644
index 00000000000..8624ec21820
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/templates/types.hbs
@@ -0,0 +1,36 @@
+{{header}}
+
+import type { {{fuelsTypeImports}} } from 'fuels';
+import type { {{commonTypeImports}} } from '../common';
+
+{{#each enums}}
+export enum {{input}};
+{{/each}}
+
+{{#each types}}
+export type {{input}};
+export type {{output}};
+{{/each}}
+
+export interface {{name}}Types {
+ {{#if isContract}}
+ functions: {
+ {{#each functions}}
+ {{name}}: {
+ inputs: {{inputs}};
+ output: {{output}};
+ };
+ {{/each}}
+ };
+ {{else}}
+ {{#each functions}}
+ inputs: {{inputs}};
+ output: {{output}};
+ {{/each}}
+ {{/if}}
+ configurables: {{#if configurables}}Partial<{
+ {{#each configurables}}
+ {{name}}: {{input}};
+ {{/each}}
+ }>{{else}}undefined{{/if}};
+}
\ No newline at end of file
diff --git a/packages/abi/src/gen/renderers/ts/typers/enums.ts b/packages/abi/src/gen/renderers/ts/typers/enums.ts
new file mode 100644
index 00000000000..4e583219d8b
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/enums.ts
@@ -0,0 +1,68 @@
+import { swayTypeMatchers, ENUM_REGEX } from '../../../../matchers/sway-type-matchers';
+
+import { structTyper } from './struct';
+import type { Typer, TyperAbiType } from './types';
+
+function isNativeEnum(abiType: TyperAbiType) {
+ return abiType.components?.every((t) => swayTypeMatchers.void(t.type)) === true;
+}
+
+export const enumTyper: Typer = (params, typer) => {
+ const { abiType } = params;
+ if (isNativeEnum(abiType)) {
+ const typeName = ENUM_REGEX.exec(abiType.swayType)?.[2] as string;
+
+ if (params.asReference) {
+ return { input: typeName, output: typeName };
+ }
+
+ const enumFields = abiType.components?.map((c) => `${c.name} = '${c.name}'`).join(', ');
+ const input = `${typeName} { ${enumFields} }`;
+ return {
+ input,
+ output: input,
+ tsType: 'enum',
+ };
+ }
+
+ return structTyper(params, typer);
+};
+
+export const optionTyper: Typer = ({ abiType }, typer) => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const { type } = abiType.components![1]!;
+ const some = typer({ abiType: type, asReference: true });
+ const input = `Option<${some.input}>`;
+ const output = `Option<${some.output}>`;
+ return {
+ input,
+ output,
+ commonTypeImports: ['Option'],
+ };
+};
+
+export const resultTyper: Typer = ({ abiType }, typer) => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const [{ type: ok }, { type: err }] = abiType.components!;
+ const mappedOk = typer({ abiType: ok, asReference: true });
+ const mappedErr = typer({ abiType: err, asReference: true });
+
+ const input = `Result<${mappedOk.input}, ${mappedErr.input}>`;
+ const output = `Result<${mappedOk.output}, ${mappedErr.output}>`;
+
+ const fuelsTypeImports = [
+ mappedOk.fuelsTypeImports ?? [],
+ mappedErr.fuelsTypeImports ?? [],
+ ].flat();
+ const commonTypeImports = [
+ mappedOk.commonTypeImports ?? [],
+ mappedErr.commonTypeImports ?? [],
+ ['Result'],
+ ].flat();
+ return {
+ input,
+ output,
+ fuelsTypeImports,
+ commonTypeImports,
+ };
+};
diff --git a/packages/abi/src/gen/renderers/ts/typers/generate-ts-type.ts b/packages/abi/src/gen/renderers/ts/typers/generate-ts-type.ts
new file mode 100644
index 00000000000..09ce1a1aa55
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/generate-ts-type.ts
@@ -0,0 +1,6 @@
+import { typerMatcher } from './typer-matcher';
+import type { TyperParams, TyperReturn } from './types';
+
+export function generateTsType(params: TyperParams): TyperReturn {
+ return typerMatcher(params.abiType)(params, generateTsType);
+}
diff --git a/packages/abi/src/gen/renderers/ts/typers/helpers.ts b/packages/abi/src/gen/renderers/ts/typers/helpers.ts
new file mode 100644
index 00000000000..892c6e1b62f
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/helpers.ts
@@ -0,0 +1,10 @@
+import type { TyperReturn } from './types';
+
+export function flattenImports(mapped: TyperReturn[]) {
+ const fuelsTypeImports = mapped.flatMap((m) => m.fuelsTypeImports).filter((x) => x !== undefined);
+ const commonTypeImports = mapped
+ .flatMap((m) => m.commonTypeImports)
+ .filter((x) => x !== undefined);
+
+ return { fuelsTypeImports, commonTypeImports };
+}
diff --git a/packages/abi/src/gen/renderers/ts/typers/iterators.ts b/packages/abi/src/gen/renderers/ts/typers/iterators.ts
new file mode 100644
index 00000000000..3ed4f99743d
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/iterators.ts
@@ -0,0 +1,37 @@
+import { ARRAY_REGEX } from '../../../../matchers/sway-type-matchers';
+import type { AbiTypeComponent } from '../../../../parser';
+
+import type { Typer } from './types';
+import { mapComponents } from './utils';
+
+export const tupleTyper: Typer = ({ abiType }, typer) =>
+ mapComponents({ parent: abiType, includeComponentNames: false, typer });
+
+export const arrayTyper: Typer = ({ abiType }, typer) => {
+ const length = ARRAY_REGEX.exec(abiType.swayType)?.[2];
+
+ const { type } = abiType.components?.[0] as AbiTypeComponent;
+ const mapped = typer({ abiType: type, asReference: true });
+
+ const input = `ArrayOfLength<${mapped.input}, ${length}>`;
+ const output = `ArrayOfLength<${mapped.output}, ${length}>`;
+
+ return {
+ input,
+ output,
+ fuelsTypeImports: mapped.fuelsTypeImports,
+ commonTypeImports: ['ArrayOfLength', ...(mapped.commonTypeImports ?? [])],
+ };
+};
+
+export const vectorTyper: Typer = ({ abiType }, typer) => {
+ const { type } = abiType.components?.[0] as AbiTypeComponent;
+ const mapped = typer({ abiType: type, asReference: true });
+ const input = `${mapped.input}[]`;
+ const output = `${mapped.output}[]`;
+ return {
+ ...mapped,
+ input,
+ output,
+ };
+};
diff --git a/packages/abi/src/gen/renderers/ts/typers/simple.ts b/packages/abi/src/gen/renderers/ts/typers/simple.ts
new file mode 100644
index 00000000000..9440f5aec51
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/simple.ts
@@ -0,0 +1,82 @@
+import { GENERIC_REGEX } from '../../../../matchers/sway-type-matchers';
+
+import type { TyperReturn, Typer } from './types';
+
+const numberTyperReturn: TyperReturn = {
+ input: 'BigNumberish',
+ output: 'number',
+ fuelsTypeImports: ['BigNumberish'],
+};
+
+export const u8Typer: Typer = () => numberTyperReturn;
+export const u16Typer = u8Typer;
+export const u32Typer = u8Typer;
+
+const u64TyperReturn: TyperReturn = {
+ input: 'BigNumberish',
+ output: 'BN',
+ fuelsTypeImports: ['BigNumberish', 'BN'],
+};
+export const u64Typer: Typer = () => u64TyperReturn;
+export const u256Typer: Typer = u64Typer;
+
+const boolTyperReturn = {
+ input: 'boolean',
+ output: 'boolean',
+};
+export const boolTyper: Typer = () => boolTyperReturn;
+
+const stringTyperReturn: TyperReturn = {
+ input: 'string',
+ output: 'string',
+};
+export const stringTyper: Typer = () => stringTyperReturn;
+export const b256Typer: Typer = stringTyper;
+export const b512Typer: Typer = stringTyper;
+
+const evmAddressTyperReturn: TyperReturn = {
+ input: 'EvmAddress',
+ output: 'EvmAddress',
+ fuelsTypeImports: ['EvmAddress'],
+};
+export const evmAddressTyper: Typer = () => evmAddressTyperReturn;
+
+const bytesTyperReturn: TyperReturn = {
+ input: 'Bytes',
+ output: 'Bytes',
+ fuelsTypeImports: ['Bytes'],
+};
+export const bytesTyper: Typer = () => bytesTyperReturn;
+
+const strTyperReturn: TyperReturn = {
+ input: 'StrSlice',
+ output: 'StrSlice',
+ fuelsTypeImports: ['StrSlice'],
+};
+export const strTyper: Typer = () => strTyperReturn;
+
+const rawSliceTyperReturn: TyperReturn = {
+ input: 'RawSlice',
+ output: 'RawSlice',
+ fuelsTypeImports: ['RawSlice'],
+};
+export const rawSliceTyper = () => rawSliceTyperReturn;
+
+const stdStringTyperReturn: TyperReturn = {
+ input: 'StdString',
+ output: 'StdString',
+ fuelsTypeImports: ['StdString'],
+};
+export const stdStringTyper: Typer = () => stdStringTyperReturn;
+
+const voidTyperReturn: TyperReturn = { input: 'undefined', output: 'void' };
+export const voidTyper: Typer = () => voidTyperReturn;
+
+export const genericTyper: Typer = ({ abiType }) => {
+ // extracts the `T` part from `generic T`
+ const typeName = GENERIC_REGEX.exec(abiType.swayType)?.[1] as string;
+ return {
+ input: typeName,
+ output: typeName,
+ };
+};
diff --git a/packages/abi/src/gen/renderers/ts/typers/struct.ts b/packages/abi/src/gen/renderers/ts/typers/struct.ts
new file mode 100644
index 00000000000..fbbcc5942fa
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/struct.ts
@@ -0,0 +1,88 @@
+import { ENUM_REGEX, STRUCT_REGEX } from '../../../../matchers/sway-type-matchers';
+import type { AbiMetadataType, AbiTypeArgument } from '../../../../parser';
+
+import { flattenImports } from './helpers';
+import type { TyperReturn, Typer, GlobalTyper, TyperAbiType } from './types';
+import { mapComponents } from './utils';
+
+function mapGenericTypeParameters(
+ typeArgs: AbiTypeArgument[] | AbiMetadataType['typeParameters'],
+ typer: GlobalTyper
+): TyperReturn {
+ if (!typeArgs) {
+ return {
+ input: '',
+ output: '',
+ };
+ }
+ const mapped = typeArgs.map((ta) => typer({ abiType: ta, asReference: true }));
+ const { fuelsTypeImports, commonTypeImports } = flattenImports(mapped);
+
+ const input = mapped.map((r) => r.input).join(', ');
+ const output = mapped.map((r) => r.output).join(', ');
+ return {
+ fuelsTypeImports,
+ commonTypeImports,
+ input: `<${input}>`,
+ output: `<${output}>`,
+ };
+}
+
+function getTypeNames(abiType: TyperAbiType) {
+ const typeName =
+ STRUCT_REGEX.exec(abiType.swayType)?.[2] ?? ENUM_REGEX.exec(abiType.swayType)?.[2];
+ return {
+ inputName: `${typeName}Input`,
+ outputName: `${typeName}Output`,
+ };
+}
+
+function mapStructAsReference(abiType: TyperAbiType, typer: GlobalTyper): TyperReturn {
+ const { inputName, outputName } = getTypeNames(abiType);
+
+ const typeArgs = mapGenericTypeParameters(
+ 'metadata' in abiType
+ ? abiType.metadata?.typeArguments
+ : (abiType as AbiMetadataType).typeParameters,
+ typer
+ );
+
+ return {
+ ...typeArgs,
+ input: `${inputName}${typeArgs.input}`,
+ output: `${outputName}${typeArgs.output}`,
+ };
+}
+
+export const structTyper: Typer = ({ abiType, asReference }, typer) => {
+ if ('metadata' in abiType || asReference) {
+ return mapStructAsReference(abiType, typer);
+ }
+
+ const { inputName, outputName } = getTypeNames(abiType);
+
+ const typeParameters = mapGenericTypeParameters(
+ (abiType as AbiMetadataType).typeParameters,
+ typer
+ );
+ const content = mapComponents({ parent: abiType, includeComponentNames: true, typer });
+
+ const inputType = `${inputName}${typeParameters.input}`;
+ const outputType = `${outputName}${typeParameters.output}`;
+
+ const input = `${inputType} = ${content.input}`;
+ let output = '';
+ if (content.input === content.output) {
+ output = `${outputType} = ${inputType}`;
+ } else {
+ output = `${outputType} = ${content.output}`;
+ }
+
+ return {
+ input,
+ output,
+ commonTypeImports: content.commonTypeImports,
+ fuelsTypeImports: content.fuelsTypeImports,
+ tsType: 'type',
+ };
+};
diff --git a/packages/abi/src/gen/renderers/ts/typers/typer-matcher.ts b/packages/abi/src/gen/renderers/ts/typers/typer-matcher.ts
new file mode 100644
index 00000000000..03da529b250
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/typer-matcher.ts
@@ -0,0 +1,51 @@
+import { createMatcher } from '../../../../matchers/sway-type-matchers';
+
+import { optionTyper, enumTyper, resultTyper } from './enums';
+import { tupleTyper, arrayTyper, vectorTyper } from './iterators';
+import {
+ boolTyper,
+ u8Typer,
+ u16Typer,
+ u32Typer,
+ u64Typer,
+ u256Typer,
+ b256Typer,
+ stringTyper,
+ evmAddressTyper,
+ genericTyper,
+ b512Typer,
+ bytesTyper,
+ rawSliceTyper,
+ stdStringTyper,
+ strTyper,
+ voidTyper,
+} from './simple';
+import { structTyper } from './struct';
+import type { Typer } from './types';
+
+export const typerMatcher = createMatcher({
+ bool: boolTyper,
+ u8: u8Typer,
+ u16: u16Typer,
+ u32: u32Typer,
+ u64: u64Typer,
+ u256: u256Typer,
+ b256: b256Typer,
+ b512: b512Typer,
+ tuple: tupleTyper,
+ array: arrayTyper,
+ struct: structTyper,
+ generic: genericTyper,
+ string: stringTyper,
+ vector: vectorTyper,
+ option: optionTyper,
+ bytes: bytesTyper,
+ str: strTyper,
+ rawUntypedSlice: rawSliceTyper,
+ stdString: stdStringTyper,
+ enum: enumTyper,
+ result: resultTyper,
+ void: voidTyper,
+ assetId: structTyper,
+ evmAddress: evmAddressTyper,
+});
diff --git a/packages/abi/src/gen/renderers/ts/typers/types.ts b/packages/abi/src/gen/renderers/ts/typers/types.ts
new file mode 100644
index 00000000000..e31a4c8f5e9
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/types.ts
@@ -0,0 +1,20 @@
+import type { AbiConcreteType, AbiTypeComponent, AbiMetadataType } from '../../../../parser';
+
+export interface TyperReturn {
+ input: string;
+ output: string;
+ fuelsTypeImports?: string[];
+ commonTypeImports?: string[];
+ tsType?: 'enum' | 'type';
+}
+
+export type TyperAbiType = AbiConcreteType | AbiMetadataType | AbiTypeComponent['type'];
+
+export type TyperParams = {
+ abiType: TyperAbiType;
+ asReference?: boolean;
+};
+
+export type GlobalTyper = (p: TyperParams) => TyperReturn;
+
+export type Typer = (params: TyperParams, typer: GlobalTyper) => TyperReturn;
diff --git a/packages/abi/src/gen/renderers/ts/typers/utils.ts b/packages/abi/src/gen/renderers/ts/typers/utils.ts
new file mode 100644
index 00000000000..9305850eb43
--- /dev/null
+++ b/packages/abi/src/gen/renderers/ts/typers/utils.ts
@@ -0,0 +1,81 @@
+import { assertUnreachable } from '@fuel-ts/utils';
+
+import { ENUM_REGEX, TUPLE_REGEX } from '../../../../matchers/sway-type-matchers';
+import type { AbiTypeComponent } from '../../../../parser';
+
+import { flattenImports } from './helpers';
+import type { TyperAbiType, GlobalTyper, TyperReturn } from './types';
+
+function componentMapper(
+ c: AbiTypeComponent,
+ includeName: boolean,
+ generateTsType: GlobalTyper
+): TyperReturn {
+ const mapped = generateTsType({ abiType: c.type, asReference: true });
+
+ if (!includeName) {
+ return mapped;
+ }
+
+ return {
+ ...mapped,
+ input: `${c.name}: ${mapped.input}`,
+ output: `${c.name}: ${mapped.output}`,
+ };
+}
+
+function wrapStructContent(text: string, wrap: '{}' | '[]' | 'Enum'): string {
+ switch (wrap) {
+ case '{}':
+ return `{ ${text} }`;
+ case '[]':
+ return `[${text}]`;
+ case 'Enum': {
+ const wrappedAsObj = wrapStructContent(text, '{}');
+ return `Enum<${wrappedAsObj}>`;
+ }
+ default:
+ return assertUnreachable(wrap);
+ }
+}
+
+/**
+ * This function maps components for a given parent type
+ * which can be a tuple or struct (and enum).
+ */
+export function mapComponents(params: {
+ parent: TyperAbiType;
+ /**
+ * Component names are included for structs and enums,
+ * but they're not included for tuples (we ignore the `__tuple_element` name).
+ */
+ includeComponentNames: boolean;
+ typer: GlobalTyper;
+}) {
+ const { parent, includeComponentNames, typer } = params;
+ const components = parent.components as AbiTypeComponent[];
+ const mapped = components.map((c) => componentMapper(c, includeComponentNames, typer));
+
+ // eslint-disable-next-line no-nested-ternary
+ const wrap = ENUM_REGEX.test(parent.swayType)
+ ? 'Enum'
+ : TUPLE_REGEX.test(parent.swayType)
+ ? '[]'
+ : '{}';
+
+ const input = wrapStructContent(mapped.map((m) => m.input).join(', '), wrap);
+ const output = wrapStructContent(mapped.map((m) => m.output).join(', '), wrap);
+
+ const { fuelsTypeImports, commonTypeImports } = flattenImports(mapped);
+
+ if (wrap === 'Enum') {
+ commonTypeImports.push('Enum');
+ }
+
+ return {
+ input,
+ output,
+ fuelsTypeImports,
+ commonTypeImports,
+ };
+}
diff --git a/packages/abi/src/gen/renderers/types.ts b/packages/abi/src/gen/renderers/types.ts
new file mode 100644
index 00000000000..5d18ccaa6e9
--- /dev/null
+++ b/packages/abi/src/gen/renderers/types.ts
@@ -0,0 +1,9 @@
+import type { BinaryVersions } from '@fuel-ts/versions';
+
+import type { AbiGenResult, ProgramDetails } from '../abi-gen-types';
+
+export type Renderer = (details: ProgramDetails[], versions: BinaryVersions) => AbiGenResult[];
+
+export type TsAbiGenResult = AbiGenResult & {
+ exportInIndexFile?: string[];
+};
diff --git a/packages/abi/src/index.ts b/packages/abi/src/index.ts
new file mode 100644
index 00000000000..c589c25a433
--- /dev/null
+++ b/packages/abi/src/index.ts
@@ -0,0 +1,3 @@
+export * from './parser';
+export * from './coder';
+export * from './gen';
diff --git a/packages/abi/src/matchers/sway-type-matchers.test.ts b/packages/abi/src/matchers/sway-type-matchers.test.ts
new file mode 100644
index 00000000000..ea6f8bfa77a
--- /dev/null
+++ b/packages/abi/src/matchers/sway-type-matchers.test.ts
@@ -0,0 +1,271 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import type { SwayType, swayTypeMatchers } from './sway-type-matchers';
+import { createMatcher } from './sway-type-matchers';
+
+const testMappings: Record = {
+ string: 'string-matched',
+ void: 'void-matched',
+ bool: 'bool-matched',
+ u8: 'u8-matched',
+ u16: 'u16-matched',
+ u32: 'u32-matched',
+ u64: 'u64-matched',
+ u256: 'u256-matched',
+ b256: 'b256-matched',
+ generic: 'generic-matched',
+ stdString: 'stdString-matched',
+ option: 'option-matched',
+ result: 'result-matched',
+ enum: 'enum-matched',
+ struct: 'struct-matched',
+ b512: 'b512-matched',
+ bytes: 'bytes-matched',
+ vector: 'vector-matched',
+ tuple: 'tuple-matched',
+ array: 'array-matched',
+ assetId: 'assetId-matched',
+ evmAddress: 'evmAddress-matched',
+ rawUntypedSlice: 'rawUntypedSlice-matched',
+ str: 'str-matched',
+};
+
+const matcher = createMatcher(testMappings);
+
+async function verifyOtherMatchersDontMatch(key: keyof typeof testMappings, swayType: string) {
+ const testMappingsWithoutKey = Object.fromEntries(
+ Object.entries(testMappings).filter(([k]) => k !== key)
+ );
+
+ const verifier = createMatcher(testMappingsWithoutKey as Record);
+
+ await expectToThrowFuelError(
+ () => verifier({ swayType }),
+ new FuelError(
+ FuelError.CODES.MATCHER_NOT_FOUND,
+ `Matcher not found for Sway type "${swayType}".`
+ )
+ );
+}
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('sway type matchers', () => {
+ test('void', async () => {
+ const key = 'void';
+ const swayType = '()';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('bool', async () => {
+ const key = 'bool';
+ const swayType = 'bool';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('u8', async () => {
+ const key = 'u8';
+ const swayType = 'u8';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('u16', async () => {
+ const key = 'u16';
+ const swayType = 'u16';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('u32', async () => {
+ const key = 'u32';
+ const swayType = 'u32';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('u64', async () => {
+ const key = 'u64';
+ const swayType = 'u64';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('u256', async () => {
+ const key = 'u256';
+ const swayType = 'u256';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('b256', async () => {
+ const key = 'b256';
+ const swayType = 'b256';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('string', async () => {
+ const key = 'string';
+ const swayType = 'str[5]';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('array', async () => {
+ const key = 'array';
+ const swayType = '[_; 3]';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('tuple', async () => {
+ const key = 'tuple';
+ const swayType = '(_, _, _)';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('struct', async () => {
+ const key = 'struct';
+ const swayType = 'struct MyStruct';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('assetId', async () => {
+ const key = 'assetId';
+ const swayType = 'struct std::asset_id::AssetId';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('b512', async () => {
+ const key = 'b512';
+ const swayType = 'struct std::b512::B512';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('assetId', async () => {
+ const key = 'assetId';
+ const swayType = 'struct std::asset_id::AssetId';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('bytes', async () => {
+ const key = 'bytes';
+ const swayType = 'struct std::bytes::Bytes';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('stdString', async () => {
+ const key = 'stdString';
+ const swayType = 'struct std::string::String';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('evmAddress', async () => {
+ const key = 'evmAddress';
+ const swayType = 'struct std::vm::evm::evm_address::EvmAddress';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('vector', async () => {
+ const key = 'vector';
+ const swayType = 'struct std::vec::Vec';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('enum', async () => {
+ const key = 'enum';
+ const swayType = 'enum MyEnum';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('option', async () => {
+ const key = 'option';
+ const swayType = 'enum std::option::Option';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('result', async () => {
+ const key = 'result';
+ const swayType = 'enum std::result::Result';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('rawUntypedSlice', async () => {
+ const key = 'rawUntypedSlice';
+ const swayType = 'raw untyped slice';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('generic', async () => {
+ const key = 'generic';
+ const swayType = 'generic T';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('str', async () => {
+ const key = 'str';
+ const swayType = 'str';
+
+ expect(matcher({ swayType })).toEqual(`${key}-matched`);
+ await verifyOtherMatchersDontMatch(key, swayType);
+ });
+
+ test('matcher without mapping for valid sway type throws', async () => {
+ const swayType = 'str';
+
+ // @ts-expect-error intentionally missing key for valid swayType
+ const matcherWithoutMappings = createMatcher({});
+
+ await expectToThrowFuelError(
+ () => matcherWithoutMappings({ swayType }),
+ new FuelError(
+ FuelError.CODES.MATCHER_NOT_FOUND,
+ `Matcher not found for Sway type "${swayType}".`
+ )
+ );
+ });
+});
diff --git a/packages/abi/src/matchers/sway-type-matchers.ts b/packages/abi/src/matchers/sway-type-matchers.ts
new file mode 100644
index 00000000000..ab47080192b
--- /dev/null
+++ b/packages/abi/src/matchers/sway-type-matchers.ts
@@ -0,0 +1,126 @@
+import { FuelError } from '@fuel-ts/errors';
+
+export type SwayType =
+ | 'void'
+ | 'bool'
+ | 'u8'
+ | 'u16'
+ | 'u32'
+ | 'u64'
+ | 'u256'
+ | 'b256'
+ | 'generic'
+ | 'string'
+ | 'str'
+ | 'stdString'
+ | 'option'
+ | 'result'
+ | 'enum'
+ | 'struct'
+ | 'b512'
+ | 'bytes'
+ | 'vector'
+ | 'tuple'
+ | 'array'
+ | 'assetId'
+ | 'evmAddress'
+ | 'rawUntypedSlice';
+
+export type Matcher = (opts: { swayType: string }) => T;
+
+const voidMatcher: Matcher = ({ swayType }) => swayType === '()';
+const bool: Matcher = ({ swayType }) => swayType === 'bool';
+const u8: Matcher = ({ swayType }) => swayType === 'u8';
+const u16: Matcher = ({ swayType }) => swayType === 'u16';
+const u32: Matcher = ({ swayType }) => swayType === 'u32';
+const u64: Matcher = ({ swayType }) => swayType === 'u64';
+const u256: Matcher = ({ swayType }) => swayType === 'u256';
+const b256: Matcher = ({ swayType }) => swayType === 'b256';
+
+export const GENERIC_REGEX = /^generic ([^\s]+)$/m;
+const generic: Matcher = ({ swayType }) => GENERIC_REGEX.test(swayType);
+
+export const STRING_REGEX = /^str\[(?[0-9]+)\]/;
+const string: Matcher = ({ swayType }) => STRING_REGEX.test(swayType);
+const str: Matcher = ({ swayType }) => swayType === 'str';
+
+export const TUPLE_REGEX = /^\((?.+)\)$/m;
+const tuple: Matcher = ({ swayType }) => TUPLE_REGEX.test(swayType);
+
+export const ARRAY_REGEX = /^\[(?- [\w\s\\[\]]+);\s*(?[0-9]+)\]/;
+const array: Matcher = ({ swayType }) => ARRAY_REGEX.test(swayType);
+
+export const STRUCT_REGEX = /^struct (.+::)?(?.+)$/m;
+const STRUCT_STD_REGEX =
+ /^struct std::.*(AssetId|B512|Vec|RawVec|EvmAddress|Bytes|String|RawBytes)$/m;
+const struct: Matcher = ({ swayType }) =>
+ STRUCT_REGEX.test(swayType) && !STRUCT_STD_REGEX.test(swayType);
+const assetId: Matcher = ({ swayType }) => swayType === 'struct std::asset_id::AssetId';
+const b512: Matcher = ({ swayType }) => swayType === 'struct std::b512::B512';
+const bytes: Matcher = ({ swayType }) => swayType === 'struct std::bytes::Bytes';
+const evmAddress: Matcher = ({ swayType }) =>
+ swayType === 'struct std::vm::evm::evm_address::EvmAddress';
+const stdString: Matcher = ({ swayType }) => swayType === 'struct std::string::String';
+const vector: Matcher = ({ swayType }) => swayType === 'struct std::vec::Vec';
+
+const option: Matcher = ({ swayType }) => swayType === 'enum std::option::Option';
+const result: Matcher = ({ swayType }) => swayType === 'enum std::result::Result';
+
+export const ENUM_REGEX = /^enum (.+::)?(?.+)$/m;
+const enumMatcher: Matcher = (opts) =>
+ !option(opts) && !result(opts) && ENUM_REGEX.test(opts.swayType);
+
+const rawUntypedSlice: Matcher = ({ swayType }) => swayType === 'raw untyped slice';
+
+export const swayTypeMatchers: Record = {
+ void: voidMatcher,
+ generic,
+ bool,
+ u8,
+ u16,
+ u32,
+ u64,
+ u256,
+ b256,
+
+ string,
+ str,
+ tuple,
+ array,
+
+ struct,
+ assetId,
+ b512,
+ bytes,
+ evmAddress,
+ stdString,
+ vector,
+
+ enum: enumMatcher,
+ option,
+ result,
+
+ rawUntypedSlice,
+};
+
+const swayTypeMatcherEntries = Object.entries(swayTypeMatchers);
+
+export function createMatcher(mappings: Record): Matcher {
+ return (opts: { swayType: string }): T => {
+ const { swayType } = opts;
+
+ for (const [key, matcher] of swayTypeMatcherEntries) {
+ if (matcher({ swayType })) {
+ if (key in mappings) {
+ return mappings[key as SwayType];
+ }
+ break;
+ }
+ }
+
+ throw new FuelError(
+ FuelError.CODES.MATCHER_NOT_FOUND,
+ `Matcher not found for Sway type "${swayType}".`
+ );
+ };
+}
diff --git a/packages/abi/src/parser/abi-parser.ts b/packages/abi/src/parser/abi-parser.ts
new file mode 100644
index 00000000000..31d230ce3cb
--- /dev/null
+++ b/packages/abi/src/parser/abi-parser.ts
@@ -0,0 +1,44 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import type { Abi } from './abi';
+import type { AbiSpecificationV1 } from './specifications';
+import { AbiParserV1 } from './specifications';
+
+/**
+ * A typed ABI object or a stringified json of a Sway program's ABI
+ */
+export type AbiSpecification = AbiSpecificationV1;
+
+export class AbiParser {
+ /**
+ * ABI specifications transpilers
+ */
+ private static specifications = {
+ '1': AbiParserV1.parse,
+ } as const;
+
+ /**
+ * Parses an ABI in JSON format.
+ *
+ * @param abi - a JSON ABI of a Sway program
+ * @returns an public interface for the Abi
+ */
+ static parse(abi: AbiSpecification): Abi {
+ if (typeof abi.specVersion !== 'string') {
+ throw new FuelError(
+ FuelError.CODES.ABI_SPECIFICATION_INVALID,
+ 'Invalid ABI: the specification version is not a string.'
+ );
+ }
+
+ const parse = AbiParser.specifications[abi.specVersion];
+ if (!parse) {
+ throw new FuelError(
+ FuelError.CODES.ABI_SPECIFICATION_INVALID,
+ `Invalid ABI: Unsupported ABI specification version ("${abi.specVersion}").`
+ );
+ }
+
+ return parse(abi);
+ }
+}
diff --git a/packages/abi/src/parser/abi.ts b/packages/abi/src/parser/abi.ts
new file mode 100644
index 00000000000..345851ec354
--- /dev/null
+++ b/packages/abi/src/parser/abi.ts
@@ -0,0 +1,142 @@
+/**
+ * This interface serves as a representation of the ABI format outputted by `forc build`
+ * that won't be changing with the introduction of new abi specifications in Sway.
+ * Its purpose is to provide a stable interface for users to work with,
+ * which won't be affected by changing ABI specification versions.
+ */
+export interface Abi {
+ encodingVersion: string;
+ programType: 'contract' | 'predicate' | 'script' | 'library';
+ /**
+ * Metadata types describe the structure of the types used in the `concreteTypes` field.
+ * One metadata type can be referenced multiple times if it is used in multiple concrete types.
+ */
+ metadataTypes: AbiMetadataType[];
+ /**
+ * Concrete types are types that are used in:
+ * function inputs/outputs, configurables, logged types, or message types.
+ *
+ * Their structure is fully known and they do not contain any unresolved generic parameters.
+ */
+ concreteTypes: AbiConcreteType[];
+ functions: AbiFunction[];
+ loggedTypes: AbiLoggedType[];
+ messageTypes: AbiMessageType[];
+ configurables: AbiConfigurable[];
+}
+
+export interface AbiConcreteType {
+ swayType: string;
+ concreteTypeId: string;
+ /**
+ * The components field is populated when the type is any non-primitive type.
+ * That includes structs, enums, arrays, and tuples.
+ */
+ components?: AbiTypeComponent[];
+ /**
+ * A concrete type can be an implementation of a metadata type,
+ * in which case the `metadata` field is populated.
+ * If the underlying metadata type has type parameters (is generic),
+ * the `typeArguments` field corresponds to those type parameters.
+ */
+ metadata?: {
+ metadataTypeId: number;
+ /**
+ * Type arguments used to resolve the type parameters in the metadata type.
+ * They are ordered in the same way as the type parameters in the metadata type.
+ */
+ typeArguments?: AbiConcreteType[];
+ };
+}
+
+export interface AbiMetadataType {
+ swayType: string;
+ metadataTypeId: number;
+ /**
+ * The components field is populated when the type is any non-primitive type.
+ * That includes structs, enums, arrays, and tuples.
+ */
+ components?: AbiTypeComponent[];
+ /**
+ * The existence of type parameters indicates that the metadata type is generic.
+ */
+ typeParameters?: AbiMetadataType[];
+}
+
+export interface AbiTypeComponent {
+ name: string;
+ type: AbiConcreteType | AbiAppliedMetadataType;
+}
+
+/**
+ * AbiAppliedMetadataType point to a metadata type but aren't the same as metadata types,
+ * as the metadata type describes the structure of the type,
+ * whereas the component is an actual implementation of that type.
+ */
+export interface AbiAppliedMetadataType {
+ swayType: string;
+ components?: AbiTypeComponent[];
+ metadata: {
+ metadataTypeId: number;
+ typeArguments?: AbiTypeArgument[];
+ };
+}
+
+export type AbiTypeArgument = AbiConcreteType | AbiAppliedMetadataType;
+
+export interface AbiFunctionInput {
+ name: string;
+ type: AbiConcreteType;
+}
+
+export interface AbiFunction {
+ name: string;
+ inputs: AbiFunctionInput[];
+ output: AbiConcreteType;
+ attributes?: readonly AbiFunctionAttribute[];
+}
+
+export interface AbiLoggedType {
+ logId: string;
+ type: AbiConcreteType;
+}
+
+export interface AbiMessageType {
+ messageId: string;
+ type: AbiConcreteType;
+}
+
+export interface AbiConfigurable {
+ name: string;
+ offset: number;
+ type: AbiConcreteType;
+}
+
+export type AbiFunctionAttribute =
+ | StorageAttr
+ | PayableAttr
+ | TestAttr
+ | InlineAttr
+ | DocCommentAttr;
+
+export interface PayableAttr {
+ readonly name: 'payable';
+}
+
+export interface StorageAttr {
+ readonly name: 'storage';
+ readonly arguments: readonly ('read' | 'write')[];
+}
+
+export interface TestAttr {
+ readonly name: 'test';
+}
+export interface InlineAttr {
+ readonly name: 'inline';
+ readonly arguments: 'never' | 'always';
+}
+
+export interface DocCommentAttr {
+ readonly name: 'doc-comment';
+ readonly arguments: readonly string[];
+}
diff --git a/packages/abi/src/parser/index.ts b/packages/abi/src/parser/index.ts
new file mode 100644
index 00000000000..053e0c0a003
--- /dev/null
+++ b/packages/abi/src/parser/index.ts
@@ -0,0 +1,3 @@
+export { AbiParser, type AbiSpecification } from './abi-parser';
+export * from './abi';
+export * from './specifications/v1/specification';
diff --git a/packages/abi/src/parser/specifications/index.ts b/packages/abi/src/parser/specifications/index.ts
new file mode 100644
index 00000000000..788ae99a778
--- /dev/null
+++ b/packages/abi/src/parser/specifications/index.ts
@@ -0,0 +1,2 @@
+export { AbiParserV1 } from './v1/parser';
+export * from './v1/specification';
diff --git a/packages/abi/src/parser/specifications/v1/abi-type-mappers.ts b/packages/abi/src/parser/specifications/v1/abi-type-mappers.ts
new file mode 100644
index 00000000000..5dad5545b11
--- /dev/null
+++ b/packages/abi/src/parser/specifications/v1/abi-type-mappers.ts
@@ -0,0 +1,94 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+import type {
+ AbiConcreteType,
+ AbiMetadataType,
+ AbiTypeArgument,
+ AbiTypeComponent,
+} from '../../abi';
+
+import type { ResolvableComponent, ResolvableType } from './resolvable-type';
+import type { ResolvedType } from './resolved-type';
+
+function mapMetadata(type: ResolvableType | ResolvedType) {
+ const result: AbiTypeComponent['type']['metadata'] = {
+ metadataTypeId: type.metadataType?.metadataTypeId as number,
+ };
+
+ if (type.typeParamsArgsMap && type.metadataType?.typeParameters?.length) {
+ result.typeArguments = [...type.typeParamsArgsMap.values()].map((rt) => toTypeArgument(rt));
+ }
+
+ return result;
+}
+
+function isResolvedConcreteType(
+ type: ResolvableType | ResolvedType
+): type is ResolvedType & { typeId: string } {
+ const isResolvedType = 'typeId' in type;
+
+ return isResolvedType && typeof type.typeId === 'string';
+}
+
+function mapComponentType(component: ResolvableComponent): AbiTypeComponent {
+ const { name, type } = component;
+
+ let result: AbiTypeComponent['type'];
+
+ if (isResolvedConcreteType(type)) {
+ result = {
+ swayType: type.swayType,
+ concreteTypeId: type.typeId,
+ };
+ if (type.metadataType) {
+ result.metadata = mapMetadata(type) as AbiConcreteType['metadata'];
+ }
+ } else {
+ result = {
+ swayType: type.swayType,
+ metadata: mapMetadata(type),
+ };
+ }
+
+ if (type.components) {
+ result.components = type.components.map(mapComponentType);
+ }
+
+ return { name, type: result };
+}
+
+function toTypeArgument(type: ResolvableType | ResolvedType): AbiTypeArgument {
+ // type args and components follow the same mapping logic
+ return mapComponentType({ name: '', type }).type;
+}
+
+export function toAbiType(t: ResolvableType | ResolvedType): AbiConcreteType | AbiMetadataType {
+ let result: AbiConcreteType | AbiMetadataType;
+
+ if (isResolvedConcreteType(t)) {
+ result = {
+ concreteTypeId: t.typeId,
+ swayType: t.swayType,
+ };
+
+ if (t.metadataType) {
+ result.metadata = mapMetadata(t) as AbiConcreteType['metadata'];
+ }
+ } else {
+ result = {
+ swayType: t.swayType,
+ metadataTypeId: t.metadataType?.metadataTypeId as number,
+ };
+
+ if (t.typeParamsArgsMap && t.metadataType?.typeParameters?.length) {
+ result.typeParameters = [...t.typeParamsArgsMap.values()].map(
+ (rt) => toAbiType(rt) as AbiMetadataType
+ );
+ }
+ }
+
+ if (t.components) {
+ result.components = t.components.map(mapComponentType);
+ }
+
+ return result;
+}
diff --git a/packages/abi/src/parser/specifications/v1/cleanup-abi.ts b/packages/abi/src/parser/specifications/v1/cleanup-abi.ts
new file mode 100644
index 00000000000..1d517f43707
--- /dev/null
+++ b/packages/abi/src/parser/specifications/v1/cleanup-abi.ts
@@ -0,0 +1,52 @@
+import type { AbiSpecificationV1 } from './specification';
+
+/**
+ * Both RawVec and RawBytes are private sway std library types
+ * that can never be used directly in sway,
+ * and the only reason they're in the abi is because they're used internally by Vec and Bytes
+ * and not ignored when forc builds the outputs.
+ * We can safely ignore them and simplify the `Vec` and `Bytes` types.
+ * This makes it simpler for us to consume these types in typegen and coder,
+ * as well as for others consuming the parsed abi,
+ * who now don't have to worry about this unnecessary complexity.
+ * `raw untyped ptr` is also in the abi only because of RawVec and RawBytes,
+ * so we ignore that as well.
+ */
+const IGNORED_TYPES = ['struct std::vec::RawVec', 'struct std::bytes::RawBytes', 'raw untyped ptr'];
+
+export function cleanupAbi(abi: AbiSpecificationV1): AbiSpecificationV1 {
+ return {
+ ...abi,
+ metadataTypes: abi.metadataTypes
+ .filter((metadataType) => !IGNORED_TYPES.includes(metadataType.type))
+ .map((metadataType) => {
+ switch (metadataType.type) {
+ /**
+ * Vectors consist of multiple components,
+ * but we only care about the `buf`'s first type argument
+ * which defines the type of the vector data.
+ * Everything else is being ignored,
+ * as it's then easier to reason about the vector.
+ */
+ case 'struct std::vec::Vec':
+ return {
+ ...metadataType,
+ components: metadataType.components?.[0].typeArguments,
+ };
+
+ /**
+ * We treat Bytes as a special type
+ * that is handled only based on its type name ('struct std::bytes::Bytes')
+ * and not its components.
+ */
+ case 'struct std::bytes::Bytes':
+ return {
+ type: metadataType.type,
+ metadataTypeId: metadataType.metadataTypeId,
+ };
+ default:
+ return metadataType;
+ }
+ }),
+ };
+}
diff --git a/packages/abi/src/parser/specifications/v1/map-attribute.ts b/packages/abi/src/parser/specifications/v1/map-attribute.ts
new file mode 100644
index 00000000000..b14ea2e83c0
--- /dev/null
+++ b/packages/abi/src/parser/specifications/v1/map-attribute.ts
@@ -0,0 +1,23 @@
+import { assertUnreachable } from '@fuel-ts/utils';
+
+import type { AbiFunctionAttribute } from '../../abi';
+
+import type { AbiFunctionAttributeV1 } from './specification';
+
+export const mapAttribute = (attribute: AbiFunctionAttributeV1): AbiFunctionAttribute => {
+ const { name, arguments: args } = attribute;
+
+ switch (name) {
+ case 'inline':
+ return { name, arguments: args[0] };
+ case 'storage':
+ return { name: 'storage', arguments: args };
+ case 'doc-comment':
+ return { name, arguments: args };
+ case 'payable':
+ case 'test':
+ return { name };
+ default:
+ return assertUnreachable(attribute);
+ }
+};
diff --git a/packages/abi/src/parser/specifications/v1/parser.ts b/packages/abi/src/parser/specifications/v1/parser.ts
new file mode 100644
index 00000000000..0048ea802f3
--- /dev/null
+++ b/packages/abi/src/parser/specifications/v1/parser.ts
@@ -0,0 +1,75 @@
+import type { Abi, AbiConcreteType, AbiMetadataType } from '../../abi';
+
+import { toAbiType } from './abi-type-mappers';
+import { cleanupAbi } from './cleanup-abi';
+import { mapAttribute } from './map-attribute';
+import { ResolvableType } from './resolvable-type';
+import { ResolvedType } from './resolved-type';
+import type {
+ AbiConfigurableV1,
+ AbiFunctionInputV1,
+ AbiFunctionV1,
+ AbiLoggedTypeV1,
+ AbiMessageTypeV1,
+ AbiSpecificationV1,
+} from './specification';
+
+export class AbiParserV1 {
+ static parse(abi: AbiSpecificationV1): Abi {
+ const cleanAbi = cleanupAbi(abi);
+
+ const abiTypeMaps = {
+ metadataTypes: new Map(cleanAbi.metadataTypes.map((type) => [type.metadataTypeId, type])),
+ concreteTypes: new Map(cleanAbi.concreteTypes.map((type) => [type.concreteTypeId, type])),
+ };
+
+ const resolvableTypes = cleanAbi.metadataTypes.map(
+ (metadataType) => new ResolvableType(abiTypeMaps, metadataType.metadataTypeId, undefined)
+ );
+
+ const concreteTypes = cleanAbi.concreteTypes.map((concreteType) => {
+ const resolvableType = resolvableTypes.find(
+ (resolvable) => resolvable.metadataTypeId === concreteType.metadataTypeId
+ );
+
+ const resolvedType = resolvableType
+ ? resolvableType.resolve(concreteType)
+ : new ResolvedType({ swayType: concreteType.type, typeId: concreteType.concreteTypeId });
+
+ return toAbiType(resolvedType) as AbiConcreteType;
+ });
+
+ const getType = (concreteTypeId: string) =>
+ // this will always be defined because it's in the context of the same ABI
+ concreteTypes.find((abiType) => abiType.concreteTypeId === concreteTypeId) as AbiConcreteType;
+
+ return {
+ metadataTypes: resolvableTypes.map((rt) => toAbiType(rt) as AbiMetadataType),
+ concreteTypes,
+ encodingVersion: cleanAbi.encodingVersion,
+ programType: cleanAbi.programType as Abi['programType'],
+ functions: cleanAbi.functions.map((fn: AbiFunctionV1) => ({
+ attributes: fn.attributes?.map(mapAttribute) ?? undefined,
+ name: fn.name,
+ output: getType(fn.output),
+ inputs: fn.inputs.map((input: AbiFunctionInputV1) => ({
+ name: input.name,
+ type: getType(input.concreteTypeId),
+ })),
+ })),
+ loggedTypes: cleanAbi.loggedTypes.map((loggedType: AbiLoggedTypeV1) => ({
+ logId: loggedType.logId,
+ type: getType(loggedType.concreteTypeId),
+ })),
+ messageTypes: cleanAbi.messagesTypes.map((messageType: AbiMessageTypeV1) => ({
+ messageId: messageType.messageId,
+ type: getType(messageType.concreteTypeId),
+ })),
+ configurables: cleanAbi.configurables.map((configurable: AbiConfigurableV1) => ({
+ name: configurable.name,
+ offset: configurable.offset,
+ type: getType(configurable.concreteTypeId),
+ })),
+ };
+ }
+}
diff --git a/packages/abi/src/parser/specifications/v1/resolvable-type.ts b/packages/abi/src/parser/specifications/v1/resolvable-type.ts
new file mode 100644
index 00000000000..94aa367dc19
--- /dev/null
+++ b/packages/abi/src/parser/specifications/v1/resolvable-type.ts
@@ -0,0 +1,373 @@
+import { FuelError } from '@fuel-ts/errors';
+
+import { swayTypeMatchers } from '../../../matchers/sway-type-matchers';
+
+import type { ResolvedComponent } from './resolved-type';
+import { ResolvedType } from './resolved-type';
+import type {
+ AbiComponentV1,
+ AbiConcreteTypeV1,
+ AbiMetadataTypeV1,
+ AbiTypeArgumentV1,
+} from './specification';
+
+export interface ResolvableComponent {
+ name: string;
+ type: ResolvableType | ResolvedType;
+}
+
+export class ResolvableType {
+ metadataType: AbiMetadataTypeV1;
+ swayType: string;
+ components: ResolvableComponent[] | undefined;
+
+ constructor(
+ private abiTypeMaps: {
+ metadataTypes: Map;
+ concreteTypes: Map;
+ },
+ public metadataTypeId: number,
+ public typeParamsArgsMap: Map | undefined
+ ) {
+ this.metadataType = this.findMetadataType(metadataTypeId);
+ this.swayType = this.metadataType.type;
+ this.typeParamsArgsMap ??=
+ this.metadataType.typeParameters &&
+ new Map(
+ this.metadataType.typeParameters.map((typeParameter) => [
+ typeParameter,
+ new ResolvableType(this.abiTypeMaps, typeParameter, undefined),
+ ])
+ );
+
+ this.components = this.metadataType.components?.map((c) =>
+ this.createResolvableComponent(this, c)
+ );
+ }
+
+ /**
+ * Find a metadata type by its ID.
+ * @param metadataTypeId - The ID of the metadata type to find.
+ * @returns The metadata type.
+ *
+ * @throws If the metadata type can not be found in the ABI.
+ */
+ private findMetadataType(metadataTypeId: number): AbiMetadataTypeV1 {
+ const metadataType = this.abiTypeMaps.metadataTypes.get(metadataTypeId);
+
+ if (!metadataType) {
+ throw new FuelError(
+ FuelError.CODES.TYPE_NOT_FOUND,
+ `Metadata type with id ${metadataTypeId} not found`
+ );
+ }
+ return metadataType;
+ }
+
+ /**
+ * Find a concrete type by its ID.
+ * @param concreteTypeId - The ID of the concrete type to find.
+ * @returns The concrete type.
+ *
+ * @throws If the concrete type can not be found in the ABI.
+ */
+ private findConcreteType(concreteTypeId: string): AbiConcreteTypeV1 {
+ const concreteType = this.abiTypeMaps.concreteTypes.get(concreteTypeId);
+
+ if (!concreteType) {
+ throw new FuelError(
+ FuelError.CODES.TYPE_NOT_FOUND,
+ `Concrete type with id ${concreteTypeId} not found`
+ );
+ }
+ return concreteType;
+ }
+
+ private static mapTypeParametersAndArgs(
+ metadataType: AbiMetadataTypeV1,
+ args: (ResolvableType | ResolvedType)[]
+ ): Map | undefined {
+ return (
+ metadataType.typeParameters &&
+ new Map(
+ metadataType.typeParameters.map((typeParameter, idx) => [typeParameter, args[idx]])
+ )
+ );
+ }
+
+ private createResolvableComponent(
+ parent: ResolvableType,
+ { typeId, typeArguments, name }: AbiComponentV1 | AbiTypeArgumentV1
+ ): ResolvableComponent {
+ const isConcreteType = typeof typeId === 'string';
+
+ if (isConcreteType) {
+ const concreteType = this.findConcreteType(typeId);
+ return {
+ name,
+ type: this.resolveConcreteType(concreteType),
+ };
+ }
+
+ const metadataType = this.findMetadataType(typeId);
+ return {
+ name,
+ type: this.handleMetadataType(parent, metadataType, typeArguments),
+ };
+ }
+
+ /**
+ * Concrete types are *resolved* because everything is known about them.
+ */
+ private resolveConcreteType(type: AbiConcreteTypeV1): ResolvedType {
+ /**
+ * If the concrete type doesn't have a linked metadata type, we can resolve it immediately.
+ * This is the case for e.g. u8, u16, ...
+ */
+ if (type.metadataTypeId === undefined) {
+ return new ResolvedType({
+ swayType: type.type,
+ typeId: type.concreteTypeId,
+ });
+ }
+ /**
+ * The concrete type has an associated metadata type.
+ * If it's not generic (no type arguments),
+ * we'll create a ResolvableType with that metadata type, and then resolve it immediately.
+ * This would be the case for e.g. non-generic structs and enums.
+ */
+ if (!type.typeArguments) {
+ return new ResolvableType(this.abiTypeMaps, type.metadataTypeId, undefined).resolveInternal(
+ type.concreteTypeId,
+ undefined
+ );
+ }
+
+ /**
+ * The concrete type's underlying metadata type is generic.
+ * We must resolve all its type parameters with the provided type arguments of the concrete type,
+ * and then resolve the metadata type itself.
+ */
+ const metadataType = this.findMetadataType(type.metadataTypeId);
+
+ const concreteTypeArgs = type.typeArguments.map((typeArgument) => {
+ const concreteTypeArg = this.findConcreteType(typeArgument);
+ return this.resolveConcreteType(concreteTypeArg);
+ });
+
+ return new ResolvableType(
+ this.abiTypeMaps,
+ type.metadataTypeId,
+ ResolvableType.mapTypeParametersAndArgs(metadataType, concreteTypeArgs)
+ ).resolveInternal(type.concreteTypeId, undefined);
+ }
+
+ /**
+ * Metadata types are *handled* and not *resolved* because they might be generic,
+ * in which case they cannot be resolved.
+ * If they're not generic, they can be immediately resolved.
+ */
+ private handleMetadataType(
+ parent: ResolvableType,
+ metadataType: AbiMetadataTypeV1,
+ typeArguments: AbiComponentV1['typeArguments']
+ ): ResolvableType | ResolvedType {
+ /**
+ * If the type is generic, we can't resolve it and thus we create a `ResolvableType` from it.
+ * This propagates to the parent type, forcing it to be a `ResolvableType` as well,
+ * as it can't be resolved until this generic type is substituted with a type argument.
+ */
+ if (swayTypeMatchers.generic({ swayType: metadataType.type })) {
+ /**
+ * This search solves the case where an e.g. `generic T` is being substituted by `generic E`.
+ * This can happen when a generic type is nested in another generic type and they have differently-named type parameters.
+ * e.g. `GenericStruct` is nested in `Vec`: `struct MyStruct { a: Vec }`
+ * We check in the parent's typeParamsArgsMap if the metadata type we're solving for
+ * has been substituted with a different generic type, and then we use that generic type.
+ */
+ const resolvableTypeParameter = parent.typeParamsArgsMap?.get(metadataType.metadataTypeId);
+
+ return (
+ resolvableTypeParameter ??
+ new ResolvableType(this.abiTypeMaps, metadataType.metadataTypeId, undefined)
+ );
+ }
+
+ if (!metadataType.components) {
+ /**
+ * types like u8, u16 can make their way into metadata types
+ * if they aren't used _directly_ in a function-input/function-output/log/configurable/messageType
+ * These types are characterized by not having components and we can resolve them as-is
+ */
+ return new ResolvableType(
+ this.abiTypeMaps,
+ metadataType.metadataTypeId,
+ undefined
+ ).resolveInternal(metadataType.metadataTypeId, undefined);
+ }
+
+ const typeArgs = typeArguments?.map(
+ (typeArgument) => this.createResolvableComponent(parent, typeArgument).type
+ );
+
+ const resolvable = new ResolvableType(
+ this.abiTypeMaps,
+ metadataType.metadataTypeId,
+ !typeArgs?.length
+ ? undefined
+ : ResolvableType.mapTypeParametersAndArgs(metadataType, typeArgs)
+ );
+
+ /**
+ * If any component is unresolved, this means that the metadata type is generic.
+ * We can't resolve it yet, so we return the resolvable type.
+ * If all components are resolved, we can resolve the metadata type immediately.
+ */
+ const isGeneric = resolvable.components?.some(
+ (component) => component.type instanceof ResolvableType
+ );
+
+ return isGeneric
+ ? resolvable
+ : resolvable.resolveInternal(metadataType.metadataTypeId, undefined);
+ }
+
+ private resolveInternal(
+ typeId: string | number,
+ typeParamsArgsMap: Map | undefined
+ ): ResolvedType {
+ const resolvedType = new ResolvedType({
+ swayType: this.swayType,
+ typeId,
+ metadataType: this.metadataType,
+ });
+
+ /**
+ * A type without components can be immediately resolved.
+ */
+ if (!this.components) {
+ return resolvedType;
+ }
+
+ /**
+ * Before resolving the components,
+ * we need to substitute the type parameters of the underlying metadata type
+ * with the type arguments of the concrete type,
+ * so that we can substitute the generic components with them later.
+ */
+ const typeArgs = this.resolveTypeArgs(typeParamsArgsMap);
+
+ const components: ResolvedComponent[] = this.components.map((component) => {
+ const { name, type } = component;
+
+ if (type instanceof ResolvedType) {
+ return component as ResolvedComponent;
+ }
+
+ /**
+ * Here the component's type is a `ResolvableType`.
+ * If the component is a generic type parameter itself,
+ * its corresponding type argument will be found in the typeArgs,
+ * which will be used to substitute the component with.
+ */
+ const resolvedGenericType = typeArgs?.get(type.metadataTypeId);
+
+ if (resolvedGenericType) {
+ return {
+ name,
+ type: resolvedGenericType,
+ };
+ }
+
+ return {
+ name,
+ /**
+ * The component is a `ResolvableType`, but it's not a generic type parameter itself.
+ * This means that one of its components (or component's components)
+ * is a generic type.
+ * We need to resolve that first before resolving the component.
+ *
+ * Note that we are passing in the original `typeParamsArgsMap` by default,
+ * which will be used to substitute the component's generic type parameters
+ * with the appropriate type arguments.
+ *
+ * The non-default case of passing `typeArgs` happens only for tuples/arrays
+ * which contain structs with implicit generics,
+ * e.g. `(bool, StructWithImplicitGenerics)`
+ */
+ type: type.resolveInternal(type.metadataTypeId, typeParamsArgsMap ?? typeArgs),
+ };
+ });
+
+ resolvedType.components = components;
+ resolvedType.typeParamsArgsMap = typeArgs;
+
+ return resolvedType;
+ }
+
+ private resolveTypeArgs(
+ typeParamsArgsMap: Map | undefined
+ ): Map | undefined {
+ /**
+ * This case only happens when the metadata type is *implicitly* generic.
+ * The type itself doesn't have any type parameters that should be resolved,
+ * but its components are still generic types.
+ * This happens in the following type:
+ * `struct StructWithImplicitGenerics { a: [E; 3], b: (E, F)}`.
+ */
+ if (this.typeParamsArgsMap === undefined) {
+ return typeParamsArgsMap;
+ }
+
+ const newMap = new Map();
+
+ /**
+ * We resolve the type parameters of the underlying metadata type
+ * with the type arguments of the concrete type.
+ */
+ this.typeParamsArgsMap.forEach((arg, typeParameter) => {
+ /**
+ * Some type parameters can already be resolved
+ * e.g. `struct MyStruct { a: DoubleGeneric }`
+ * where the second type parameter of DoubleGeneric is already known.
+ */
+ if (arg instanceof ResolvedType) {
+ newMap.set(typeParameter, arg);
+ return;
+ }
+
+ /**
+ * The type parameter is either directly substituted with a type argument,
+ * or it's a metadata type which accepts the type argument,
+ * so that metadata type will be resolved and subsitute the type parameter.
+ */
+ const resolved =
+ typeParamsArgsMap?.get(arg.metadataTypeId) ??
+ arg.resolveInternal(arg.metadataTypeId, typeParamsArgsMap);
+
+ newMap.set(arg.metadataTypeId, resolved);
+ });
+
+ return newMap;
+ }
+
+ /**
+ * Resolves the instance of `ResolvableType` with the specific concrete type's data.
+ * @returns a `ResolvedType` in which all its components are resolved.
+ */
+ public resolve(concreteType: AbiConcreteTypeV1): ResolvedType {
+ const concreteTypeArgs = concreteType.typeArguments?.map((typeArgument) => {
+ const concreteTypeArg = this.findConcreteType(typeArgument);
+ return this.resolveConcreteType(concreteTypeArg);
+ });
+
+ const typeParamsArgsMap = concreteTypeArgs
+ ? (ResolvableType.mapTypeParametersAndArgs(this.metadataType, concreteTypeArgs) as Map<
+ number,
+ ResolvedType
+ >)
+ : undefined;
+
+ return this.resolveInternal(concreteType.concreteTypeId, typeParamsArgsMap);
+ }
+}
diff --git a/packages/abi/src/parser/specifications/v1/resolved-type.ts b/packages/abi/src/parser/specifications/v1/resolved-type.ts
new file mode 100644
index 00000000000..6465d10f6d5
--- /dev/null
+++ b/packages/abi/src/parser/specifications/v1/resolved-type.ts
@@ -0,0 +1,28 @@
+import type { AbiMetadataTypeV1 } from './specification';
+
+export interface ResolvedComponent {
+ name: string;
+ type: ResolvedType;
+}
+
+export class ResolvedType {
+ public swayType: string;
+ public typeId: string | number;
+ public components: ResolvedComponent[] | undefined;
+ public typeParamsArgsMap: Map | undefined;
+ public metadataType: AbiMetadataTypeV1 | undefined;
+
+ constructor(params: {
+ swayType: string;
+ typeId: string | number;
+ components?: ResolvedComponent[];
+ typeParamsArgsMap?: Map;
+ metadataType?: AbiMetadataTypeV1;
+ }) {
+ this.swayType = params.swayType;
+ this.typeId = params.typeId;
+ this.components = params.components;
+ this.typeParamsArgsMap = params.typeParamsArgsMap;
+ this.metadataType = params.metadataType;
+ }
+}
diff --git a/packages/abi/src/parser/specifications/v1/specification.ts b/packages/abi/src/parser/specifications/v1/specification.ts
new file mode 100644
index 00000000000..f360fbba8bb
--- /dev/null
+++ b/packages/abi/src/parser/specifications/v1/specification.ts
@@ -0,0 +1,98 @@
+/**
+ * Types for Fuel JSON ABI Format specification v1, as defined on:
+ * https://github.com/FuelLabs/fuel-specs/blob/master/src/abi/json-abi-format.md
+ */
+export interface AbiSpecificationV1 {
+ readonly specVersion: '1';
+ readonly encodingVersion: string;
+ readonly programType: string;
+ readonly concreteTypes: readonly AbiConcreteTypeV1[];
+ readonly metadataTypes: readonly AbiMetadataTypeV1[];
+ readonly functions: readonly AbiFunctionV1[];
+ readonly loggedTypes: readonly AbiLoggedTypeV1[];
+ readonly messagesTypes: readonly AbiMessageTypeV1[];
+ readonly configurables: readonly AbiConfigurableV1[];
+}
+
+export interface AbiConcreteTypeV1 {
+ readonly type: string;
+ readonly concreteTypeId: string;
+ readonly metadataTypeId?: number;
+ readonly typeArguments?: readonly string[];
+}
+
+export interface AbiMetadataTypeV1 {
+ readonly type: string;
+ readonly metadataTypeId: number;
+ readonly components?: readonly AbiComponentV1[];
+ readonly typeParameters?: readonly number[];
+}
+
+export interface AbiComponentV1 extends AbiTypeArgumentV1 {}
+
+export interface AbiTypeArgumentV1 {
+ readonly name: string;
+ readonly typeId: number | string; // the type metadata declaration ID or type concrete declaration hash based ID of the type of the component.
+ readonly typeArguments?: readonly AbiTypeArgumentV1[];
+}
+
+export interface AbiFunctionV1 {
+ readonly name: string;
+ readonly inputs: readonly AbiFunctionInputV1[];
+ readonly output: string;
+ readonly attributes: readonly AbiFunctionAttributeV1[] | null;
+}
+
+export interface AbiFunctionInputV1 {
+ readonly name: string;
+ readonly concreteTypeId: string;
+}
+
+export interface AbiLoggedTypeV1 {
+ readonly logId: string;
+ // the _type concrete declaration_ hash based ID of the value being logged.
+ readonly concreteTypeId: string;
+}
+
+export interface AbiMessageTypeV1 {
+ readonly messageId: string;
+ readonly concreteTypeId: string;
+}
+
+export interface AbiConfigurableV1 {
+ readonly name: string;
+ readonly concreteTypeId: string;
+ readonly offset: number;
+}
+
+export type AbiFunctionAttributeV1 =
+ | StorageAttrV1
+ | PayableAttrV1
+ | TestAttrV1
+ | InlineAttrV1
+ | DocCommentAttrV1;
+
+export interface PayableAttrV1 {
+ readonly name: 'payable';
+ readonly arguments: readonly [];
+}
+
+export interface StorageAttrV1 {
+ readonly name: 'storage';
+ readonly arguments: readonly ('read' | 'write')[];
+}
+
+export interface TestAttrV1 {
+ readonly name: 'test';
+ readonly arguments: readonly [];
+}
+
+export interface InlineAttrV1 {
+ readonly name: 'inline';
+ readonly arguments: readonly ['never'] | readonly ['always'];
+}
+
+export interface DocCommentAttrV1 {
+ readonly name: 'doc-comment';
+ readonly arguments: string[];
+}
diff --git a/packages/abi/src/utils/evaluate-function-inputs-optionality.ts b/packages/abi/src/utils/evaluate-function-inputs-optionality.ts
new file mode 100644
index 00000000000..0664e920df2
--- /dev/null
+++ b/packages/abi/src/utils/evaluate-function-inputs-optionality.ts
@@ -0,0 +1,15 @@
+import { swayTypeMatchers } from '../matchers/sway-type-matchers';
+import type { AbiFunction, AbiFunctionInput } from '../parser';
+
+export function evaluateFunctionInputsOptionality(
+ fn: AbiFunction
+): (AbiFunctionInput & { isOptional: boolean })[] {
+ let isMandatory = false;
+ return fn.inputs.reduceRight<(AbiFunctionInput & { isOptional: boolean })[]>((result, input) => {
+ const isTypeMandatory =
+ !swayTypeMatchers.void(input.type) && !swayTypeMatchers.option(input.type);
+
+ isMandatory = isMandatory || isTypeMandatory;
+ return [{ ...input, isOptional: !isMandatory }, ...result];
+ }, []);
+}
diff --git a/packages/abi/test/encoding/AbiCoder.test.ts b/packages/abi/test/encoding/AbiCoder.test.ts
new file mode 100644
index 00000000000..db86b96c5e1
--- /dev/null
+++ b/packages/abi/test/encoding/AbiCoder.test.ts
@@ -0,0 +1,361 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { AbiCoder } from '../../src';
+import { v1 as specificationV1 } from '../fixtures/v1';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('AbiCoder', () => {
+ describe('intialisation', () => {
+ it('should create an ABI coder', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ expect(coder).toBeDefined();
+ expect(coder.abi).toBeInstanceOf(Object);
+ expect(coder.specification).toEqual(specificationV1);
+
+ expect(coder.functions).toBeInstanceOf(Object);
+ expect(coder.logs).toBeInstanceOf(Object);
+ expect(coder.configurables).toBeInstanceOf(Object);
+
+ expect(coder.getFunction).toBeInstanceOf(Function);
+ expect(coder.getLog).toBeInstanceOf(Function);
+ expect(coder.getType).toBeInstanceOf(Function);
+ expect(coder.getConfigurable).toBeInstanceOf(Function);
+ });
+ });
+
+ describe('getFunction', () => {
+ describe('properties', () => {
+ it('should get a function and return the correct values', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ const fn = coder.getFunction('types_u8');
+
+ expect(fn).toBeDefined();
+ expect(fn.name).toBe('types_u8');
+ expect(fn.inputs).toStrictEqual([
+ {
+ name: 'x',
+ type: {
+ concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b',
+ swayType: 'u8',
+ },
+ },
+ ]);
+ expect(fn.signature).toStrictEqual('types_u8(u8)');
+ expect(fn.selector).toStrictEqual('0x00000000469feadd');
+ expect(fn.selectorBytes).toStrictEqual(
+ new Uint8Array([0, 0, 0, 0, 0, 0, 0, 8, 116, 121, 112, 101, 115, 95, 117, 56])
+ );
+ expect(fn.attributes).toStrictEqual([]);
+ expect(fn.isReadOnly()).toBe(true);
+ });
+
+ it('should get a function by name', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ const fn = coder.getFunction('configurables');
+
+ expect(fn).toBeDefined();
+ expect(fn.name).toStrictEqual('configurables');
+ expect(fn.signature).toStrictEqual('configurables()');
+ expect(fn.selector).toStrictEqual('0x00000000fdaf4480');
+ });
+
+ it('should get a function by signature', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ const fn = coder.getFunction('configurables()');
+
+ expect(fn).toBeDefined();
+ expect(fn.name).toStrictEqual('configurables');
+ expect(fn.signature).toStrictEqual('configurables()');
+ expect(fn.selector).toStrictEqual('0x00000000fdaf4480');
+ });
+
+ it('should get a function by selector', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ const fn = coder.getFunction('0x00000000fdaf4480');
+
+ expect(fn).toBeDefined();
+ expect(fn.name).toStrictEqual('configurables');
+ expect(fn.signature).toStrictEqual('configurables()');
+ expect(fn.selector).toStrictEqual('0x00000000fdaf4480');
+ });
+
+ it('should throw an error if the function is not found', async () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ await expectToThrowFuelError(
+ () => coder.getFunction('unknown'),
+ new FuelError(
+ FuelError.CODES.FUNCTION_NOT_FOUND,
+ 'Unable to find function with the name or signature or selector of "unknown".'
+ )
+ );
+ });
+ });
+
+ describe('getFunction::encodeArguments', () => {
+ it('should encode the arguments correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const fn = coder.getFunction('types_u8');
+
+ const encoded = fn.encodeArguments([1]);
+
+ expect(encoded).toStrictEqual(new Uint8Array([1]));
+ });
+
+ it('should pad the arguments with undefined if the number of arguments is less than the expected number', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const fn = coder.getFunction('types_void');
+
+ const encoded = fn.encodeArguments([]);
+
+ expect(encoded).toStrictEqual(new Uint8Array([]));
+ });
+
+ it('should throw an error if the number of arguments is less than the expected number', async () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const fn = coder.getFunction('types_u8');
+
+ await expectToThrowFuelError(
+ () => fn.encodeArguments([]),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid number of arguments. Expected a minimum of 1 arguments, received 0'
+ )
+ );
+ });
+ });
+
+ describe('decodeArguments', () => {
+ it('should decode the arguments correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const fn = coder.getFunction('types_u8');
+
+ const decoded = fn.decodeArguments(new Uint8Array([1]));
+
+ expect(decoded).toStrictEqual([1]);
+ });
+
+ it('should arrayify the arguments', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const fn = coder.getFunction('types_u8');
+
+ const decoded = fn.decodeArguments('0x01');
+
+ expect(decoded).toStrictEqual([1]);
+ });
+ });
+
+ describe('encodeOutput', () => {
+ it('should encode the output correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const fn = coder.getFunction('types_u8');
+
+ const encoded = fn.encodeOutput(1);
+
+ expect(encoded).toStrictEqual(new Uint8Array([1]));
+ });
+ });
+
+ describe('decodeOutput', () => {
+ it('should decode the output correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const fn = coder.getFunction('types_u8');
+
+ const decoded = fn.decodeOutput(new Uint8Array([1]));
+
+ expect(decoded).toStrictEqual(1);
+ });
+
+ it('should arrayify the output', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const fn = coder.getFunction('types_u8');
+
+ const decoded = fn.decodeOutput('0x01');
+
+ expect(decoded).toStrictEqual(1);
+ });
+ });
+ });
+
+ describe('getConfigurable', () => {
+ describe('properties', () => {
+ it('should get a configurable by name', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ const configurable = coder.getConfigurable('U8_VALUE');
+
+ expect(configurable).toBeDefined();
+ expect(configurable.name).toBe('U8_VALUE');
+ expect(configurable.offset).toBe(113392);
+ expect(configurable.encode).toBeDefined();
+ expect(configurable.decode).toBeDefined();
+ });
+
+ it('should throw an error if the configurable is not found', async () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ await expectToThrowFuelError(
+ () => coder.getConfigurable('unknown'),
+ new FuelError(
+ FuelError.CODES.CONFIGURABLE_NOT_FOUND,
+ "Configurable with name 'unknown' doesn't exist in the ABI."
+ )
+ );
+ });
+ });
+
+ describe('encode', () => {
+ it('should encode the value correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const configurable = coder.getConfigurable('U8_VALUE');
+
+ const encoded = configurable.encode(1);
+
+ expect(encoded).toStrictEqual(new Uint8Array([1]));
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode the value correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const configurable = coder.getConfigurable('U8_VALUE');
+
+ const decoded = configurable.decode(new Uint8Array([1]));
+
+ expect(decoded).toStrictEqual(1);
+ });
+
+ it('should arrayify the value correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const configurable = coder.getConfigurable('U8_VALUE');
+
+ const decoded = configurable.decode('0x01');
+
+ expect(decoded).toStrictEqual(1);
+ });
+ });
+ });
+
+ describe('getLog', () => {
+ it('should get a log by id', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ const log = coder.getLog('8961848586872524460');
+
+ expect(log).toBeDefined();
+ });
+
+ it('should throw an error if the log is not found', async () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ await expectToThrowFuelError(
+ () => coder.getLog('unknown'),
+ new FuelError(
+ FuelError.CODES.LOG_TYPE_NOT_FOUND,
+ `Log type with logId 'unknown' doesn't exist in the ABI.`
+ )
+ );
+ });
+
+ describe('encode', () => {
+ it('should encode the value correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const log = coder.getLog('8961848586872524460');
+ const expected = new Uint8Array([
+ -1, 58, 156, 113, 34, 38, 148, 86, 145, 26, 238, 58, 41, 126, 101, 222, 26, 221, 83, 216,
+ 61, 217, 86, 108, 149, 69, 160, 219, 244, 168, 219, 162,
+ ]);
+ const value = '0xff3a9c7122269456911aee3a297e65de1add53d83dd9566c9545a0dbf4a8dba2';
+
+ const encoded = log.encode(value);
+
+ expect(encoded).toStrictEqual(expected);
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode the value correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const log = coder.getLog('8961848586872524460');
+ const expected = '0xff3a9c7122269456911aee3a297e65de1add53d83dd9566c9545a0dbf4a8dba2';
+ const data = new Uint8Array([
+ -1, 58, 156, 113, 34, 38, 148, 86, 145, 26, 238, 58, 41, 126, 101, 222, 26, 221, 83, 216,
+ 61, 217, 86, 108, 149, 69, 160, 219, 244, 168, 219, 162, 209,
+ ]);
+
+ const decoded = log.decode(data);
+
+ expect(decoded).toStrictEqual(expected);
+ });
+ });
+ });
+
+ describe('getType', () => {
+ it('should get a type by concreteTypeId', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ const type = coder.getType(
+ '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b'
+ );
+
+ expect(type).toBeDefined();
+ });
+
+ it('should throw an error if the type is not found', async () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+
+ await expectToThrowFuelError(
+ () => coder.getType('unknown'),
+ new FuelError(
+ FuelError.CODES.TYPE_NOT_FOUND,
+ `Type with concreteTypeId 'unknown' doesn't exist in the ABI.`
+ )
+ );
+ });
+
+ describe('encode', () => {
+ it('should encode the value correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const type = coder.getType(
+ '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b'
+ );
+ const expected = new Uint8Array([
+ -1, 58, 156, 113, 34, 38, 148, 86, 145, 26, 238, 58, 41, 126, 101, 222, 26, 221, 83, 216,
+ 61, 217, 86, 108, 149, 69, 160, 219, 244, 168, 219, 162,
+ ]);
+ const value = '0xff3a9c7122269456911aee3a297e65de1add53d83dd9566c9545a0dbf4a8dba2';
+
+ const encoded = type.encode(value);
+
+ expect(encoded).toStrictEqual(expected);
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode the value correctly', () => {
+ const coder = AbiCoder.fromAbi(specificationV1);
+ const type = coder.getType(
+ '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b'
+ );
+ const expected = '0xff3a9c7122269456911aee3a297e65de1add53d83dd9566c9545a0dbf4a8dba2';
+ const data = new Uint8Array([
+ -1, 58, 156, 113, 34, 38, 148, 86, 145, 26, 238, 58, 41, 126, 101, 222, 26, 221, 83, 216,
+ 61, 217, 86, 108, 149, 69, 160, 219, 244, 168, 219, 162, 209,
+ ]);
+
+ const decoded = type.decode(data);
+
+ expect(decoded).toStrictEqual(expected);
+ });
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/AbiEncoding.test.ts b/packages/abi/test/encoding/AbiEncoding.test.ts
new file mode 100644
index 00000000000..b0830ed9dab
--- /dev/null
+++ b/packages/abi/test/encoding/AbiEncoding.test.ts
@@ -0,0 +1,136 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import type { CoderFactoryParameters } from '../../src/coder/encoding';
+import { encoding } from '../../src/coder/encoding';
+import { coders as v1 } from '../../src/coder/encoding/v1';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('encoding', () => {
+ describe('from', () => {
+ it('should create a encoding instance just fine', () => {
+ expect(encoding).toBeDefined();
+
+ // Current version (v1)
+ expect(encoding.array).toEqual(v1.array);
+ expect(encoding.enum).toEqual(v1.enum);
+ expect(encoding.option).toEqual(v1.option);
+ expect(encoding.vector).toEqual(v1.vector);
+ expect(encoding.u8).toEqual(v1.u8);
+ expect(encoding.u16).toEqual(v1.u16);
+ expect(encoding.u32).toEqual(v1.u32);
+ expect(encoding.u64).toEqual(v1.u64);
+ expect(encoding.u256).toEqual(v1.u256);
+ expect(encoding.b256).toEqual(v1.b256);
+ expect(encoding.b512).toEqual(v1.b512);
+ expect(encoding.bool).toEqual(v1.bool);
+ expect(encoding.void).toEqual(v1.void);
+ expect(encoding.bytes).toEqual(v1.bytes);
+ expect(encoding.rawSlice).toEqual(v1.rawSlice);
+ expect(encoding.str).toEqual(v1.str);
+ expect(encoding.stdString).toEqual(v1.stdString);
+ expect(encoding.string).toEqual(v1.string);
+ expect(encoding.vector).toEqual(v1.vector);
+ expect(encoding.array).toEqual(v1.array);
+ expect(encoding.tuple).toEqual(v1.tuple);
+ expect(encoding.struct).toEqual(v1.struct);
+ expect(encoding.enum).toEqual(v1.enum);
+ expect(encoding.option).toEqual(v1.option);
+
+ // V1
+ expect(encoding.v1.array).toEqual(v1.array);
+ expect(encoding.v1.enum).toEqual(v1.enum);
+ expect(encoding.v1.option).toEqual(v1.option);
+ expect(encoding.v1.vector).toEqual(v1.vector);
+ expect(encoding.v1.u8).toEqual(v1.u8);
+ expect(encoding.v1.u16).toEqual(v1.u16);
+ expect(encoding.v1.u32).toEqual(v1.u32);
+ expect(encoding.v1.u64).toEqual(v1.u64);
+ expect(encoding.v1.u256).toEqual(v1.u256);
+ expect(encoding.v1.b256).toEqual(v1.b256);
+ expect(encoding.v1.b512).toEqual(v1.b512);
+ expect(encoding.v1.bool).toEqual(v1.bool);
+ expect(encoding.v1.void).toEqual(v1.void);
+ expect(encoding.v1.bytes).toEqual(v1.bytes);
+ expect(encoding.v1.rawSlice).toEqual(v1.rawSlice);
+ expect(encoding.v1.str).toEqual(v1.str);
+ expect(encoding.v1.stdString).toEqual(v1.stdString);
+ expect(encoding.v1.string).toEqual(v1.string);
+ expect(encoding.v1.vector).toEqual(v1.vector);
+ expect(encoding.v1.array).toEqual(v1.array);
+ expect(encoding.v1.tuple).toEqual(v1.tuple);
+ expect(encoding.v1.struct).toEqual(v1.struct);
+ expect(encoding.v1.enum).toEqual(v1.enum);
+ expect(encoding.v1.option).toEqual(v1.option);
+ });
+
+ it('should throw an error if the version is not supported', async () => {
+ await expectToThrowFuelError(
+ () => encoding.fromVersion('0'),
+ new FuelError(
+ FuelError.CODES.UNSUPPORTED_ENCODING_VERSION,
+ 'Unsupported encoding version "0"'
+ )
+ );
+ });
+ });
+
+ describe('getCoder', () => {
+ const encodingV1 = encoding.fromVersion('1');
+
+ it('should get a coder for a given type and name [u8]', () => {
+ const params: CoderFactoryParameters = {
+ name: 'test',
+ type: { swayType: 'u8', concreteTypeId: '0x1' },
+ };
+
+ const coder = encodingV1.getCoder(params);
+
+ expect(coder).toBeDefined();
+ });
+
+ it('should get a coder for a given type and name [string]', () => {
+ const params: CoderFactoryParameters = {
+ name: 'test',
+ type: { swayType: 'str[4]', concreteTypeId: '0x1' },
+ };
+
+ const coder = encodingV1.getCoder(params);
+
+ expect(coder).toBeDefined();
+ });
+
+ it('should throw an error if the coder is not supported', async () => {
+ const params: CoderFactoryParameters = {
+ name: 'test',
+ type: { swayType: 'unknown', concreteTypeId: '0x1' },
+ };
+
+ await expectToThrowFuelError(
+ () => encodingV1.getCoder(params),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'Unsupported coder type "unknown" for element "test"'
+ )
+ );
+ });
+
+ it('should throw an error if a generic coder is used [should not happen]', async () => {
+ const params: CoderFactoryParameters = {
+ name: 'test',
+ type: { swayType: 'generic', concreteTypeId: '0x1' },
+ };
+
+ await expectToThrowFuelError(
+ () => encodingV1.getCoder(params),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'Unsupported coder type "generic" for element "test"'
+ )
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/array.test.ts b/packages/abi/test/encoding/v1/array.test.ts
new file mode 100644
index 00000000000..c28d6802ea9
--- /dev/null
+++ b/packages/abi/test/encoding/v1/array.test.ts
@@ -0,0 +1,229 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import type { AbiTypeComponent, CoderFactoryParameters } from '../../../src';
+import { encoding, MAX_BYTES } from '../../../src';
+import { U8_MAX } from '../../utils/constants';
+
+const isBrowser = typeof window !== 'undefined';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('v1/array', () => {
+ describe('fromAbi', () => {
+ it('should throw when a component is not provided', async () => {
+ const swayType = 'enum MyEnum';
+ const components: AbiTypeComponent[] = [];
+ const factory = vi.fn();
+
+ await expectToThrowFuelError(
+ () =>
+ encoding.v1.array.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ ),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'The provided array type is missing ABI components.',
+ { swayType }
+ )
+ );
+ });
+
+ it('should throw when a component is not provided', async () => {
+ const swayType = '[u8; 4]';
+ const components: AbiTypeComponent[] = [];
+ const factory = vi.fn();
+
+ await expectToThrowFuelError(
+ () =>
+ encoding.v1.array.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ ),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'The provided array type is missing a ABI component.'
+ )
+ );
+ });
+
+ it('should get the coder for a valid array type', () => {
+ const swayType = '[u8; 4]';
+ const components: AbiTypeComponent[] = [{} as AbiTypeComponent];
+ const factory = vi.fn();
+
+ const coder = encoding.v1.array.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ );
+
+ expect(factory).toHaveBeenCalledWith(components[0], factory);
+ expect(factory).toHaveBeenCalledTimes(1);
+ expect(coder).toBeDefined();
+ });
+ });
+
+ describe('encode', () => {
+ it('should encode an array [u8, length = 0]', () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 0);
+ const input: number[] = [];
+ const expected = new Uint8Array([]);
+
+ const actual = coder.encode(input);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should encode an array [u8, length = 4]', () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 4);
+ const input = [0, 13, 37, 255];
+ const expected = new Uint8Array(input);
+
+ const actual = coder.encode(input);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should encode an array of enums [enum, length = 4]', () => {
+ const coder = encoding.v1.array(
+ encoding.v1.enum({ a: encoding.v1.u8, b: encoding.v1.bool }),
+ 4
+ );
+ const input = [{ a: 0 }, { b: false }, { b: true }, { a: U8_MAX }];
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 255,
+ ]);
+
+ const actual = coder.encode(input);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should throw when value to encode is not an array [u8, length = 1]', async () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 1);
+ const input = { object: true };
+
+ await expectToThrowFuelError(
+ // @ts-expect-error - testing a non-array value
+ () => coder.encode(input),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid array value - expected array value.', {
+ value: input,
+ })
+ );
+ });
+
+ it('should throw when value to encode is not an array of the expected length [u8, length = 1]', async () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 1);
+ const input = [1, 2, 3];
+
+ await expectToThrowFuelError(
+ () => coder.encode(input),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid array value - unexpected length.', {
+ value: input,
+ expectedLength: 1,
+ })
+ );
+ });
+
+ it('should throw when value to encode is invalid [u8, length = 1]', async () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 1);
+ const input = [256];
+
+ await expectToThrowFuelError(
+ () => coder.encode(input),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u8 value - value exceeds maximum.', {
+ type: 'u8',
+ value: '256',
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode an array [u8, length = 0]', () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 0);
+ const input = new Uint8Array([]);
+ const expected: number[] = [];
+
+ const [actual, offset] = coder.decode(input);
+
+ expect(actual).toEqual(expected);
+ expect(offset).toEqual(0);
+ });
+
+ it('should decode an array [u8, length = 4]', () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 4);
+ const input = new Uint8Array([0, 13, 37, 255]);
+ const expected = [0, 13, 37, 255];
+
+ const [actual, offset] = coder.decode(input);
+
+ expect(actual).toEqual(expected);
+ expect(offset).toEqual(4);
+ });
+
+ it('should decode an array of enums [enum, length = 4]', () => {
+ const coder = encoding.v1.array(
+ encoding.v1.enum({ a: encoding.v1.u8, b: encoding.v1.bool }),
+ 4
+ );
+ const expected = [{ a: 0 }, { b: false }, { b: true }, { a: U8_MAX }];
+ const data = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 255,
+ ]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toEqual(expected);
+ expect(offset).toEqual(36);
+ });
+
+ /**
+ * TODO: this test is failing on browser, need to investigate why.
+ *
+ * RangeError: Array buffer allocation failed
+ */
+ it.skipIf(isBrowser)('should throw when decoding too many bytes [u8, length = 1]', async () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 1);
+ const data = new Uint8Array(MAX_BYTES + 1);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid array data - exceeds maximum bytes.', {
+ data,
+ length: data.length,
+ maxLength: MAX_BYTES,
+ })
+ );
+ });
+
+ it('should throw when decoding empty bytes [u8, length = 1]', async () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 1);
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u8 data - unexpected length.', {
+ data,
+ })
+ );
+ });
+
+ it('should throw when decoding too few bytes [u8, length = 1]', async () => {
+ const coder = encoding.v1.array(encoding.v1.u8, 1);
+ const data = new Uint8Array([]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u8 data - unexpected length.', {
+ data,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/b256.test.ts b/packages/abi/test/encoding/v1/b256.test.ts
new file mode 100644
index 00000000000..82ebec9c69c
--- /dev/null
+++ b/packages/abi/test/encoding/v1/b256.test.ts
@@ -0,0 +1,138 @@
+import { ErrorCode, FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('b256', () => {
+ describe('encode', () => {
+ it('should encode a b256 [zero]', () => {
+ const coder = encoding.v1.b256;
+ const expected = new Uint8Array(32).fill(0);
+ const value = '0x0000000000000000000000000000000000000000000000000000000000000000';
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it(`should encode a b256 [address]`, () => {
+ const coder = encoding.v1.b256;
+ const expected = new Uint8Array([
+ 213, 87, 156, 70, 223, 204, 127, 24, 32, 112, 19, 230, 91, 68, 228, 203, 78, 44, 34, 152,
+ 244, 172, 69, 123, 168, 248, 39, 67, 243, 30, 147, 11,
+ ]);
+ const value = '0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b';
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should throw an error encoding a b256 [too short]', async () => {
+ const coder = encoding.v1.b256;
+ const value = '0xTooShort';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256 value - malformed hex value.', {
+ value,
+ expectedLength: 32,
+ })
+ );
+ });
+
+ it('should throw an error encoding a b256 [too long]', async () => {
+ const coder = encoding.v1.b256;
+ const value = `0x${'a'.repeat(33)}`;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256 value - malformed hex value.', {
+ value,
+ expectedLength: 32,
+ })
+ );
+ });
+
+ it('should throw an error encoding a b256 [not a hex string]', async () => {
+ const coder = encoding.v1.b256;
+ const value = 'not a hex string';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256 value - malformed hex value.')
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a b256 [zero]', () => {
+ const coder = encoding.v1.b256;
+ const expected = '0x0000000000000000000000000000000000000000000000000000000000000000';
+ const data = new Uint8Array(32).fill(0);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(32);
+ });
+
+ it(`should decode a b256 [address]`, () => {
+ const coder = encoding.v1.b256;
+ const expected = '0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b';
+ const data = new Uint8Array([
+ 213, 87, 156, 70, 223, 204, 127, 24, 32, 112, 19, 230, 91, 68, 228, 203, 78, 44, 34, 152,
+ 244, 172, 69, 123, 168, 248, 39, 67, 243, 30, 147, 11,
+ ]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(32);
+ });
+
+ it('should throw an error decoding a b256 [too short]', async () => {
+ const coder = encoding.v1.b256;
+ const data = new Uint8Array(31);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data - unexpected length.', {
+ data,
+ expectedLength: 32,
+ })
+ );
+ });
+
+ it('should throw an error decoding a b256 [empty]', async () => {
+ const coder = encoding.v1.b256;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data - unexpected length.', {
+ data,
+ expectedLength: 32,
+ })
+ );
+ });
+
+ it('should throw an error decoding a b256 [with offset]', async () => {
+ const coder = encoding.v1.b256;
+ const offset = 10;
+ const data = new Uint8Array(32);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, offset),
+ new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data - unexpected length.', {
+ data: data.slice(offset),
+ expectedLength: 32,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/b512.test.ts b/packages/abi/test/encoding/v1/b512.test.ts
new file mode 100644
index 00000000000..7709f250338
--- /dev/null
+++ b/packages/abi/test/encoding/v1/b512.test.ts
@@ -0,0 +1,146 @@
+import { ErrorCode, FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('b512', () => {
+ describe('encode', () => {
+ it('should encode a b512 [zero]', () => {
+ const coder = encoding.v1.b512;
+ const expected = new Uint8Array(64).fill(0);
+ const value =
+ '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it(`should encode a b512 [address]`, () => {
+ const coder = encoding.v1.b512;
+ const expected = new Uint8Array([
+ 142, 157, 218, 111, 119, 147, 116, 90, 197, 170, 207, 158, 144, 124, 174, 48, 178, 160, 31,
+ 223, 13, 35, 183, 117, 10, 133, 198, 164, 79, 202, 12, 41, 240, 144, 111, 157, 31, 30, 146,
+ 230, 161, 251, 60, 61, 206, 243, 204, 59, 60, 219, 170, 226, 126, 71, 185, 217, 164, 198,
+ 164, 252, 228, 207, 22, 178,
+ ]);
+ const value =
+ '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2';
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should throw an error encoding a b512 [too short]', async () => {
+ const coder = encoding.v1.b512;
+ const value = '0xTooShort';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b512 value - malformed hex value.', {
+ value,
+ expectedLength: 64,
+ })
+ );
+ });
+
+ it('should throw an error encoding a b512 [too long]', async () => {
+ const coder = encoding.v1.b512;
+ const value = `0x${'a'.repeat(33)}`;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b512 value - malformed hex value.', {
+ value,
+ expectedLength: 64,
+ })
+ );
+ });
+
+ it('should throw an error encoding a b512 [not a hex string]', async () => {
+ const coder = encoding.v1.b512;
+ const value = 'not a hex string';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b512 value - malformed hex value.')
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a b512 [zero]', () => {
+ const coder = encoding.v1.b512;
+ const expected =
+ '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
+ const data = new Uint8Array(64).fill(0);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(64);
+ });
+
+ it(`should decode a b512 [address]`, () => {
+ const coder = encoding.v1.b512;
+ const expected =
+ '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2';
+ const data = new Uint8Array([
+ 142, 157, 218, 111, 119, 147, 116, 90, 197, 170, 207, 158, 144, 124, 174, 48, 178, 160, 31,
+ 223, 13, 35, 183, 117, 10, 133, 198, 164, 79, 202, 12, 41, 240, 144, 111, 157, 31, 30, 146,
+ 230, 161, 251, 60, 61, 206, 243, 204, 59, 60, 219, 170, 226, 126, 71, 185, 217, 164, 198,
+ 164, 252, 228, 207, 22, 178,
+ ]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(64);
+ });
+
+ it('should throw an error decoding a b512 [too short]', async () => {
+ const coder = encoding.v1.b512;
+ const data = new Uint8Array(31);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data - unexpected length.', {
+ data,
+ expectedLength: 64,
+ })
+ );
+ });
+
+ it('should throw an error decoding a b512 [empty]', async () => {
+ const coder = encoding.v1.b512;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data - unexpected length.', {
+ data,
+ expectedLength: 64,
+ })
+ );
+ });
+
+ it('should throw an error decoding a b512 [with offset]', async () => {
+ const coder = encoding.v1.b512;
+ const offset = 10;
+ const data = new Uint8Array(64);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, offset),
+ new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data - unexpected length.', {
+ data: data.slice(offset),
+ expectedLength: 64,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/boolean.test.ts b/packages/abi/test/encoding/v1/boolean.test.ts
new file mode 100644
index 00000000000..d64e5b63e9f
--- /dev/null
+++ b/packages/abi/test/encoding/v1/boolean.test.ts
@@ -0,0 +1,107 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('boolean', () => {
+ describe('encode', () => {
+ it('should encode a boolean [true]', () => {
+ const coder = encoding.v1.bool;
+ const value = true;
+ const expected = new Uint8Array([1]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode a boolean [false]', () => {
+ const coder = encoding.v1.bool;
+ const value = false;
+ const expected = new Uint8Array([0]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should throw when encoding an invalid boolean value [undefined]', async () => {
+ const coder = encoding.v1.bool;
+ const value = undefined;
+
+ await expectToThrowFuelError(
+ // @ts-expect-error or expects a boolean value
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid boolean value.', {
+ value: undefined,
+ })
+ );
+ });
+
+ it('should throw when encoding an invalid boolean value [string value]', async () => {
+ const coder = encoding.v1.bool;
+ const value = 'true';
+
+ await expectToThrowFuelError(
+ // @ts-expect-error or expects a boolean value
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid boolean value.', { value: 'true' })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a boolean [1 = true]', () => {
+ const coder = encoding.v1.bool;
+ const value = new Uint8Array([1]);
+ const expected = true;
+
+ const [actual, offset] = coder.decode(value);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(value.length);
+ });
+
+ it('should decode a boolean [0 = false]', () => {
+ const coder = encoding.v1.bool;
+ const value = new Uint8Array([0]);
+ const expected = false;
+
+ const [actual, offset] = coder.decode(value);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(value.length);
+ });
+
+ it('should throw when decoding an invalid boolean value [empty]', async () => {
+ const coder = encoding.v1.bool;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid bool data - not enough data.', {
+ data,
+ expectedLength: 1,
+ })
+ );
+ });
+
+ it('should throw when decoding an invalid boolean value [2]', async () => {
+ const coder = encoding.v1.bool;
+ const data = new Uint8Array([2]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid bool value - invalid boolean value.', {
+ data,
+ type: 'bool',
+ value: 2,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/byte.test.ts b/packages/abi/test/encoding/v1/byte.test.ts
new file mode 100644
index 00000000000..c7ceaadb780
--- /dev/null
+++ b/packages/abi/test/encoding/v1/byte.test.ts
@@ -0,0 +1,84 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('byte', () => {
+ describe('encode', () => {
+ it('should encode an array of numbers [1, 2, 3]', () => {
+ const coder = encoding.v1.bytes;
+ const value = [1, 2, 3];
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 3, 1, 2, 3]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode a Uint8Array [1, 2, 3]', () => {
+ const coder = encoding.v1.bytes;
+ const value = new Uint8Array([1, 2, 3]);
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 3, 1, 2, 3]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode a full word size [1, 2, 3, 4, 5, 6, 7, 8]', () => {
+ const coder = encoding.v1.bytes;
+ const value = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 8, 1, 2, 3, 4, 5, 6, 7, 8]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a byte array [1, 2, 3]', () => {
+ const coder = encoding.v1.bytes;
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 3, 1, 2, 3]);
+ const expected = new Uint8Array([1, 2, 3]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toEqual(11);
+ });
+
+ it('should throw when decoding an empty byte array', async () => {
+ const coder = encoding.v1.bytes;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid bytes data - malformed bytes.', {
+ data,
+ })
+ );
+ });
+
+ it('should throw when decoding a byte array with not enough data', async () => {
+ const coder = encoding.v1.bytes;
+ const data = new Uint8Array([
+ // Length = 0
+ 0, 0, 0, 0, 0, 0, 0, 3,
+ // Data = [1, 2]
+ 1, 2,
+ ]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid bytes data - malformed bytes.', {
+ data,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/enum.test.ts b/packages/abi/test/encoding/v1/enum.test.ts
new file mode 100644
index 00000000000..00fbbbfa796
--- /dev/null
+++ b/packages/abi/test/encoding/v1/enum.test.ts
@@ -0,0 +1,272 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+import { bn } from '@fuel-ts/math';
+
+import type { AbiTypeComponent, CoderFactoryParameters } from '../../../src';
+import { encoding } from '../../../src';
+import { U64_MAX } from '../../utils/constants';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('enum', () => {
+ describe('fromAbi', () => {
+ it('should throw when a component is not provided', async () => {
+ const factory = vi.fn();
+ const swayType = 'enum MyEnum';
+ const components: AbiTypeComponent[] | undefined = undefined;
+
+ await expectToThrowFuelError(
+ () =>
+ encoding.v1.enum.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ ),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'The provided enum type is missing ABI components.',
+ { swayType, components }
+ )
+ );
+ });
+
+ it('should get the coder for a valid enum type', () => {
+ const components: AbiTypeComponent[] = [
+ { name: 'a', type: 'bool' } as unknown as AbiTypeComponent,
+ { name: 'b', type: 'u64' } as unknown as AbiTypeComponent,
+ ];
+ const factory = vi.fn();
+ factory.mockReturnValue(encoding.v1.bool);
+ factory.mockReturnValue(encoding.v1.u64);
+
+ const coder = encoding.v1.enum.factory(
+ { type: { components } } as CoderFactoryParameters,
+ factory
+ );
+
+ expect(factory).toHaveBeenCalledWith(components[0], factory);
+ expect(factory).toHaveBeenCalledWith(components[1], factory);
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(coder).toBeDefined();
+ });
+ });
+
+ describe('encode', () => {
+ it('should encode an enum [boolean]', () => {
+ const coder = encoding.v1.enum({ a: encoding.v1.bool, b: encoding.v1.u64 });
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 1]);
+ const value = { a: true };
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an enum [u64]', () => {
+ const coder = encoding.v1.enum({ a: encoding.v1.bool, b: encoding.v1.u64 });
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 1, 255, 255, 255, 255, 255, 255, 255, 255,
+ ]);
+ const value = { b: bn(U64_MAX) };
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an enum [non-native]', () => {
+ const coder = encoding.v1.enum({
+ nonNative: encoding.v1.bool,
+ b: encoding.v1.u64,
+ native: encoding.v1.void,
+ });
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 1]);
+ const value = { nonNative: true };
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an enum [native]', () => {
+ const coder = encoding.v1.enum({
+ nonNative: encoding.v1.bool,
+ b: encoding.v1.u64,
+ native: encoding.v1.void,
+ });
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2]);
+ const value = 'native';
+
+ // @ts-expect-error native
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should throw when encoding [no enum key]', async () => {
+ const coder = encoding.v1.enum({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ native: encoding.v1.void,
+ });
+ const value = {};
+ await expectToThrowFuelError(
+ // @ts-expect-error no enum key
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid enum value - a valid case key must be provided.',
+ { value, validKeys: ['a', 'b', 'native'] }
+ )
+ );
+ });
+
+ it('should throw when encoding [non-existent enum key]', async () => {
+ const coder = encoding.v1.enum({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ native: encoding.v1.void,
+ });
+ const value = { nonExistentKey: true };
+
+ await expectToThrowFuelError(
+ // @ts-expect-error non-existent enum key
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid enum value - invalid case key "nonExistentKey".',
+ { value, validKeys: ['a', 'b', 'native'] }
+ )
+ );
+ });
+
+ it('should throw when encoding [multiple fields]', async () => {
+ const coder = encoding.v1.enum({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ native: encoding.v1.void,
+ });
+ const value = { a: true, b: bn(1) };
+
+ await expectToThrowFuelError(
+ // @ts-expect-error multiple fields
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid enum value - only one field must be provided.',
+ { value, validKeys: ['a', 'b', 'native'] }
+ )
+ );
+ });
+
+ it('should throw when encoding [invalid case value]', async () => {
+ const coder = encoding.v1.enum({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ native: encoding.v1.void,
+ });
+ const value = { b: true };
+
+ await expectToThrowFuelError(
+ // @ts-expect-error invalid case value
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u64 value - expected a BNInput.', {
+ value: true,
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode an enum [boolean]', () => {
+ const coder = encoding.v1.enum({ a: encoding.v1.bool, b: encoding.v1.u64 });
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 1]);
+ const expected = { a: true };
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toBe(9);
+ });
+
+ it('should decode an enum [u64]', () => {
+ const coder = encoding.v1.enum({ a: encoding.v1.bool, b: encoding.v1.u64 });
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 255, 255, 255, 255, 255, 255, 255, 255]);
+ const expected = { b: bn(U64_MAX) };
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toBe(16);
+ });
+
+ it('should decode an enum [non-native]', () => {
+ const coder = encoding.v1.enum({
+ nonNative: encoding.v1.bool,
+ b: encoding.v1.u64,
+ native: encoding.v1.void,
+ });
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 1]);
+ const expected = { nonNative: true };
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toBe(9);
+ });
+
+ it('should decode an enum [native]', () => {
+ const coder = encoding.v1.enum({
+ nonNative: encoding.v1.bool,
+ b: encoding.v1.u64,
+ native: encoding.v1.void,
+ });
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2]);
+ const expected = 'native';
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toBe(8);
+ });
+
+ it('should throw an error [invalid case key]', async () => {
+ const coder = encoding.v1.enum({ a: encoding.v1.bool, b: encoding.v1.u64 });
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 255, 255]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid enum data - invalid case key.', {
+ validKeys: ['a', 'b'],
+ })
+ );
+ });
+
+ it('should throw an error [empty bytes]', async () => {
+ const coder = encoding.v1.enum({ a: encoding.v1.bool, b: encoding.v1.u64 });
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid enum data - invalid case key.', {
+ validKeys: ['a', 'b'],
+ })
+ );
+ });
+
+ it('should throw an error [empty case element]', async () => {
+ const coder = encoding.v1.enum({ a: encoding.v1.bool, b: encoding.v1.u64 });
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u64 data - unexpected length.', {
+ data: new Uint8Array([]),
+ expectedLength: 8,
+ type: 'u64',
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/option.test.ts b/packages/abi/test/encoding/v1/option.test.ts
new file mode 100644
index 00000000000..0a91b6fde85
--- /dev/null
+++ b/packages/abi/test/encoding/v1/option.test.ts
@@ -0,0 +1,133 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import type { AbiTypeComponent, CoderFactoryParameters } from '../../../src';
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('option', () => {
+ describe('fromAbi', () => {
+ it('should throw when a component is not provided', async () => {
+ const swayType = 'enum std::option::Option';
+ const components: AbiTypeComponent[] | undefined = undefined;
+ const factory = vi.fn();
+
+ await expectToThrowFuelError(
+ () =>
+ encoding.v1.option.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ ),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'The provided option type is missing ABI components.',
+ { swayType, components }
+ )
+ );
+ });
+
+ it('should get the coder for a valid option type', () => {
+ const components: AbiTypeComponent[] = [
+ { name: 'None', type: '()' } as unknown as AbiTypeComponent,
+ { name: 'Some', type: 'u64' } as unknown as AbiTypeComponent,
+ ];
+ const factory = vi.fn();
+ factory.mockReturnValue(encoding.v1.void);
+ factory.mockReturnValue(encoding.v1.u64);
+
+ const coder = encoding.v1.option.factory(
+ { type: { components } } as CoderFactoryParameters,
+ factory
+ );
+
+ expect(factory).toHaveBeenCalledWith(components[0], factory);
+ expect(factory).toHaveBeenCalledWith(components[1], factory);
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(coder).toBeDefined();
+ });
+ });
+
+ describe('encode', () => {
+ it('should encode a value [Some]', () => {
+ const coder = encoding.v1.option({ None: encoding.v1.void, Some: encoding.v1.u8 });
+ const value = 100;
+
+ const actual = coder.encode(value);
+
+ const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1, 100]);
+ expect(actual).toEqual(expected);
+ });
+
+ it('should encode a value [None]', () => {
+ const coder = encoding.v1.option({ None: encoding.v1.void, Some: encoding.v1.u8 });
+ const value = undefined;
+
+ const actual = coder.encode(value);
+
+ const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
+ expect(actual).toEqual(expected);
+ });
+
+ it('should encode a value [None - optional]', () => {
+ const coder = encoding.v1.option({ None: encoding.v1.void, Some: encoding.v1.u8 });
+
+ // @ts-expect-error optional value
+ const actual = coder.encode();
+
+ const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
+ expect(actual).toEqual(expected);
+ });
+
+ it('should throw when encoding [malformed value]', async () => {
+ const coder = encoding.v1.option({ None: encoding.v1.void, Some: encoding.v1.u8 });
+ const value = 'malformed value';
+
+ await expectToThrowFuelError(
+ // @ts-expect-error invalid option value
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, `Invalid option value - malformed value.`, {
+ value: { Some: value },
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a value [Some]', () => {
+ const coder = encoding.v1.option({ None: encoding.v1.void, Some: encoding.v1.u8 });
+ const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1, 100]);
+ const expected = 100;
+
+ const [decoded, length] = coder.decode(input, 0);
+
+ expect(decoded).toEqual(expected);
+ expect(length).toEqual(9);
+ });
+
+ it('should decode a value [None]', () => {
+ const coder = encoding.v1.option({ None: encoding.v1.void, Some: encoding.v1.u8 });
+ const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
+ const expected = undefined;
+
+ const [decoded, length] = coder.decode(input, 0);
+
+ expect(decoded).toEqual(expected);
+ expect(length).toEqual(8);
+ });
+
+ it('should throw when decoding [malformed value]', async () => {
+ const coder = encoding.v1.option({ None: encoding.v1.void, Some: encoding.v1.u8 });
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, `Invalid option data - malformed bytes.`, {
+ data: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]),
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/raw-slice.test.ts b/packages/abi/test/encoding/v1/raw-slice.test.ts
new file mode 100644
index 00000000000..f8abaa69065
--- /dev/null
+++ b/packages/abi/test/encoding/v1/raw-slice.test.ts
@@ -0,0 +1,74 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('raw_slice', () => {
+ describe('encode', () => {
+ it('should encode an array of numbers [1, 2, 3]', () => {
+ const coder = encoding.v1.rawSlice;
+ const value = [1, 2, 3];
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 3, 1, 2, 3]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode a full word size [1, 2, 3, 4, 5, 6, 7, 8]', () => {
+ const coder = encoding.v1.rawSlice;
+ const value = [1, 2, 3, 4, 5, 6, 7, 8];
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 8, 1, 2, 3, 4, 5, 6, 7, 8]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a rawSlice array [1, 2, 3]', () => {
+ const coder = encoding.v1.rawSlice;
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 3, 1, 2, 3]);
+ const expected = [1, 2, 3];
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toEqual(11);
+ });
+
+ it('should throw when decoding an empty rawSlice array', async () => {
+ const coder = encoding.v1.rawSlice;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid raw slice data - malformed bytes.', {
+ data,
+ })
+ );
+ });
+
+ it('should throw when decoding a rawSlice array with not enough data', async () => {
+ const coder = encoding.v1.rawSlice;
+ const data = new Uint8Array([
+ // Length = 0
+ 0, 0, 0, 0, 0, 0, 0, 3,
+ // Data = [1, 2]
+ 1, 2,
+ ]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid raw slice data - malformed bytes.', {
+ data,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/std-string.test.ts b/packages/abi/test/encoding/v1/std-string.test.ts
new file mode 100644
index 00000000000..6e7bba6afab
--- /dev/null
+++ b/packages/abi/test/encoding/v1/std-string.test.ts
@@ -0,0 +1,93 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('std_string', () => {
+ describe('encode', () => {
+ it('should encode an empty string [""]', () => {
+ const coder = encoding.v1.stdString;
+ const value = '';
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an empty string [hello world]', () => {
+ const coder = encoding.v1.stdString;
+ const value = 'hello world';
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 11, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100,
+ ]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an empty string [H3llo W0rld]', () => {
+ const coder = encoding.v1.stdString;
+ const value = 'H3llo W0rld';
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 11, 72, 51, 108, 108, 111, 32, 87, 48, 114, 108, 100,
+ ]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an empty string [abcdefghijklmnopqrstuvwxyz1234567890]', () => {
+ const coder = encoding.v1.stdString;
+ const value = 'abcdefghijklmnopqrstuvwxyz1234567890';
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 36, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110,
+ 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 49, 50, 51, 52, 53, 54, 55, 56,
+ 57, 48,
+ ]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a rawSlice array [1, 2, 3]', () => {
+ const coder = encoding.v1.stdString;
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 4, 102, 117, 101, 108]);
+ const expected = 'fuel';
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toEqual(12);
+ });
+
+ it('should throw when decoding a string with empty bytes', async () => {
+ const coder = encoding.v1.stdString;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid std string data - malformed bytes.')
+ );
+ });
+
+ it('should throw when decoding a string with empty byte data', async () => {
+ const coder = encoding.v1.stdString;
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 255, 255, 1]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid std string data - malformed bytes.')
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/str.test.ts b/packages/abi/test/encoding/v1/str.test.ts
new file mode 100644
index 00000000000..93710b3eb22
--- /dev/null
+++ b/packages/abi/test/encoding/v1/str.test.ts
@@ -0,0 +1,93 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('str', () => {
+ describe('encode', () => {
+ it('should encode an empty string [""]', () => {
+ const coder = encoding.v1.str;
+ const value = '';
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an empty string [hello world]', () => {
+ const coder = encoding.v1.str;
+ const value = 'hello world';
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 11, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100,
+ ]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an empty string [H3llo W0rld]', () => {
+ const coder = encoding.v1.str;
+ const value = 'H3llo W0rld';
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 11, 72, 51, 108, 108, 111, 32, 87, 48, 114, 108, 100,
+ ]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode an empty string [abcdefghijklmnopqrstuvwxyz1234567890]', () => {
+ const coder = encoding.v1.str;
+ const value = 'abcdefghijklmnopqrstuvwxyz1234567890';
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 36, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110,
+ 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 49, 50, 51, 52, 53, 54, 55, 56,
+ 57, 48,
+ ]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a rawSlice array [1, 2, 3]', () => {
+ const coder = encoding.v1.str;
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 4, 102, 117, 101, 108]);
+ const expected = 'fuel';
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toEqual(12);
+ });
+
+ it('throws when decoding a string with empty bytes', async () => {
+ const coder = encoding.v1.str;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid str data - malformed bytes.')
+ );
+ });
+
+ it('throws when decoding a string with empty byte data', async () => {
+ const coder = encoding.v1.str;
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 255, 255, 1]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid str data - malformed bytes.')
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/string.test.ts b/packages/abi/test/encoding/v1/string.test.ts
new file mode 100644
index 00000000000..b626cbe83fd
--- /dev/null
+++ b/packages/abi/test/encoding/v1/string.test.ts
@@ -0,0 +1,95 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import type { CoderFactoryParameters } from '../../../src';
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('string', () => {
+ describe('fromAbi', () => {
+ it('should throw when the array type is not valid', async () => {
+ const factory = vi.fn();
+ const swayType = 'invalid[123]';
+
+ await expectToThrowFuelError(
+ () => encoding.v1.string.factory({ type: { swayType } } as CoderFactoryParameters, factory),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'Unable to find string coder for the provided type "invalid[123]".',
+ { swayType }
+ )
+ );
+ });
+
+ it('should get the coder for a valid array type', () => {
+ const factory = vi.fn();
+ const swayType = 'str[1]';
+
+ const coder = encoding.v1.string.factory(
+ { type: { swayType } } as CoderFactoryParameters,
+ factory
+ );
+
+ expect(coder).toBeDefined();
+ });
+ });
+
+ describe('encode', () => {
+ it('should encode a string', () => {
+ const coder = encoding.v1.string(4);
+ const value = 'fuel';
+ const expected = new Uint8Array([102, 117, 101, 108]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should throw when encoding a string [mismatch length]', async () => {
+ const coder = encoding.v1.string(2);
+ const value = 'fuel';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid string value - unexpected length.')
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a string', () => {
+ const coder = encoding.v1.string(4);
+ const data = new Uint8Array([102, 117, 101, 108]);
+ const expected = 'fuel';
+
+ const [actual, offset] = coder.decode(data, 0);
+
+ expect(actual).toEqual(expected);
+ expect(offset).toEqual(4);
+ });
+
+ it('should throw when decoding a string [too few bytes]', async () => {
+ const coder = encoding.v1.string(4);
+ const data = new Uint8Array([0, 0]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid string data - unexpected length.')
+ );
+ });
+
+ it('should throw when decoding a string [too few bytes w/ offset]', async () => {
+ const coder = encoding.v1.string(4);
+ const offset = 10;
+ const data = new Uint8Array([0, 0, 0, 0]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, offset),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid string data - unexpected length.')
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/struct.test.ts b/packages/abi/test/encoding/v1/struct.test.ts
new file mode 100644
index 00000000000..9d5379a2e5b
--- /dev/null
+++ b/packages/abi/test/encoding/v1/struct.test.ts
@@ -0,0 +1,141 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import type { AbiTypeComponent, CoderFactoryParameters } from '../../../src';
+import { encoding } from '../../../src';
+import { U32_MAX, U64_MAX } from '../../utils/constants';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('struct', () => {
+ describe('fromAbi', () => {
+ it('should throw when a component is not provided', async () => {
+ const swayType = 'struct MyStruct';
+ const components: AbiTypeComponent[] | undefined = undefined;
+ const factory = vi.fn();
+
+ await expectToThrowFuelError(
+ () =>
+ encoding.v1.struct.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ ),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'The provided struct type is missing ABI components.',
+ { swayType, components }
+ )
+ );
+ });
+
+ it('should get the coder for a valid struct type', () => {
+ const components: AbiTypeComponent[] = [{}] as AbiTypeComponent[];
+ const factory = vi.fn();
+
+ const coder = encoding.v1.struct.factory(
+ { type: { components } } as CoderFactoryParameters,
+ factory
+ );
+
+ expect(coder).toBeDefined();
+ });
+ });
+
+ describe('encode', () => {
+ it('should encode a struct [{ a: boolean, b: u64 }]', () => {
+ const coder = encoding.v1.struct({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ });
+ const expected = new Uint8Array([1, 0, 0, 0, 0, 255, 255, 255, 255]);
+ const value = { a: true, b: U32_MAX };
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should throw when encoding [missing element]', async () => {
+ const coder = encoding.v1.struct({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ });
+ const value = { a: true };
+
+ await expectToThrowFuelError(
+ // @ts-expect-error - expected missing field
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid struct value - malformed object.', {
+ value,
+ paths: [{ path: 'b', error: 'Field not present.' }],
+ })
+ );
+ });
+
+ it('should throw when encoding [missing and extra elements]', async () => {
+ const coder = encoding.v1.struct({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ });
+ const value = { a: true, c: 1 };
+
+ await expectToThrowFuelError(
+ // @ts-expect-error - expected missing field
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid struct value - malformed object.', {
+ value,
+ paths: [{ path: 'b', error: 'Field not present.' }],
+ })
+ );
+ });
+
+ it('should throw when encoding [invalid element]', async () => {
+ const coder = encoding.v1.struct({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ });
+ const value = { a: true, b: U64_MAX.add(1) };
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u64 value - value exceeds maximum.', {
+ type: 'u64',
+ value: U64_MAX.add(1).toString(),
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a struct [{ a: boolean, b: u64 }]', () => {
+ const coder = encoding.v1.struct({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ });
+ const expected = { a: true, b: expect.toEqualBn(U32_MAX) };
+ const data = new Uint8Array([1, 0, 0, 0, 0, 255, 255, 255, 255]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toEqual(9);
+ });
+
+ it('should throw when decoding empty bytes', async () => {
+ const coder = encoding.v1.struct({
+ a: encoding.v1.bool,
+ b: encoding.v1.u64,
+ });
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid bool data - not enough data.', {
+ data,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/tuple.test.ts b/packages/abi/test/encoding/v1/tuple.test.ts
new file mode 100644
index 00000000000..383e2c9a002
--- /dev/null
+++ b/packages/abi/test/encoding/v1/tuple.test.ts
@@ -0,0 +1,90 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import type { AbiTypeComponent, CoderFactoryParameters } from '../../../src';
+import { encoding } from '../../../src';
+import { U64_MAX } from '../../utils/constants';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('tuple', () => {
+ describe('fromAbi', () => {
+ it('should throw when a component is not provided', async () => {
+ const swayType = '(u8, bool)';
+ const components: AbiTypeComponent[] | undefined = undefined;
+ const factory = vi.fn();
+
+ await expectToThrowFuelError(
+ () =>
+ encoding.v1.tuple.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ ),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'The provided tuple type is missing ABI components.',
+ { swayType, components }
+ )
+ );
+ });
+
+ it('should get the coder for a valid tuple type', () => {
+ const components: AbiTypeComponent[] = [{}] as AbiTypeComponent[];
+ const factory = vi.fn();
+
+ const coder = encoding.v1.tuple.factory(
+ { type: { components } } as CoderFactoryParameters,
+ factory
+ );
+
+ expect(coder).toBeDefined();
+ });
+ });
+
+ describe('encode', () => {
+ it('should encode a tuple [boolean, u64]', () => {
+ const coder = encoding.v1.tuple([encoding.v1.bool, encoding.v1.u64]);
+ const expected = new Uint8Array([1, 255, 255, 255, 255, 255, 255, 255, 255]);
+ const value = [true, U64_MAX];
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should throw when encoding [too few inputs]', async () => {
+ const coder = encoding.v1.tuple([encoding.v1.bool, encoding.v1.u64]);
+ const value = [true];
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid tuple value - unexpected length.')
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a tuple [boolean, u64]', () => {
+ const coder = encoding.v1.tuple([encoding.v1.bool, encoding.v1.u64]);
+ const expected = [true, U64_MAX];
+ const data = new Uint8Array([1, 255, 255, 255, 255, 255, 255, 255, 255]);
+
+ const [actual, actualOffset] = coder.decode(data, 0);
+
+ expect(actual).toStrictEqual(expected);
+ expect(actualOffset).toEqual(9);
+ });
+
+ it('throws when decoding bytes [empty]', async () => {
+ const coder = encoding.v1.tuple([encoding.v1.bool, encoding.v1.u64]);
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid tuple data - malformed data.')
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/u16.test.ts b/packages/abi/test/encoding/v1/u16.test.ts
new file mode 100644
index 00000000000..3a4ef257547
--- /dev/null
+++ b/packages/abi/test/encoding/v1/u16.test.ts
@@ -0,0 +1,114 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('u16', () => {
+ describe('encode', () => {
+ it('should encode a u16 [min = 0]', () => {
+ const coder = encoding.v1.u16;
+ const expected = new Uint8Array([0, 0]);
+ const value = 0;
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it(`should encode a u16 [max = 65535]`, () => {
+ const coder = encoding.v1.u16;
+ const expected = new Uint8Array([255, 255]);
+ const value = 65535;
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should fail to encode value [min - 1 = -1]', async () => {
+ const coder = encoding.v1.u16;
+ const value = -1;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid u16 value - value is less than zero.',
+ {
+ value: '-1',
+ type: 'u16',
+ }
+ )
+ );
+ });
+
+ it(`should fail to encode value [max + 1 = 65536]`, async () => {
+ const coder = encoding.v1.u16;
+ const value = 65536;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u16 value - value exceeds maximum.', {
+ value: '65536',
+ type: 'u16',
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a u16 [min]', () => {
+ const coder = encoding.v1.u16;
+ const expected = 0;
+ const data = new Uint8Array([0, 0]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(2);
+ });
+
+ it(`should decode a u16 [max = 65535]`, () => {
+ const coder = encoding.v1.u16;
+ const expected = 65535;
+ const data = new Uint8Array([255, 255]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(2);
+ });
+
+ it('should throw when decoding invalid u16 data [empty]', async () => {
+ const coder = encoding.v1.u16;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u16 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u16',
+ expectedLength: 2,
+ })
+ );
+ });
+
+ it('should throw when decoding invalid u16 data [empty with offset]', async () => {
+ const coder = encoding.v1.u16;
+ const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 8),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u16 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u16',
+ expectedLength: 2,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/u256.test.ts b/packages/abi/test/encoding/v1/u256.test.ts
new file mode 100644
index 00000000000..dea0d0101e3
--- /dev/null
+++ b/packages/abi/test/encoding/v1/u256.test.ts
@@ -0,0 +1,210 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+import { bn } from '@fuel-ts/math';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('u256', () => {
+ describe('encode', () => {
+ it('should encode a u256 [min = 0]', () => {
+ const coder = encoding.v1.u256;
+ const expected = new Uint8Array(32).fill(0);
+ const value = 0;
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it(`should encode a u256 [max = 115792089237316195423570985008687907853269984665640564039457584007913129639935]`, () => {
+ const coder = encoding.v1.u256;
+ const expected = new Uint8Array(32).fill(255);
+ const value =
+ '115792089237316195423570985008687907853269984665640564039457584007913129639935';
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode a u64 [max safe integer]', () => {
+ const coder = encoding.v1.u256;
+ const value: number = Number.MAX_SAFE_INTEGER;
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31, 255, 255,
+ 255, 255, 255, 255,
+ ]);
+
+ const data = coder.encode(value);
+
+ expect(data).toEqual(expected);
+ });
+
+ it('should throw an error when encoding [number more than max safe integer]', async () => {
+ const coder = encoding.v1.u256;
+ const value: number = Number.MAX_SAFE_INTEGER + 1;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid u256 type - number value is too large. Number can only safely handle up to 53 bits.'
+ )
+ );
+ });
+
+ it('should encode a u64 [very big number - as string]', () => {
+ const coder = encoding.v1.u256;
+ const value: string = '76472027892439376';
+ const expected = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 15, 174, 231,
+ 121, 200, 89, 80,
+ ]);
+
+ const data = coder.encode(value);
+
+ expect(data).toEqual(expected);
+ });
+
+ it('should throw an error when encoding [number more than max safe integer]', async () => {
+ const coder = encoding.v1.u256;
+ const value: number = 76472027892439376;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid u256 type - number value is too large. Number can only safely handle up to 53 bits.'
+ )
+ );
+ });
+
+ it('should fail to encode value [boolean]', async () => {
+ const coder = encoding.v1.u256;
+ const value = true;
+
+ await expectToThrowFuelError(
+ // @ts-expect-error: a boolean value boolean
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u256 value - expected a BNInput.', {
+ value,
+ })
+ );
+ });
+
+ it('should fail to encode value [not a BNInput]', async () => {
+ const coder = encoding.v1.u256;
+ const value = 'not a BNInput';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u256 value - expected a BNInput.', {
+ value,
+ })
+ );
+ });
+
+ it('should fail to encode value [min - 1 = -1]', async () => {
+ const coder = encoding.v1.u256;
+ const value = -1;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid u256 value - value is less than zero.',
+ {
+ value: '-1',
+ type: 'u256',
+ }
+ )
+ );
+ });
+
+ it(`should fail to encode value [max + 1 = 115792089237316195423570985008687907853269984665640564039457584007913129639936]`, async () => {
+ const coder = encoding.v1.u256;
+ const value =
+ '115792089237316195423570985008687907853269984665640564039457584007913129639936';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u256 value - value exceeds maximum.', {
+ value: '115792089237316195423570985008687907853269984665640564039457584007913129639936',
+ type: 'u256',
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a u256 [min]', () => {
+ const coder = encoding.v1.u256;
+ const expected = expect.toEqualBn(0);
+ const data = new Uint8Array(32).fill(0);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(32);
+ });
+
+ it(`should decode a u256 [max = 115792089237316195423570985008687907853269984665640564039457584007913129639935]`, () => {
+ const coder = encoding.v1.u256;
+ const expected = expect.toEqualBn(
+ '115792089237316195423570985008687907853269984665640564039457584007913129639935'
+ );
+ const data = new Uint8Array(32).fill(255);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(32);
+ });
+
+ it('should decode a u64 [very big number]', () => {
+ const coder = encoding.v1.u256;
+ const data = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 15, 174, 231,
+ 121, 200, 89, 80,
+ ]);
+ const expectedValue = bn('76472027892439376');
+
+ const [actualValue, actualLength] = coder.decode(data, 0);
+
+ expect(actualValue).toEqualBn(expectedValue);
+ expect(actualLength).toEqual(32);
+ });
+
+ it('should throw when decoding invalid u256 data [empty]', async () => {
+ const coder = encoding.v1.u256;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u256 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u256',
+ expectedLength: 32,
+ })
+ );
+ });
+
+ it('should throw when decoding invalid u256 data [empty with offset]', async () => {
+ const coder = encoding.v1.u256;
+ const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 8),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u256 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u256',
+ expectedLength: 32,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/u32.test.ts b/packages/abi/test/encoding/v1/u32.test.ts
new file mode 100644
index 00000000000..d81395c529f
--- /dev/null
+++ b/packages/abi/test/encoding/v1/u32.test.ts
@@ -0,0 +1,114 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('u32', () => {
+ describe('encode', () => {
+ it('should encode a u32 [min = 0]', () => {
+ const coder = encoding.v1.u32;
+ const expected = new Uint8Array([0, 0, 0, 0]);
+ const value = 0;
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it(`should encode a u32 [max = 4294967295]`, () => {
+ const coder = encoding.v1.u32;
+ const expected = new Uint8Array([255, 255, 255, 255]);
+ const value = 4294967295;
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should fail to encode value [min - 1 = -1]', async () => {
+ const coder = encoding.v1.u32;
+ const value = -1;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid u32 value - value is less than zero.',
+ {
+ value: '-1',
+ type: 'u32',
+ }
+ )
+ );
+ });
+
+ it(`should fail to encode value [max + 1 = 4294967296]`, async () => {
+ const coder = encoding.v1.u32;
+ const value = 4294967296;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u32 value - value exceeds maximum.', {
+ value: '4294967296',
+ type: 'u32',
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a u32 [min]', () => {
+ const coder = encoding.v1.u32;
+ const expected = 0;
+ const data = new Uint8Array([0, 0, 0, 0]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(4);
+ });
+
+ it(`should decode a u32 [max = 4294967295]`, () => {
+ const coder = encoding.v1.u32;
+ const expected = 4294967295;
+ const data = new Uint8Array([255, 255, 255, 255]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(4);
+ });
+
+ it('should throw when decoding invalid u32 data [empty]', async () => {
+ const coder = encoding.v1.u32;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u32 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u32',
+ expectedLength: 4,
+ })
+ );
+ });
+
+ it('should throw when decoding invalid u32 data [empty with offset]', async () => {
+ const coder = encoding.v1.u32;
+ const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 8),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u32 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u32',
+ expectedLength: 4,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/u64.test.ts b/packages/abi/test/encoding/v1/u64.test.ts
new file mode 100644
index 00000000000..80cb5b407f0
--- /dev/null
+++ b/packages/abi/test/encoding/v1/u64.test.ts
@@ -0,0 +1,197 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+import { bn } from '@fuel-ts/math';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('u64', () => {
+ describe('encode', () => {
+ it('should encode a u64 [min = 0]', () => {
+ const coder = encoding.v1.u64;
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
+ const value = 0;
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it(`should encode a u64 [max = 18446744073709551615]`, () => {
+ const coder = encoding.v1.u64;
+ const expected = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]);
+ const value = '18446744073709551615';
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode a u64 [max safe integer]', () => {
+ const coder = encoding.v1.u64;
+ const value: number = Number.MAX_SAFE_INTEGER;
+ const expected = new Uint8Array([0, 31, 255, 255, 255, 255, 255, 255]);
+
+ const data = coder.encode(value);
+
+ expect(data).toEqual(expected);
+ });
+
+ it('should throw an error when encoding [number more than max safe integer]', async () => {
+ const coder = encoding.v1.u64;
+ const value: number = Number.MAX_SAFE_INTEGER + 1;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid u64 type - number value is too large. Number can only safely handle up to 53 bits.'
+ )
+ );
+ });
+
+ it('should encode a u64 [very big number - as string]', () => {
+ const coder = encoding.v1.u64;
+ const value: string = '76472027892439376';
+ const expected = new Uint8Array([1, 15, 174, 231, 121, 200, 89, 80]);
+
+ const data = coder.encode(value);
+
+ expect(data).toEqual(expected);
+ });
+
+ it('should throw an error when encoding [number more than max safe integer]', async () => {
+ const coder = encoding.v1.u64;
+ const value: number = 76472027892439376;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid u64 type - number value is too large. Number can only safely handle up to 53 bits.'
+ )
+ );
+ });
+
+ it('should fail to encode value [boolean]', async () => {
+ const coder = encoding.v1.u64;
+ const value = true;
+
+ await expectToThrowFuelError(
+ // @ts-expect-error: a boolean value boolean
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u64 value - expected a BNInput.', {
+ value,
+ })
+ );
+ });
+
+ it('should fail to encode value [not a BNInput]', async () => {
+ const coder = encoding.v1.u64;
+ const value = 'not a BNInput';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u64 value - expected a BNInput.', {
+ value,
+ })
+ );
+ });
+
+ it('should fail to encode value [min - 1 = -1]', async () => {
+ const coder = encoding.v1.u64;
+ const value = -1;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid u64 value - value is less than zero.',
+ {
+ value: '-1',
+ type: 'u64',
+ }
+ )
+ );
+ });
+
+ it(`should fail to encode value [max + 1 = 18446744073709551616]`, async () => {
+ const coder = encoding.v1.u64;
+ const value = '18446744073709551616';
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u64 value - value exceeds maximum.', {
+ value: '18446744073709551616',
+ type: 'u64',
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a u64 [min]', () => {
+ const coder = encoding.v1.u64;
+ const expected = expect.toEqualBn(0);
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(8);
+ });
+
+ it(`should decode a u64 [max = 18446744073709551615]`, () => {
+ const coder = encoding.v1.u64;
+ const expected = expect.toEqualBn('18446744073709551615');
+ const data = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(8);
+ });
+
+ it('should decode a u64 [very big number]', () => {
+ const coder = encoding.v1.u64;
+ const data = new Uint8Array([1, 15, 174, 231, 121, 200, 89, 80]);
+ const expectedValue = bn('76472027892439376');
+
+ const [actualValue, actualLength] = coder.decode(data, 0);
+
+ expect(actualValue).toEqualBn(expectedValue);
+ expect(actualLength).toEqual(8);
+ });
+
+ it('should throw when decoding invalid u64 data [empty]', async () => {
+ const coder = encoding.v1.u64;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u64 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u64',
+ expectedLength: 8,
+ })
+ );
+ });
+
+ it('should throw when decoding invalid u64 data [empty with offset]', async () => {
+ const coder = encoding.v1.u64;
+ const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 8),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u64 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u64',
+ expectedLength: 8,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/u8.test.ts b/packages/abi/test/encoding/v1/u8.test.ts
new file mode 100644
index 00000000000..1a54a1d8f2f
--- /dev/null
+++ b/packages/abi/test/encoding/v1/u8.test.ts
@@ -0,0 +1,110 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('u8', () => {
+ describe('encode', () => {
+ it('should encode a u8 [min = 0]', () => {
+ const coder = encoding.v1.u8;
+ const expected = new Uint8Array([0]);
+ const value = 0;
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode a u8 [max = 255]', () => {
+ const coder = encoding.v1.u8;
+ const expected = new Uint8Array([255]);
+ const value = 255;
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should fail to encode value [-1]', async () => {
+ const coder = encoding.v1.u8;
+ const value = -1;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u8 value - value is less than zero.', {
+ value: '-1',
+ type: 'u8',
+ })
+ );
+ });
+
+ it('should fail to encode value [256]', async () => {
+ const coder = encoding.v1.u8;
+ const value = 256;
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u8 value - value exceeds maximum.', {
+ value: '256',
+ type: 'u8',
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a u8 [min]', () => {
+ const coder = encoding.v1.u8;
+ const expected = 0;
+ const data = new Uint8Array([0]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(1);
+ });
+
+ it('should decode a u8 [max]', () => {
+ const coder = encoding.v1.u8;
+ const expected = 255;
+ const data = new Uint8Array([255]);
+
+ const [actual, offset] = coder.decode(data);
+
+ expect(actual).toStrictEqual(expected);
+ expect(offset).toStrictEqual(1);
+ });
+
+ it('should throw when decoding invalid u8 data [empty]', async () => {
+ const coder = encoding.v1.u8;
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u8 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u8',
+ expectedLength: 1,
+ })
+ );
+ });
+
+ it('should throw when decoding invalid u8 data [empty with offset]', async () => {
+ const coder = encoding.v1.u8;
+ const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 8),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u8 data - unexpected length.', {
+ data: new Uint8Array([]),
+ type: 'u8',
+ expectedLength: 1,
+ })
+ );
+ });
+ });
+});
diff --git a/packages/abi/test/encoding/v1/vector.test.ts b/packages/abi/test/encoding/v1/vector.test.ts
new file mode 100644
index 00000000000..2493867eb52
--- /dev/null
+++ b/packages/abi/test/encoding/v1/vector.test.ts
@@ -0,0 +1,212 @@
+import { FuelError } from '@fuel-ts/errors';
+import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
+
+import type { AbiTypeComponent, CoderFactoryParameters } from '../../../src';
+import { encoding, MAX_BYTES } from '../../../src';
+
+const isBrowser = typeof window !== 'undefined';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('vector', () => {
+ describe('fromAbi', () => {
+ it('should throw when a component is not provided', async () => {
+ const swayType = 'struct std::vec::Vec';
+ const components: AbiTypeComponent[] | undefined = undefined;
+ const factory = vi.fn();
+
+ await expectToThrowFuelError(
+ () =>
+ encoding.v1.vector.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ ),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'The provided vector type is missing ABI components.',
+ { swayType, components }
+ )
+ );
+ });
+
+ // NEDIM TODO: decide on if the vector should stay as-is or be modified in ResolvableType
+ it.skip('should throw when a "buf" component is not provided', async () => {
+ const swayType = 'struct std::vec::Vec';
+ const components: AbiTypeComponent[] = [];
+ const factory = vi.fn();
+
+ await expectToThrowFuelError(
+ () =>
+ encoding.v1.vector.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ ),
+ new FuelError(
+ FuelError.CODES.CODER_NOT_FOUND,
+ 'The provided vector type is missing ABI component "buf".',
+ { swayType, components }
+ )
+ );
+ });
+
+ it('should get the coder for a valid vector type', () => {
+ const swayType = 'struct std::vec::Vec';
+ const components: AbiTypeComponent[] = [
+ {
+ name: 'buf',
+ type: {
+ swayType: 'u64',
+ concreteTypeId: 'some_hash',
+ },
+ },
+ ];
+ const factory = vi.fn();
+
+ const coder = encoding.v1.vector.factory(
+ { type: { swayType, components } } as CoderFactoryParameters,
+ factory
+ );
+
+ expect(coder).toBeDefined();
+ });
+ });
+
+ describe('encode', () => {
+ it('should encode a vector of booleans [true, false, true, false, true, true]', () => {
+ const coder = encoding.v1.vector(encoding.v1.bool);
+ const value = [true, false, true, false, true, true];
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 6, 1, 0, 1, 0, 1, 1]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should encode a vector of numbers [8, 6, 7]', () => {
+ const coder = encoding.v1.vector(encoding.v1.u8);
+ const value = Uint8Array.from([8, 6, 7]);
+ const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 3, 8, 6, 7]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toStrictEqual(expected);
+ });
+
+ it('should throw when encoding non array input', async () => {
+ const coder = encoding.v1.vector(encoding.v1.u8);
+ const value = 'Nope';
+
+ await expectToThrowFuelError(
+ // @ts-expect-error - testing invalid input
+ () => coder.encode(value),
+ new FuelError(
+ FuelError.CODES.ENCODE_ERROR,
+ 'Invalid vector value - expected array value, or a Uint8Array.',
+ { value }
+ )
+ );
+ });
+
+ it('should throw when encoding a vector with a value that cannot be encoded', async () => {
+ const coder = encoding.v1.vector(encoding.v1.u8);
+ const value = [256];
+
+ await expectToThrowFuelError(
+ () => coder.encode(value),
+ new FuelError(FuelError.CODES.ENCODE_ERROR, 'Invalid u8 value - value exceeds maximum.', {
+ type: 'u8',
+ value: '256',
+ })
+ );
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a vector of booleans [true, false, true, false, true, true]', () => {
+ const coder = encoding.v1.vector(encoding.v1.bool);
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 6, 1, 0, 1, 0, 1, 1]);
+ const expected = [true, false, true, false, true, true];
+
+ const [actual, newOffset] = coder.decode(data, 0);
+
+ expect(actual).toStrictEqual(expected);
+ expect(newOffset).toEqual(14);
+ });
+
+ it('should decode a vector of numbers [8, 6, 7]', () => {
+ const coder = encoding.v1.vector(encoding.v1.u8);
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 3, 8, 6, 7]);
+ const expected = [8, 6, 7];
+
+ const [actual, newOffset] = coder.decode(data, 0);
+
+ expect(actual).toStrictEqual(expected);
+ expect(newOffset).toEqual(11);
+ });
+
+ it('should decode a vector of numbers [empty]', () => {
+ const coder = encoding.v1.vector(encoding.v1.u8);
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
+ const expected: number[] = [];
+
+ const [actual, newOffset] = coder.decode(data, 0);
+
+ expect(actual).toStrictEqual(expected);
+ expect(newOffset).toEqual(8);
+ });
+
+ it('should throw when decoding empty vector', async () => {
+ const coder = encoding.v1.vector(encoding.v1.u8);
+ const data = new Uint8Array(0);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid vector data - malformed bytes.', {
+ data,
+ })
+ );
+ });
+
+ it('should throw when decoding vector with element data missing', async () => {
+ const coder = encoding.v1.vector(encoding.v1.u8);
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2, 1]);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(FuelError.CODES.DECODE_ERROR, 'Invalid u8 data - unexpected length.', {
+ data: new Uint8Array([]),
+ expectedLength: 1,
+ type: 'u8',
+ })
+ );
+ });
+
+ /**
+ * TODO: this test is failing on browser, need to investigate why.
+ *
+ * RangeError: Array buffer allocation failed
+ */
+ it.skipIf(isBrowser)(
+ 'should throw when decoding an array over the max vec size [VM constraints]',
+ async () => {
+ const coder = encoding.v1.vector(encoding.v1.u8);
+ const data = new Uint8Array(MAX_BYTES + 1);
+
+ await expectToThrowFuelError(
+ () => coder.decode(data, 0),
+ new FuelError(
+ FuelError.CODES.DECODE_ERROR,
+ 'Invalid vector data - exceeds maximum bytes.',
+ {
+ data,
+ length: data.length,
+ maxLength: MAX_BYTES,
+ }
+ )
+ );
+ }
+ );
+ });
+});
diff --git a/packages/abi/test/encoding/v1/void.test.ts b/packages/abi/test/encoding/v1/void.test.ts
new file mode 100644
index 00000000000..8f82ef1a1ce
--- /dev/null
+++ b/packages/abi/test/encoding/v1/void.test.ts
@@ -0,0 +1,32 @@
+import { encoding } from '../../../src';
+
+/**
+ * @group node
+ * @group browser
+ */
+describe('void', () => {
+ describe('encode', () => {
+ it('should encode a void', () => {
+ const coder = encoding.v1.void;
+ const value = undefined;
+ const expected = new Uint8Array([]);
+
+ const actual = coder.encode(value);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('decode', () => {
+ it('should decode a void', () => {
+ const coder = encoding.v1.void;
+ const data = new Uint8Array([]);
+ const expected = undefined;
+
+ const [actual, offset] = coder.decode(data, 0);
+
+ expect(actual).toEqual(expected);
+ expect(offset).toEqual(0);
+ });
+ });
+});
diff --git a/packages/abi/test/fixtures/v1.ts b/packages/abi/test/fixtures/v1.ts
new file mode 100644
index 00000000000..5b1c6e0da21
--- /dev/null
+++ b/packages/abi/test/fixtures/v1.ts
@@ -0,0 +1,2836 @@
+import type { AbiSpecificationV1 } from '../../src';
+
+export const v1: AbiSpecificationV1 = {
+ programType: 'contract',
+ specVersion: '1',
+ encodingVersion: '1',
+ concreteTypes: [
+ {
+ type: '()',
+ concreteTypeId: '2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d',
+ },
+ {
+ type: '(b256, bool)',
+ concreteTypeId: 'd5f6ab61fc224aae1bf15a89ab88840ed54e312a76a9735d1f60d4d0d1fae640',
+ metadataTypeId: 0,
+ },
+ {
+ type: '(bool, u64)',
+ concreteTypeId: 'c998ca9a5f221fe7b5c66ae70c8a9562b86d964408b00d17f883c906bc1fe4be',
+ metadataTypeId: 8,
+ },
+ {
+ type: '(str[5], bool)',
+ concreteTypeId: 'a1e229302ed2f092752a6bc4fbe66bb9305e0802b1b01ecc5e1d59356702e956',
+ metadataTypeId: 1,
+ },
+ {
+ type: '(str[5], str[5])',
+ concreteTypeId: '30022fd7ad3fda4035d30e4d86b705d4870924d4b4fe054624d2561fa12bb33e',
+ metadataTypeId: 2,
+ },
+ {
+ type: '(struct data_structures::StructDoubleGeneric<[b256; 3],u8>, [struct data_structures::StructDoubleGeneric; 4], (str[5], bool), struct data_structures::StructSimple)',
+ concreteTypeId: '343f07ddcd75b9385bc193e0419f2e89c75fad67cbf4ad1b36a01a136620817e',
+ metadataTypeId: 13,
+ },
+ {
+ type: '(struct data_structures::StructSimple, struct std::vec::Vec)',
+ concreteTypeId: '5ebb7c8cdd38d1f676f9c7089a2da12b27114ee3771c2047f3295d4d30f8fd2c',
+ metadataTypeId: 3,
+ },
+ {
+ type: '(struct std::asset_id::AssetId, struct std::asset_id::AssetId, bool)',
+ concreteTypeId: 'a95e1fcceb1451b8a76471f593f66c4a52ca04bde3c227c746ad7aaf988de5c6',
+ metadataTypeId: 10,
+ },
+ {
+ type: '(struct std::vec::Vec, b256)',
+ concreteTypeId: '52e2726988c7da304606fbe4ed696efac04beb29e9a22e15778f8a0539c9cb94',
+ metadataTypeId: 5,
+ },
+ {
+ type: '(struct std::vec::Vec, struct std::vec::Vec)',
+ concreteTypeId: '87a4626758542d7b6a03099839e440a052a4d5a00e3abfdf22bcc564ca19a4fd',
+ metadataTypeId: 6,
+ },
+ {
+ type: '(u32, struct std::vec::Vec, struct std::vec::Vec