Skip to content

Encode and Decode ​

In order to interact with the FuelVM, types must be encoded and decoded as per the argument encoding specification. The SDK provides the AbiCoder class to encode and decode data.

It has three static methods:

  • encode
  • decode
  • getCoder

The methods encode and decode describe the aforementioned process, while getCoder returns an instance of the internal coder required to serialize the passed type. This coder is then used internally by the encode and decode methods.

All methods expect you to pass the ABI and ABI Argument as function parameters to deduce the specific type coders that will be required to parse the data.

Imagine we are working with the following script that returns the sum of two u32 integers:

rust
script;

configurable {
    AMOUNT: u32 = 10,
}

fn main(inputted_amount: u32) -> u32 {
    inputted_amount + AMOUNT
}
See code in context

When you build this script, using:

sh
forc build

It will produce the following ABI:

json
{
  "encoding": "1",
  "types": [
    {
      "typeId": 0,
      "type": "u32",
      "components": null,
      "typeParameters": null,
    },
  ],
  "functions": [
    {
      "inputs": [
        {
          "name": "inputted_amount",
          "type": 0,
          "typeArguments": null,
        },
      ],
      "name": "main",
      "output": {
        "name": "",
        "type": 0,
        "typeArguments": null,
      },
      "attributes": null,
    },
  ],
  "loggedTypes": [],
  "messagesTypes": [],
  "configurables": [
    {
      "name": "AMOUNT",
      "configurableType": {
        "name": "",
        "type": 0,
        "typeArguments": null,
      },
      "offset": 824,
    },
  ],
}
See code in context

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:

ts
import { Script } from 'fuels';
import type { JsonAbi } from 'fuels';
import { factory } from './sway-programs-api';

// 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 = factory.abi;
const bytecode: string = factory.bin;

// Create the invocation scope for the script call, passing the initial
// value for the configurable constant
const script = new Script(bytecode, abi, wallet);
const initialValue = 10;
script.setConfigurableConstants({ AMOUNT: initialValue });
const invocationScope = script.functions.main(0);

// Create the transaction request, this can be picked off the invocation
// scope so the script bytecode is preset on the transaction
const request = await invocationScope.getTransactionRequest();
See code in context

Now, we can encode the script data to use in the transaction:

ts
import { AbiCoder } from 'fuels';
import type { JsonAbiArgument } from 'fuels';

// Now we can encode the argument we want to pass to the function. The argument is required
// as a function parameter for all `AbiCoder` functions and we can extract it from the ABI itself
const argument: JsonAbiArgument = abi.functions
  .find((f) => f.name === 'main')
  ?.inputs.find((i) => i.name === 'inputted_amount') as JsonAbiArgument;

// Using the `AbiCoder`'s `encode` method,  we can now create the encoding required for
// a u32 which takes 4 bytes up of property space
const argumentToAdd = 10;
const encodedArguments = AbiCoder.encode(abi, argument, [argumentToAdd]);
// Therefore the value of 10 will be encoded to:
// Uint8Array([0, 0, 0, 10]

// The encoded value can now be set on the transaction via the script data property
request.scriptData = encodedArguments;

// Now we can build out the rest of the transaction and then fund it
const txCost = await wallet.provider.getTransactionCost(request);
request.maxFee = txCost.maxFee;
request.gasLimit = txCost.gasUsed;
await wallet.fund(request, txCost);

// Finally, submit the built transaction
const response = await wallet.sendTransaction(request);
await response.waitForResult();
See code in context

Finally, we can decode the result:

ts
import { AbiCoder, ReceiptType, arrayify, buildFunctionResult } from 'fuels';
import type { TransactionResultReturnDataReceipt } from 'fuels';

// Get result of the transaction, including the contract call result. For this we'll need
// the previously created invocation scope, the transaction response and the script
const invocationResult = await buildFunctionResult({
  funcScope: invocationScope,
  isMultiCall: false,
  program: script,
  transactionResponse: response,
});

// The decoded value can be destructured from the `FunctionInvocationResult`
const { value } = invocationResult;

// Or we can decode the returned bytes ourselves, by retrieving the return data
// receipt that contains the returned bytes. We can get this by filtering on
// the returned receipt types
const returnDataReceipt = invocationResult.transactionResult.receipts.find(
  (r) => r.type === ReceiptType.ReturnData
) as TransactionResultReturnDataReceipt;

// The data is in hex format so it makes sense to use arrayify so that the data
// is more human readable
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 `AbiCoder`
const [decodedReturnData] = AbiCoder.decode(abi, argument, returnData, 0);
// 20
See code in context

A similar approach can be taken with Predicates; however, you must set the encoded values to the predicateData property.

Contracts require more care. Although you can utilize the scriptData property, the arguments must be encoded as part of the contract call script. Therefore, it is recommended to use a FunctionInvocationScope when working with contracts which will be instantiated for you when submitting a contract function, and therefore handles all the encoding.