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:
script;
configurable {
AMOUNT: u32 = 10,
}
fn main(inputted_amount: u32) -> u32 {
inputted_amount + AMOUNT
}
When you build this script, using:
forc build
It will produce the following ABI:
{
"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": 856,
},
],
}
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:
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();
Now, we can encode the script data to use in the transaction:
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();
Finally, we can decode the result:
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
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.