Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Fuel Rust SDK (fuels) — User Guide

Generated June 2026 from FuelLabs/fuels-rs at b404424

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

The Fuel Rust SDK is a collection of Rust crates, published on crates.io as fuels, for building applications and tests against the Fuel blockchain and the FuelVM. With it you can launch Fuel nodes, generate type‑safe Rust bindings from a Sway contract's ABI, deploy and call contracts, run scripts, work with predicates, craft and sign custom transactions, and manage wallets and assets — all from ordinary, compile‑time‑checked Rust.

This guide is task‑oriented: it shows you how to use the SDK. It is built directly from the SDK's source code and its own documentation at version 0.77.0. For deployment architecture, crate internals, and security hardening, see the companion Admin Guide.

Note

The s in fuels is intentional. The SDK family (fuels-rs, fuels-ts, …) is modelled on The Ethers Project, so the crate is fuels, not fuel.


1. Quick Start

The fastest way to see the SDK work is to launch a short‑lived in‑process Fuel node, deploy a compiled contract, and call one of its methods.

Add the dependency to your Cargo.toml:

[dev-dependencies]
fuels = "0.77.0"
tokio = { version = "1", features = ["full"] }

Then, in a test:

use fuels::prelude::*;

#[tokio::test]
async fn quick_start() -> Result<()> {
    // 1. Launch a local node and get a funded test wallet linked to it.
    let wallet = launch_provider_and_get_wallet().await?;

    // 2. Generate type-safe bindings from a contract's JSON ABI.
    abigen!(Contract(
        name = "MyContract",
        abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
    ));

    // 3. Deploy the compiled contract binary.
    let contract_id = Contract::load_from(
        "e2e/sway/contracts/contract_test/out/release/contract_test.bin",
        LoadConfiguration::default(),
    )?
    .deploy(&wallet, TxPolicies::default())
    .await?
    .contract_id;

    // 4. Bind an instance and call a method.
    let contract = MyContract::new(contract_id, wallet);
    let response = contract.methods().initialize_counter(42).call().await?;

    assert_eq!(42, response.value);
    Ok(())
}
running 1 test
test quick_start ... ok

Everything you need is re‑exported from fuels::prelude::*. The rest of this guide unpacks each step.

Tip

launch_provider_and_get_wallet(), abigen!, Contract::load_from, TxPolicies, and Result all come from the prelude. Importing fuels::prelude::* is the recommended way to bootstrap any project.


2. Installation & Prerequisites

The SDK is a library, not an application — you install it as a crate and write Rust against it. To compile and test Sway contracts you also need the Fuel toolchain binaries.

2.1 Prerequisites

Tool Purpose Install
Rust (latest stable) Build your project and the SDK Fuel installation guide
forc Sway compiler & package manager (Sway's cargo) via fuelup
fuel-core A Fuel full‑node implementation via fuelup

Important

The SDK shells out to a fuel-core binary on your PATH when launching test nodes (unless you enable the fuel-core-lib feature — see §2.3). Make sure fuel-core is installed and discoverable, or test setup will fail with no 'fuel-core' in PATH.

The current SDK targets fuel-core 0.48.x and fuel-vm 0.66.x. The exact supported fuel-core version is pinned in the SDK (SUPPORTED_FUEL_CORE_VERSION); using a mismatched node may cause schema or consensus‑parameter errors.

2.2 Adding the crate

For an application that talks to a live node, add fuels to [dependencies]:

[dependencies]
fuels = "0.77.0"

For a project that only tests contracts, fuels is typically a [dev-dependencies] entry, since the test helpers and node launching are test‑time concerns.

In your Rust code:

use fuels::prelude::*;

2.3 Feature flags

fuels exposes a number of Cargo features. The most commonly used:

Feature Default? What it enables
std ✅ Full standard‑library support (node client, test helpers). Disable for no_std/WASM.
coin-cache ✅ In‑memory cache that prevents re‑spending coins submitted in recent transactions.
test-helpers — The fuels-test-helpers surface (node launching, wallet setup) outside of dev builds.
fuel-core-lib — Run fuel-core as a library — no separate binary needed. Pulls in heavy deps.
rocksdb — Persistent on‑disk node storage (combine with fuel-core-lib).
accounts-keystore — Encrypted on‑disk key storage (Keystore).
accounts-signer-aws-kms — Sign transactions with AWS KMS.
accounts-signer-google-kms — Sign transactions with Google Cloud KMS.

Enable features the usual way:

fuels = { version = "0.77.0", features = ["fuel-core-lib", "rocksdb"] }

Tip

Use fuel-core-lib when you cannot guarantee a fuel-core binary on the machine (e.g. CI without fuelup). It downloads and compiles the node as a dependency instead.

2.4 Starting a project from a Sway template

If you are starting from a Sway contract, the official cargo generate template scaffolds a Rust integration‑test harness for you:

cargo install cargo-generate
cargo generate --init fuellabs/sway templates/sway-test-rs --name my_fuel_project --force
forc build
cargo test

Note

--force preserves the casing of your --name for the {{project-name}} placeholder; without it, cargo-generate lowercases and kebab‑cases the name.


3. Core Concepts

A handful of concepts recur throughout the SDK. Understanding them up front makes everything else click.

Concept What it is
Provider A read‑only connection to a fuel-core node. Queries balances, coins, blocks, transactions; submits transactions.
Signer Something that can produce a signature. Built‑ins: PrivateKeySigner, AWS/Google KMS signers, FakeSigner.
Wallet An account backed by a key. Wallet<Unlocked<S>> has a signer (can sign/send); Wallet<Locked> knows only an address (read‑only).
Account A trait (Account / ViewOnlyAccount) implemented by both Wallet and Predicate. The common interface for balances and transfers.
Contract A program with persistent storage deployed on‑chain. The abigen! macro generates a type‑safe binding for it.
Script A one‑shot executable run inside a transaction. Has no storage.
Predicate A pure program that guards an address; it returns a boolean and can own assets.
Asset / Coin / UTXO Fuel uses a UTXO model. Each coin is a UTXO with an amount of a given AssetId. The base asset (AssetId 0x000…0) pays fees.
abigen! A procedural macro that turns a JSON ABI into Rust structs and methods, checked at compile time.

Important

Fuel is a UTXO chain, not an account‑balance chain. "Your balance" for an asset is the sum of the amounts of all your unspent coins of that asset. The SDK hides most of this, but it explains why you'll see Coin, UtxoId, and "spendable resources" in the API.


4. Connecting to a Fuel Node

Every interaction needs a Provider pointing at a fuel-core node. You have two broad options:

  1. Connect to a real node (testnet, mainnet, or your own fuel-core) — for building applications.
  2. Launch a short‑lived local node with the test helpers — for contract testing.

4.1 Connecting to the testnet or an external node

use std::str::FromStr;
use fuels::{crypto::SecretKey, prelude::*};

// Create a provider pointing to the testnet.
let provider = Provider::connect("testnet.fuel.network").await?;

// Build a wallet from a private key.
let secret = SecretKey::from_str(
    "a1447cd75accc6b71a976fd3401a1f6ce318d27ba660b0315ee6ac347bf39568",
)?;
let wallet = Wallet::new(PrivateKeySigner::new(secret), provider);

// Print the address so the faucet can fund it.
dbg!(wallet.address().to_string());

To connect to a local node started with fuel-core directly, just change the URL:

let provider = Provider::connect("127.0.0.1:4000").await?;

Note

A fresh testnet wallet has no assets. Fund it via the testnet faucet by pasting the address printed above. Once funded, reuse the same private key across runs.

Tip

Provider::connect_with_fallbacks(&[url1, url2, …]) lets you supply several node URLs; the SDK uses the first reachable one.

4.2 Running a short‑lived node (for testing)

The test helpers spin up a local node and tear it down with the test. The simplest entry point gives you a single funded wallet:

let wallet = launch_provider_and_get_wallet().await?;

For several wallets, use launch_custom_provider_and_get_wallets:

// 10 wallets, each with 1 coin of 1_000_000_000 of the base asset (defaults).
let wallets =
    launch_custom_provider_and_get_wallets(WalletsConfig::default(), None, None).await?;

If you only need a Provider (no wallet), use setup_test_provider:

let provider = setup_test_provider(vec![], vec![], None, None).await?;
let wallet = Wallet::random(&mut rand::thread_rng(), provider);

Note

Wallets produced by launch_provider_and_get_wallet and launch_custom_provider_and_get_wallets have deterministic addresses (their secret keys are derived from a counter). This makes tests reproducible.

4.3 Persistent storage with RocksDB

By default a test node keeps state in memory. To persist the chain locally, enable the rocksdb + fuel-core-lib features and point the node at a database path:

use std::path::PathBuf;

let provider_config = NodeConfig {
    database_type: DbType::RocksDb(Some(PathBuf::from("/tmp/.fuel/db"))),
    ..NodeConfig::default()
};

launch_custom_provider_and_get_wallets(Default::default(), Some(provider_config), None).await?;

Note

If the database path does not exist, a new database is created there. You need either the fuel-core binary present, or both the fuel-core-lib and rocksdb features enabled.

4.4 Querying the blockchain

A Provider exposes a broad read API. Common queries:

// All unspent coins of an asset owned by an address.
let coins = provider.get_coins(&address, *consensus_parameters.base_asset_id()).await?;

// Spendable resources matching a filter (asset, amount, exclusions).
let filter = ResourceFilter { from: address, amount: 1, ..Default::default() };
let spendable = provider.get_spendable_resources(filter).await?;

// Per-asset balances (HashMap<asset_id_hex, amount>).
let balances = provider.get_balances(&address).await?;

Other useful provider calls include chain_info(), consensus_parameters(), latest_block_height(), produce_blocks(), get_transaction_by_id(), get_message_proof(), and estimate_gas_price(). See the Admin Guide and docs.rs/fuels for the full surface.

4.5 Retrying failed requests

A Provider can automatically retry requests that fail with an io::Error:

use std::time::Duration;

let retry_config = RetryConfig::new(3, Backoff::Fixed(Duration::from_secs(2)))?;
let provider = setup_test_provider(coins, vec![], None, None)
    .await?
    .with_retry_config(retry_config);

Backoff controls the wait between attempts:

Variant Behaviour
Linear(Duration) (default) Wait grows linearly with each attempt.
Exponential(Duration) Wait doubles with each attempt.
Fixed(Duration) Constant wait between attempts.

Warning

All node errors currently surface as io::Error, so a retry can happen even for non‑transient failures (e.g. a transaction that failed verification). Tune the attempt count accordingly.


5. Wallets & Accounts

A wallet is your interface to coins, balances, transfers, and signing. Every wallet needs a Provider.

5.1 Locked vs. unlocked wallets

The SDK encodes "can this wallet sign?" in the type:

Type Has signer? Implements Use for
Wallet<Unlocked<S>> ✅ Account + ViewOnlyAccount Signing, transfers, contract calls, deploys
Wallet<Locked> ❌ ViewOnlyAccount Read‑only: balances, coins, transaction history

Move from unlocked to locked with lock():

let wallet_locked = wallet_unlocked.lock();

Tip

When designing functions that accept a wallet, ask for the least access you need. Prefer Wallet<Locked> / impl ViewOnlyAccount for read‑only work so signers spend as little time in memory as possible.

5.2 Creating wallets from private keys

use std::str::FromStr;
use fuels::{crypto::SecretKey, prelude::*};

let provider = setup_test_provider(vec![], vec![], None, None).await?;

let secret = SecretKey::from_str(
    "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
)?;
let wallet = Wallet::new(PrivateKeySigner::new(secret), provider);

A random wallet:

let wallet = Wallet::random(&mut rand::thread_rng(), provider);

5.3 Creating wallets from a mnemonic phrase

let phrase = "oblige salon price punch saddle immune slogan rare snap desert retire surprise";

// A specific derivation path.
let key = SecretKey::new_from_mnemonic_phrase_with_path(phrase, "m/44'/1179993420'/0'/0/0")?;
let wallet = Wallet::new(PrivateKeySigner::new(key), provider.clone());

// Or the SDK's default derivation path.
let key = SecretKey::new_from_mnemonic_phrase_with_path(phrase, DEFAULT_DERIVATION_PATH)?;
let wallet = Wallet::new(PrivateKeySigner::new(key), provider);

Note

Fuel's default derivation path is m/44'/1179993420'/0'/0/0 (DEFAULT_DERIVATION_PATH). The coin type 1179993420' is Fuel's registered BIP‑44 value.

5.4 Encrypting and storing keys (Keystore)

Never store a raw private key or mnemonic on disk. The Keystore (feature accounts-keystore) encrypts a key with a master password:

use fuels::accounts::keystore::Keystore;

let dir = std::env::temp_dir();
let keystore = Keystore::new(&dir);

let key = SecretKey::new_from_mnemonic_phrase_with_path(phrase, DEFAULT_DERIVATION_PATH)?;
let uuid = keystore.save_key(key, "my_master_password", rand::thread_rng())?;

// Later, recover it with the same password.
let recovered_key = keystore.load_key(&uuid, "my_master_password")?;

Caution

Anyone with the master password and the encrypted file can recover the key. Treat both as secrets. The recovered SecretKey is zeroized on drop, but your own copies are not.

5.5 Signing with a KMS

For production key management, delegate signing to AWS or Google Cloud KMS so the private key never touches your process. Enable accounts-signer-aws-kms or accounts-signer-google-kms and construct the corresponding signer, then build a wallet the usual way (Wallet::new(kms_signer, provider)). The signer types live under fuels::accounts::signers::kms.

5.6 Impersonating an account (FakeSigner)

For tests against a node with utxo_validation = false, FakeSigner lets you simulate ownership of an address's assets without its key:

let node_config = NodeConfig { utxo_validation: false, ..Default::default() };
let provider = setup_test_provider(coins, vec![], Some(node_config), None).await?;

let fake_signer = FakeSigner::new(some_address);
let impersonator = Wallet::new(fake_signer, provider.clone());

Warning

A FakeSigner only unlocks assets when the network does not enforce signature validation (utxo_validation = false). It is a testing tool, never a production one.

5.7 Checking balances and coins

use std::collections::HashMap;

// Sum of all spendable coins for one asset.
let asset_id = AssetId::zeroed();
let balance: u128 = wallet.get_asset_balance(&asset_id).await?;

// All balances, keyed by asset-id hex string.
let balances: HashMap<String, u128> = wallet.get_balances().await?;
let base_balance = balances.get(&asset_id.to_string()).unwrap();

5.8 Transferring assets

The Account trait provides three transfer methods:

Method Destination
transfer Another address (wallet or predicate)
force_transfer_to_contract A contract's balance
withdraw_to_base_layer An address on the base (L1) chain

Wallet → wallet:

let _res = wallets[0]
    .transfer(wallets[1].address(), 1 /* amount */, AssetId::zeroed(), TxPolicies::default())
    .await?;

Wallet → contract:

let _res = wallet
    .force_transfer_to_contract(contract_id, 300, asset_id, TxPolicies::default())
    .await?;

Wallet → base layer:

let response = wallet
    .withdraw_to_base_layer(base_layer_address, 1000, TxPolicies::default())
    .await?;

// Verify with a message proof.
let proof = wallet
    .try_provider()?
    .get_message_proof(&response.tx_id, &response.nonce, None, Some(2))
    .await?;
assert_eq!(proof.amount, 1000);

Caution

force_transfer_to_contract transfers coins unconditionally. If the receiving contract has no logic to account for the funds, they may be permanently lost. Use with care.

5.9 Signing messages and transactions

Sign an arbitrary message with any signer:

let message = Message::new([0; 32]);
let signature = wallet.signer().sign(message).await?;

When building a transaction, you usually add a signer to the builder rather than signing manually. The builder defers witness‑index resolution until build():

tb.add_signer(signer.clone())?;
let tx = tb.build(&provider).await?;

Note

Witness indexes (which tie a signed input to its signature) only become valid once input order is final. The SDK resolves them automatically at build() time, so add signers before building. To sign an already‑built transaction, use tx.sign_with(...) and ensure signature order matches signed‑input order.


6. Generating Contract Bindings with abigen!

To call a Sway program from Rust you first generate type‑safe bindings from its JSON ABI. The abigen! macro does this at compile time.

6.1 The JSON ABI file

When you compile a Sway program with forc build, you get two artifacts: the compiled binary (*.bin) and a JSON ABI (*-abi.json). The ABI describes the program's functions, inputs, and outputs — it is what tells the SDK how to encode calls and decode results.

[
  {
    "type": "function",
    "inputs": [],
    "name": "test_function",
    "outputs": [{ "name": "", "type": "bool", "components": null }]
  }
]

6.2 The abigen! macro

abigen! accepts one or more program declarations of the form ProgramType(name=…, abi=…):

  • ProgramType is Contract, Script, or Predicate.
  • name is the Rust name for the generated binding.
  • abi is either a path to the JSON ABI file or the ABI's literal contents.
abigen!(
    Contract(
        name = "ContractA",
        abi = "e2e/sway/bindings/sharing_types/contract_a/out/release/contract_a-abi.json"
    ),
    Contract(
        name = "ContractB",
        abi = "e2e/sway/bindings/sharing_types/contract_b/out/release/contract_b-abi.json"
    ),
    Script(
        name = "MyScript",
        abi = "e2e/sway/scripts/arguments/out/release/arguments-abi.json"
    ),
    Predicate(
        name = "MyPredicateEncoder",
        abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
    ),
);

6.3 What gets generated

abigen! emits, under an abigen_bindings module, one sub‑module per program (named after name). Each contains the program's custom types (structs/enums) and the binding through which calls are made. Convenience pub use re‑exports are added so you don't have to fully qualify the types.

pub mod abigen_bindings {
    pub mod contract_a_mod {
        struct SomeCustomStruct { /* ... */ }
        struct ContractA { /* ... */ }
        // ...
    }
    pub mod shared_types { /* ... */ }
}

If two programs share an identical type (same name and definition), it is lifted into a generated shared_types module and reused, with re‑exports so each program's module can still reference it as its own.

Important

Generate all your bindings in a single abigen! call whenever possible. This enables type sharing and avoids name collisions you'd otherwise hit when calling abigen! multiple times in one namespace. If two unrelated types share a name, only uniquely‑named types get a pub use; qualify the rest as abigen_bindings::some_contract_mod::TheType.

Note

Everything abigen! emits is generated code — you never write it. Predicate names get Encoder and Configurables suffixes appended automatically: name = "MyPredicate" yields MyPredicateEncoder and MyPredicateConfigurables.


7. Deploying Contracts

There are two ways to work with contracts: deploy one yourself, or connect to an already‑deployed one.

7.1 Deploying a compiled contract

Load the .bin produced by forc build, then deploy():

let wallet = launch_provider_and_get_wallet().await?;

let contract_id = Contract::load_from(
    "e2e/sway/contracts/contract_test/out/release/contract_test.bin",
    LoadConfiguration::default(),
)?
.deploy(&wallet, TxPolicies::default())
.await?
.contract_id;

println!("Contract deployed @ {contract_id}");

7.2 Configuring the load with LoadConfiguration

LoadConfiguration controls how the binary is loaded before deployment. It lets you:

  • Apply a Salt so the same binary yields a new contract_id (enables redeploying the same code).
  • Override storage slots.
  • Set configurable constants.
use rand::prelude::{Rng, SeedableRng, StdRng};

let salt: [u8; 32] = StdRng::seed_from_u64(2322).r#gen();

let storage_configuration =
    StorageConfiguration::default().add_slot_overrides([StorageSlot::new(key, value)]);

let configuration = LoadConfiguration::default()
    .with_storage_configuration(storage_configuration)
    .with_salt(salt);

let tx_policies = TxPolicies::default()
    .with_tip(1)
    .with_script_gas_limit(1_000_000)
    .with_maturity(0)
    .with_expiration(10_000);

let contract_id = Contract::load_from("....bin", configuration)?
    .deploy(&wallet, tx_policies)
    .await?
    .contract_id;

Warning

Redeploying an identical binary with the same salt produces the same contract_id and fails with a collision. Use with_salt(...) (or LoadConfiguration::default().with_salt(...)) to get a fresh ID each time.

7.3 Configurable constants

Sway configurable constants can be overridden at deploy time. Each constant gets a generated with_<NAME> method on a Configurables struct:

let configurables = MyContractConfigurables::default()
    .with_STR_4("FUEL".try_into()?)?
    .with_U8(42)?;

let contract_id = Contract::load_from("....bin", LoadConfiguration::default().with_configurables(configurables))?
    .deploy(&wallet, TxPolicies::default())
    .await?
    .contract_id;

7.4 Overriding storage slots

If your contract uses storage, forc generates a *-storage_slots.json of default values, which the SDK loads automatically. To override some defaults:

use fuels::{programs::contract::Contract, tx::StorageSlot};

let slot_override = StorageSlot::new([1; 32].into(), [2; 32].into());
let storage_config = StorageConfiguration::default().add_slot_overrides([slot_override]);
let load_config = LoadConfiguration::default().with_storage_configuration(storage_config);
let _ = Contract::load_from("....bin", load_config);

To disable auto‑loading entirely (e.g. you don't have the slots file):

let storage_config = StorageConfiguration::default().with_autoload(false);
let load_config = LoadConfiguration::default().with_storage_configuration(storage_config);

Note

With autoload enabled (the default) and no *-storage_slots.json next to the binary, load_from fails with "could not autoload storage slots from file … Either provide the file or disable autoloading". Disable autoload or supply the file.

7.5 Connecting to an already‑deployed contract

You don't need to redeploy to call an existing contract — just bind its ID:

abigen!(Contract(name = "MyContract", abi = "....abi.json"));

let contract_id: ContractId =
    "0x65b6a3d081966040bbccbb7f79ac91b48c635729c59a4c02f15ae7da999b32d3".parse()?;

let contract = MyContract::new(contract_id, wallet);

7.6 Deploying large contracts (blobs & loaders)

If a contract exceeds the single‑transaction size limit, deploy it as a loader: the code is split into blobs uploaded via blob transactions, and a small loader contract loads them at runtime.

The simplest path — convert to a loader and deploy:

let max_words_per_blob = 10_000;
let contract_id = Contract::load_from("....bin", LoadConfiguration::default().with_salt(salt))?
    .convert_to_loader(max_words_per_blob)?
    .deploy(&wallet, TxPolicies::default())
    .await?
    .contract_id;

Let the SDK decide automatically (normal deploy if small enough, loader if not):

let contract_id = Contract::load_from("....bin", LoadConfiguration::default().with_salt(salt))?
    .smart_deploy(&wallet, TxPolicies::default(), max_words_per_blob)
    .await?
    .contract_id;

Estimate a safe blob size:

let max_blob_size = BlobTransactionBuilder::default()
    .estimate_max_blob_size(&provider)
    .await?;

Blob size is bounded by (1) the max transaction size, (2) the max gas per transaction, and (3) the node's max HTTP body size. The estimate above only considers (1), so make your blobs a few percent smaller than the estimate.

Tip

For fine‑grained control you can upload blobs first (upload_blobs), split code into blobs manually (Contract::loader_from_blobs / loader_from_blob_ids), and deploy the loader separately. See the cookbook and Admin Guide for the low‑level flow.


8. Calling Contracts

Once you have a bound contract instance, call its ABI methods through .methods().

8.1 A basic call

let contract = MyContract::new(contract_id, wallet);

let response = contract
    .methods()
    .initialize_counter(42) // build the call
    .call()                 // submit it
    .await?;                // await the result

assert_eq!(42, response.value);

Note

You'll chain .call().await? constantly. That's because you choose .call() vs .simulate(), the call is async (so you .await), and it returns a Result you handle with ? (or .unwrap()).

To separate submission from result retrieval:

let submitted = contract.methods().initialize_counter(42).submit().await?;
// ... do other work ...
let value = submitted.response().await?.value;

8.2 The call response

call() returns a CallResponse<D> (here D is the decoded return type):

pub struct CallResponse<D> {
    pub value: D,           // the decoded return value
    pub receipts: Vec<Receipt>,
    pub gas_used: u64,
    pub tx_id: Option<TxId>,
    // ...
}
  • value is the method's return value, decoded to the matching Rust type (a Sway u64 → u64, a Sway struct → a generated Rust struct, b256 → [u8; 32], …).
  • receipts holds every receipt the call produced.
  • gas_used is the gas the call consumed.
  • tx_id is the submitted transaction's ID.

8.3 Transaction policies

TxPolicies configures transaction‑level limits and pricing. Attach them with with_tx_policies:

let tx_policies = TxPolicies::default()
    .with_tip(1)
    .with_script_gas_limit(1_000_000)
    .with_maturity(0)
    .with_expiration(10_000);

let response = contract.methods().initialize_counter(42)
    .with_tx_policies(tx_policies)
    .call()
    .await?;
Policy Meaning
Tip Amount paid to the block producer to prioritise the transaction.
Witness Limit Maximum witness‑data size. If unset, the SDK sets it from the actual witnesses/signatures.
Maturity Block height before which the transaction cannot be included.
Expiration Block height after which the transaction cannot be included.
Max Fee Maximum fee the transaction may pay.
Script Gas Limit Max gas for executing the script. If unset, the SDK estimates it for you.

TxPolicies::default() uses sensible defaults (the SDK estimates gas and witness limits automatically).

8.4 Call parameters (forwarding coins & gas)

CallParameters controls what is forwarded to the contract call itself: amount, asset ID, and forwarded gas. Attach with call_params (which returns a Result):

let call_params = CallParameters::default().with_amount(1_000_000);

let response = contract.methods()
    .get_msg_amount()
    .call_params(call_params)?   // returns Result — errors if forwarding to a non-payable method
    .call()
    .await?;

Forwarding a specific asset and gas:

let call_params = CallParameters::default()
    .with_amount(deposit_amount)
    .with_asset_id(base_asset_id)
    .with_gas_forwarded(4300);

Warning

call_params returns a Result so the SDK can stop you forwarding coins (amount ≠ 0) to a method not annotated #[payable] in Sway. Forwarding gas is always allowed regardless of payability. The default CallParameters forwards 0 of the base asset; if you don't set parameters, the whole transaction gas limit is forwarded.

8.5 Custom asset transfer alongside a call

Transfer an arbitrary asset to an address within the same transaction as a contract call:

let _ = contract.methods()
    .initialize_counter(42)
    .add_custom_asset(AssetId::zeroed(), 1000, Some(destination_addr))
    .call()
    .await?;

8.6 Custom inputs and outputs

Add specific transaction inputs/outputs to a call:

let _ = contract.methods()
    .initialize_counter(42)
    .with_inputs(custom_inputs)
    .with_outputs(custom_outputs)
    .add_signer(other_wallet.signer().clone())
    .call()
    .await?;

Note

If your custom inputs include coins that must be signed, attach the matching signer with add_signer.

8.7 Variable outputs

If a contract method transfers funds out (e.g. minting tokens to an address), the transaction needs variable outputs. Set the policy with with_variable_output_policy:

let response = contract.methods()
    .transfer(1_000_000, asset_id, address.into())
    .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
    .call()
    .await?;
VariableOutputPolicy Behaviour
Exactly(n) Allocate exactly n variable outputs.
EstimateMinimum Let the SDK estimate the minimum needed.

Note

Sway's mint_to_address calls transfer_to_address under the hood, so it needs variable outputs too.

8.8 Calling other contracts

If a method calls another contract, declare the external contract(s) so the right inputs/outputs are added:

// With instances (enables log & require-revert decoding from the external contract):
let response = contract.methods().my_method()
    .with_contracts(&[&other_contract_instance])
    .call().await?;

// With raw IDs (no log decoding):
let response = contract.methods().my_method()
    .with_contract_ids(&[external_contract_id])
    .call().await?;

8.9 Letting the SDK estimate dependencies

Rather than specifying external contracts and variable outputs by hand, chain with_variable_output_policy(EstimateMinimum) and determine_missing_contracts() to have the SDK figure them out (at the cost of extra simulated calls):

let response = contract.methods()
    .mint_then_increment_from_contract(called_contract_id, amount, address.into())
    .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
    .determine_missing_contracts()
    .await?
    .call()
    .await?;

Note

determine_missing_contracts() does not enable logging from external contracts — for that you still need with_contracts(&[…]). The same helpers work for script and multi‑calls.

8.10 Multiple calls in one transaction (multicalls)

Bundle several calls into one transaction with CallHandler::new_multi_call:

let methods = MyContract::new(contract_id, wallet.clone()).methods();
let call_1 = methods.initialize_counter(42);
let call_2 = methods.get_array([42; 2]);

let multi = CallHandler::new_multi_call(wallet.clone())
    .add_call(call_1)
    .add_call(call_2)
    .with_tx_policies(TxPolicies::default());

// Supply explicit type annotations to decode the bundled return values.
let (counter, array): (u64, [u64; 2]) = multi.call().await?.value;

Note

Per‑call TxPolicies are ignored in favour of the multi‑call's policies. Per‑call parameters, variable outputs, and external contracts are honoured, as long as you haven't already executed them. Custom input/output order follows the order calls are added.

8.11 Simulating calls

To execute a call without changing chain state, use .simulate(...) with an execution context:

// Realistic: behaves like a real call. Needs a wallet with base assets (none are consumed).
let counter = contract.methods().mint_coins(100)
    .simulate(Execution::realistic()).await?;

// State-read-only: disables most validation; read state with an unfunded account.
let balance = contract.methods().get_balance(contract_id, AssetId::zeroed())
    .simulate(Execution::state_read_only()).await?.value;

If the node supports historical execution (using rocksdb and started with --historical-execution), simulate at a specific height:

let balance = contract.methods().get_balance(contract_id, AssetId::zeroed())
    .simulate(Execution::state_read_only().at_height(block_height)).await?.value;

8.12 Estimating call cost

let transaction_cost = contract.methods()
    .initialize_counter(42)
    .estimate_transaction_cost(Some(0.0) /* tolerance */, Some(1) /* block_horizon */)
    .await?;

println!("total gas: {}", transaction_cost.total_gas);

TransactionCost exposes gas_price, metered_bytes_size, total_fee, script_gas, and total_gas. The same estimate_transaction_cost method exists on multi‑call handlers and script calls.

8.13 Calling with a different wallet

with_account returns a new instance bound to another wallet:

let response = contract
    .with_account(wallet_2)
    .methods()
    .get_msg_amount()
    .call()
    .await?;

Note

Connecting a different wallet ignores that wallet's provider in favour of the provider used to deploy the contract. This only matters if you have multiple fuel-core instances in play.

8.14 Configuring the encoder/decoder for a call

You can tune the ABI encoder (for arguments) and decoder (for return values):

let _ = contract
    .with_encoder_config(EncoderConfig { max_depth: 10, max_tokens: 2_000 })
    .methods()
    .initialize_counter(42)
    .with_decoder_config(DecoderConfig { max_depth: 10, max_tokens: 2_000 })
    .call()
    .await?;

8.15 Low‑level calls

For dynamic, runtime‑specified calls (indirect calls through another contract), build an encoded function selector and calldata:

use fuels::core::codec::{calldata, encode_fn_selector};

let function_selector = encode_fn_selector("set_value_multiple_complex");
let call_data = calldata!(
    MyStruct { a: true, b: [1, 2, 3] },
    SizedAsciiString::<4>::try_from("fuel")?
)?;

caller_contract.methods()
    .call_low_level_call(target_id, Bytes(function_selector), Bytes(call_data))
    .determine_missing_contracts().await?
    .call().await?;

Note

The calldata! macro uses the default EncoderConfig.


9. Running Scripts

A script is a one‑shot executable run inside a transaction. Generate its bindings with abigen!(Script(...)), then run it with arguments:

abigen!(Script(name = "MyScript", abi = "....abi.json"));

let script_instance = MyScript::new(wallet, "path/to/script.bin");
let response = script_instance.main(arg_1, arg_2).call().await?;

Scripts support the same building blocks as contract calls:

  • Transaction policies — with_tx_policies(...), identical to contracts.
  • Custom inputs/outputs — with_inputs / with_outputs (add add_signer for signed coins).
  • Logs — decode_logs() and decode_logs_with_type::<T>() (see §10).
  • Calling contracts — with_contracts(&[…]) / with_contract_ids(&[…]).
  • Configurable constants — each configurable gets a with_<NAME> method, set via the generated configurables struct.
  • Cost estimation — estimate_transaction_cost(...).
let response = script_instance
    .main(arg)
    .with_tx_policies(TxPolicies::default())
    .with_contract_ids(&[external_id])
    .call()
    .await?;

10. Logs

When a Sway method logs a value, the SDK can decode it back into a Rust type. From a CallResponse:

// Decode all logs of a given type into a Vec<T>.
let logs: Vec<MyType> = response.decode_logs_with_type::<MyType>()?;

// Decode every log into a LogResult (each entry is a Result<String>).
let log_result = response.decode_logs();

Warning

decode_logs() can be expensive — reserve it for debugging, not hot paths. Sway string slices cannot be logged directly; convert them with __to_str_array() to a str[N] first.

The same two functions are available for script calls.


11. Predicates

A predicate is a pure Sway program that returns a boolean and can own assets. A predicate's address is derived from its bytecode (like a Bitcoin P2SH address). To spend a predicate's funds, you supply the original bytecode plus the predicate data; if the predicate validates against that data, the funds can move.

11.1 Instantiating a predicate

abigen!(Predicate(name = "MyPredicate", abi = "....abi.json"));

// Encode the predicate's main-function arguments.
let predicate_data = MyPredicateEncoder::default().encode_data(4096, 4096)?;

let predicate: Predicate = Predicate::load_from("....bin")?
    .with_provider(provider)
    .with_data(predicate_data);

11.2 Funding and spending a predicate

A predicate implements the Account trait, so it transfers like any account:

// Fund the predicate from a wallet.
wallet.transfer(predicate.address(), 500, asset_id, TxPolicies::default()).await?;
assert_eq!(predicate.get_asset_balance(&asset_id).await?, 500);

// Spend predicate funds (predicate must validate against its data).
predicate.transfer(receiver.address(), 300, asset_id, TxPolicies::default()).await?;

11.3 A signature‑checking predicate

Predicates commonly validate signatures. For a predicate that requires 2‑of‑3 matching signatures:

let data_to_sign = Message::new([0; 32]);
let signature1: B512 = wallet.signer().sign(data_to_sign).await?.as_ref().try_into()?;
// ... signature2, signature3 ...

let predicate_data = MyPredicateEncoder::default().encode_data([signature1, signature2, signature3])?;
let predicate = Predicate::load_from("signatures.bin")?
    .with_provider(provider)
    .with_data(predicate_data);

11.4 Configurable constants in predicates

Like contracts and scripts, predicates support configurable constants. Each gets a with_<NAME> method on the generated <Name>Configurables struct, applied with with_configurables. Note that changing a predicate's configurables changes its bytecode and therefore its address.


12. Custom Transactions

When the high‑level helpers aren't enough, build transactions directly with the transaction builders.

12.1 The transaction builders

The SDK provides CreateTransactionBuilder and ScriptTransactionBuilder (plus the TransactionBuilder trait). Calling build(&provider) yields a CreateTransaction or ScriptTransaction ready to submit. The builders handle two fiddly concerns for you:

  • Predicate data offsets — pointers inside dynamic predicate data stay valid because the builder resolves them only at build().
  • Witness indexing — signed inputs reference witnesses by index; the builder assigns correct indexes at finalisation.

They also make a built transaction immutable, so its transaction ID can't accidentally change.

12.2 Building a custom transaction

A worked example: use a hot wallet to pay a predicate's ask_amount, unlocking the predicate's bridged asset, which is sent to cold storage.

// Start with a ScriptTransactionBuilder (no contract deploy).
let tb = ScriptTransactionBuilder::default();

// Inputs/outputs of the base asset (paid to unlock the predicate).
let base_inputs = hot_wallet
    .get_asset_inputs_for_amount(*cp.base_asset_id(), ask_amount, None).await?;
let base_outputs = hot_wallet
    .get_asset_outputs_for_amount(receiver, *cp.base_asset_id(), ask_amount as u64);

// Inputs/outputs that move the predicate's bridged asset to cold storage.
let other_inputs = predicate
    .get_asset_inputs_for_amount(bridged_asset_id, locked_amount, None).await?;
let other_outputs =
    predicate.get_asset_outputs_for_amount(cold_wallet.address(), bridged_asset_id, 500);

let inputs = base_inputs.into_iter().chain(other_inputs).collect();
let outputs = base_outputs.into_iter().chain(other_outputs).collect();
let mut tb = tb.with_inputs(inputs).with_outputs(outputs);

// Sign with the hot wallet (signature created at build()).
tb.add_signer(hot_wallet.signer().clone())?;

// Cover the fee. Second arg = base asset we expect to spend regardless of fees.
hot_wallet.adjust_for_fee(&mut tb, ask_amount).await?;

// Optional policies.
let tb = tb.with_tx_policies(TxPolicies::default().with_maturity(64).with_expiration(128));

// Build & submit.
let tx = tb.build(&provider).await?;
let tx_id = provider.send_transaction(tx).await?;

Tip

Add signers before calling adjust_for_fee() — the fee estimate includes witness sizes, so adding signers afterwards can under‑fund the transaction.

12.3 Building without signatures

For cost estimation or simulation you may want an unsigned transaction. Change the build strategy:

let tx = tb.with_build_strategy(ScriptBuildStrategy::NoSignatures).build(&provider).await?;

Note

When signing a built transaction (rather than adding signers to the builder), signature order must match the order of signed inputs. Multiple signed inputs with the same owner share one witness index.

12.4 Customising a contract or script call's transaction

You can fetch the underlying builder from a CallHandler, tweak it, submit it, and still decode the result:

let call_handler = contract.methods().initialize_counter(counter);
let mut tb = call_handler.transaction_builder().await?;
// ... customise tb ...
wallet.adjust_for_fee(&mut tb, 0).await?;
wallet.add_witnesses(&mut tb)?;

let tx = tb.build(provider).await?;
let tx_id = provider.send_transaction(tx).await?;
let tx_status = provider.tx_status(&tx_id).await?;
let response = call_handler.get_response(tx_status)?;

13. Pre‑uploading Code (Blobs)

For a script or predicate that is large or called often, pre‑upload its code as a blob and run a small loader instead. The loader can still carry per‑run configurables, so you change configuration without re‑uploading the (large) code.

Scripts — high‑level:

let result = my_script
    .convert_into_loader()   // uploads the blob internally
    .await?
    .main(arg)
    .call()
    .await?;

Predicates — prepare without network requests, then upload the blob before executing:

let predicate = predicate.convert_into_loader()?;     // prepare (no network)
predicate.upload_blob(&wallet).await?;                // upload the code blob
// subsequent spends are cheaper

Tip

Pre‑uploading amortises the cost of large code across many callers — every later call references the already‑uploaded blob instead of re‑embedding the code.


14. Working with Types

The SDK ships Rust equivalents for Fuel/Sway types. Many are re‑exported from fuel-types; all are reachable through fuels::types.

Sway type Rust type (in fuels::types) Notes
b256 Bits256 / [u8; 32] 256‑bit value
Address Address 32‑byte account address
ContractId ContractId 32‑byte contract identifier
AssetId AssetId 32‑byte asset identifier (base asset = AssetId::zeroed())
b512 B512 512‑bit value (e.g. signatures)
Bytes32 Bytes32 32 raw bytes
Bytes Bytes dynamic byte vector
str[N] SizedAsciiString<N> fixed‑length ASCII
String (std lib) AsciiString / String dynamic string
Vec<T> Vec<T> dynamic vector
EvmAddress EvmAddress 20‑byte Ethereum‑style address
structs / enums generated by abigen! / #[derive(...)] see Codec below

Construct and convert as ordinary Rust:

let asset_id = AssetId::zeroed();                          // base asset
let addr = Address::from_str("0x4710…d8fe")?;             // from hex
let contract_id: ContractId = "0x65b6…32d3".parse()?;     // via FromStr
let sized: SizedAsciiString<4> = "fuel".try_into()?;

Note

The base asset's AssetId is a 32‑byte zeroed value (AssetId::zeroed(), 0x000…0). It is the only asset that can pay transaction fees and gas.


15. Encoding & Decoding (Codec)

Encoding/decoding follows the Fuel ABI spec via ABIEncoder and ABIDecoder.

15.1 The two traits

To encode a value you turn it into a Token (via the Tokenizable trait). To decode you also need a ParamType describing the type's schema (via the Parameterize trait). All abigen!‑generated types implement both, and the SDK implements them for built‑in and many foreign types (u8, u16, Vec<T>, …).

Derive them for your own structs/enums:

use fuels::macros::{Parameterize, Tokenizable};

#[derive(Parameterize, Tokenizable)]
struct MyStruct { field_a: u8 }

#[derive(Parameterize, Tokenizable)]
enum SomeEnum { A(MyStruct), B(Vec<u64>) }

Note

Deriving Tokenizable on enums requires every variant to also implement Parameterize. If fuels isn't reachable as ::fuels, point the derive at the right paths with #[FuelsCorePath = "…"] / #[FuelsTypesPath = "…"]. For no_std output, add #[NoStd].

15.2 Encoding

use fuels::core::{codec::ABIEncoder, traits::Tokenizable};

let instance = MyStruct { field: 101 };
let encoded: Vec<u8> = ABIEncoder::default().encode(&[instance.into_token()])?;

The calldata! macro encodes several Tokenizable values at once:

let bytes: Vec<u8> = calldata!(MyStruct { field: 101 }, MyStruct { field: 102 })?;

Tune limits with EncoderConfig:

ABIEncoder::new(EncoderConfig { max_depth: 5, max_tokens: 100 });

15.3 Decoding

use fuels::core::{codec::ABIDecoder, traits::{Parameterize, Tokenizable}, };

let bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 101];
let token = ABIDecoder::default().decode(&MyStruct::param_type(), bytes)?;
let value: MyStruct = MyStruct::from_token(token)?;

If a type derives TryFrom, you can decode bytes directly:

use fuels::macros::{Parameterize, Tokenizable, TryFrom};

#[derive(Parameterize, Tokenizable, TryFrom)]
struct MyStruct { field: u64 }

let value: MyStruct = bytes.try_into()?;   // calls try_from_bytes under the hood

Tune with DecoderConfig:

ABIDecoder::new(DecoderConfig { max_depth: 5, max_tokens: 100 });

Tip

DecoderConfig and EncoderConfig are safety limits against pathological inputs (deeply nested or huge token streams). Raise them only when you genuinely have large/deep types.


16. Testing Your Contracts

The SDK is built for testing. Most patterns above run against a local node from the test helpers.

16.1 Test wallets

// 5 wallets, 3 coins each, 100 per coin.
let config = WalletsConfig::new(Some(5), Some(3), Some(100));
let wallets = launch_custom_provider_and_get_wallets(config, None, None).await?;

Multiple custom assets per wallet:

let assets = vec![
    AssetConfig { id: AssetId::zeroed(), num_coins: 2, coin_amount: 4 },
    AssetConfig { id: asset_id_1,        num_coins: 6, coin_amount: 8 },
];
let config = WalletsConfig::new_multiple_assets(1, assets);
let wallets = launch_custom_provider_and_get_wallets(config, None, None).await?;

Lower‑level coin builders (setup_single_asset_coins, setup_multiple_assets_coins, setup_custom_assets_coins) let you craft exact UTXO sets to feed setup_test_provider.

Default constant Value
DEFAULT_NUM_WALLETS 10
DEFAULT_NUM_COINS 1
DEFAULT_COIN_AMOUNT 1_000_000_000

Warning

Generating a very large number of coins/assets is slow. Keep setup_multiple_assets_coins under ~1,000 coins and assets combined (or ~1,000,000 coins for a single asset) until the helpers are optimised.

16.2 The setup_program_test! macro

setup_program_test! collapses the wallet → abigen → deploy boilerplate. It accepts commands of the form COMMAND(ARG…):

setup_program_test!(
    Wallets("wallet"),
    Abigen(Contract(name = "TestContract", project = "e2e/sway/contracts/contract_test")),
    Deploy(name = "contract_instance", contract = "TestContract", wallet = "wallet"),
);

let response = contract_instance.methods().initialize_counter(42).call().await?;
assert_eq!(42, response.value);
Command Purpose Cardinality
Options(profile=…) Set the cargo/forc build profile ("release" default, "debug"). 0–1
Wallets("a", "b", …) Launch a provider and create named wallets. 0–1
Abigen(Contract/Script/Predicate(name=…, project=…)) Generate bindings from a forc project (built in release mode). 0–N
Deploy(name=…, contract=…, wallet=…) Deploy a contract (with salt) and bind an instance. 0–N
LoadScript(name=…, script=…, wallet=…) Create a script instance. 0–N

Note

Projects referenced by Abigen must be compiled in release mode (forc build --release). The macro deploys with salt, so the same contract can be deployed multiple times (different IDs). You can also create a wallet yourself and pass its name into the macro.

16.3 Tweaking the blockchain in tests

Advance the block height (useful for maturity tests):

// Requires `manual_blocks_enabled = true` on the node.
provider.produce_blocks(5, None).await?;

// With a custom block time:
provider.produce_blocks(5, Some(custom_time)).await?;

Customise consensus parameters via ChainConfig:

let tx_params = TxParameters::default().with_max_gas_per_tx(1_000).with_max_inputs(2);
let mut consensus_parameters = ConsensusParameters::default();
consensus_parameters.set_tx_params(tx_params);
let chain_config = ChainConfig { consensus_parameters, ..ChainConfig::default() };

let provider = setup_test_provider(coins, vec![], Some(NodeConfig::default()), Some(chain_config)).await?;

16.4 Running the SDK's own tests

The integration tests in e2e/tests/ exercise nearly the whole SDK. To run them, first build the Sway test projects, then run cargo test:

forc build --release --path e2e
cargo test
# Run a subset and show output:
cargo test --test types in_vector -- --show-output

# Capture output even on pass:
cargo test -- --nocapture

WASM tests live in packages/wasm-tests and run with wasm-pack test (add the wasm32-unknown-unknown target and install wasm-pack first).


17. Cookbook

17.1 Deposit and withdraw (liquidity pool)

abigen!(Contract(name = "MyContract", abi = "....liquidity_pool-abi.json"));

let methods = MyContract::new(contract_id, wallet.clone()).methods();

// Deposit: forward base asset to the contract.
let call_params = CallParameters::default().with_amount(1_000_000).with_asset_id(base_asset_id);
methods.deposit(wallet.address().into())
    .call_params(call_params)?
    .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
    .call().await?;

// Withdraw: forward the LP token back.
let lp_asset_id = contract_id.asset_id(&SubAssetId::zeroed());
let lp_balance = wallet.get_asset_balance(&lp_asset_id).await?;
let call_params = CallParameters::default()
    .with_amount(lp_balance.try_into().unwrap())
    .with_asset_id(lp_asset_id);
methods.withdraw(wallet.address().into())
    .call_params(call_params)?
    .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
    .call().await?;

17.2 Custom consensus parameters

use fuels::tx::{ConsensusParameters, FeeParameters, TxParameters};

let tx_params = TxParameters::default().with_max_gas_per_tx(1_000).with_max_inputs(2);
let fee_params = FeeParameters::default().with_gas_price_factor(10);

let mut consensus_parameters = ConsensusParameters::default();
consensus_parameters.set_tx_params(tx_params);
consensus_parameters.set_fee_params(fee_params);

let chain_config = ChainConfig { consensus_parameters, ..ChainConfig::default() };
let provider = setup_test_provider(coins, vec![], Some(NodeConfig::default()), Some(chain_config)).await?;

17.3 Transfer all assets in one transaction

Loop over a wallet's balances, building inputs/outputs per asset, then submit one transfer (keeping back half the base asset for fees):

let balances = wallet_1.get_balances().await?;
let mut inputs = vec![];
let mut outputs = vec![];
for (id_string, amount) in balances {
    let id = AssetId::from_str(&id_string)?;
    inputs.extend(wallet_1.get_asset_inputs_for_amount(id, amount, None).await?);
    let out_amount = if id == *cp.base_asset_id() { (amount / 2) as u64 } else { amount as u64 };
    outputs.extend(wallet_1.get_asset_outputs_for_amount(wallet_2.address(), id, out_amount));
}

let mut tb = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, TxPolicies::default());
wallet_1.add_witnesses(&mut tb)?;
let tx = tb.build(&provider).await?;
provider.send_transaction_and_await_commit(tx).await?;

18. Debugging

18.1 Generating a function selector

The SDK computes a function selector for every contract call. To compute one yourself:

use fuels::core::codec::encode_fn_selector;
let selector = encode_fn_selector("my_function");

18.2 Decoding script transactions

The SDK can make a script transaction human‑readable — telling you whether it is a contract call, a loader script (with the blob ID), or neither. With the ABI you can decode the call's arguments via ABIFormatter:

use fuels::{core::codec::ABIFormatter, programs::debug::ScriptType};

let TransactionType::Script(tx) = provider.get_transaction_by_id(&tx_id).await?.unwrap().transaction
    else { panic!("not a script transaction") };

let ScriptType::ContractCall(calls) = ScriptType::detect(tx.script(), tx.script_data())?
    else { panic!("not a contract call") };

let abi = ABIFormatter::from_json_abi(std::fs::read_to_string("....abi.json")?)?;
let call = &calls[0];
let fn_selector = call.decode_fn_selector()?;
let decoded_args = abi.decode_fn_args(&fn_selector, call.encoded_args.as_slice())?;

eprintln!("The script called: {fn_selector}({})", decoded_args.join(", "));
The script called: initialize_counter(42)

18.3 Inspecting the compiled binary

forc can disassemble a compiled binary to FuelVM assembly:

forc parse-bytecode out/release/my-test.bin
half-word   byte   op                raw           notes
        0   0      JI(4)             90 00 00 04   jump to byte 16
        1   4      NOOP              47 00 00 00
        ...

19. The fuels-abi CLI

fuels-abi-cli (sway-abi-cli) is a small utility to encode Sway function calls and decode their output.

sway-abi-cli <SUBCOMMAND>

SUBCOMMANDS:
    codegen   Output Rust types file
    decode    Decode ABI call result
    encode    Encode ABI call
    help      Prints help

Encode raw parameters (-v <type> <value> per parameter):

$ cargo run -- encode params -v bool true -v u32 42 -v u32 100
0000000000000001000000000000002a0000000000000064

Encode a full function call from a JSON ABI (the first 8 bytes carry the function selector):

$ cargo run -- encode function examples/simple.json takes_u32_returns_bool -p 4
000000006355e6ee0000000000000004

Decode parameters or a function's output:

$ cargo run -- decode params -t bool -t u32 -t u32 0000000000000001000000000000002a0000000000000064
Bool(true)
U32(42)
U32(100)

20. Troubleshooting

Error: no 'fuel-core' in PATH

Cause: The test helpers launch the fuel-core binary, which isn't installed or isn't on your PATH. Solution: Install the Fuel toolchain via fuelup, or enable the fuel-core-lib feature to run the node as a library instead. Prevention: In CI, install fuelup (and run fuelup toolchain install latest) before tests, or use fuel-core-lib.

Error: contract deployment fails with a contract‑ID collision

Cause: Deploying an identical binary with the same Salt produces the same contract_id, and the chain already has it. Solution: Load with a unique salt: LoadConfiguration::default().with_salt(random_salt). Prevention: Always randomise the salt when redeploying the same code in tests.

Error: could not autoload storage slots from file …

Cause: Autoloading is on (the default) but the *-storage_slots.json next to the binary is missing. Solution: Provide the slots file, or disable autoload: StorageConfiguration::default().with_autoload(false). Prevention: Keep the forc‑generated storage‑slots JSON alongside the .bin.

Error: forwarding coins to a non‑payable method (from call_params)

Cause: You set CallParameters with amount != 0 for a method not annotated #[payable] in Sway. Solution: Only forward coins to payable methods; for others use CallParameters::default() (amount 0). Prevention: Check the Sway ABI for the #[payable] annotation before forwarding.

Error: Error::Transaction(Reason::Failure { .. }) when a method mints/calls another contract

Cause: Missing variable outputs and/or external‑contract inputs. Solution: Add with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) and determine_missing_contracts(), or specify them manually with with_contract_ids. Prevention: Whenever a method transfers funds or calls another contract, set the appropriate dependencies.

Error: "… was submitted recently in a transaction — attempting to spend it again will result in an error"

Cause: The coin-cache feature is preventing you from re‑spending a coin that's still in flight from a recent transaction. Solution: Wait for the previous transaction to commit, or fetch fresh spendable resources before building the next transaction. Prevention: Don't reuse the same UTXOs across rapid back‑to‑back transactions; let prior transactions settle.

Error: tests fail unexpectedly on master

Cause: Stale build artifacts or a stale Cargo.lock. Solution:

cargo clean
rm Cargo.lock
forc build --release --path e2e
cargo test

Prevention: Rebuild the Sway test projects after pulling changes.

Error: schema / consensus‑parameter mismatch against a node

Cause: Your fuel-core version differs from the SDK's supported version (SUPPORTED_FUEL_CORE_VERSION). Solution: Align fuel-core with the SDK's pinned version (fuelup can pin toolchains). Prevention: Upgrade SDK and node together.

Error: abigen!() fails after renaming a Sway import

Cause: A known issue (#1703) — renaming Sway imports can break abigen!. Solution: Avoid renaming the imports involved, or regenerate the ABI after the rename. Prevention: Keep import names stable across ABI regeneration.


21. FAQ

Q: Why is the crate called fuels and not fuel? The SDK family is modelled on The Ethers Project, so it carries a trailing s to feel familiar to ethers.js users.

Q: What's the difference between a contract, a script, and a predicate? A contract has persistent storage and lives at a ContractId. A script is a one‑shot executable run inside a transaction (no storage). A predicate is a pure program that guards an address and can own assets.

Q: Do I need a running fuel-core to write tests? You need it available either as a binary on PATH or via the fuel-core-lib feature. The test helpers launch and tear it down for you.

Q: What is the base asset? The asset with AssetId 0x000…0 (AssetId::zeroed()). It is the only asset that pays fees and gas.

Q: How do I call a contract without redeploying it? Generate bindings with abigen!, then MyContract::new(contract_id, wallet) with the known ID.

Q: When do I need variable outputs? Whenever a contract method transfers funds to an address during execution (e.g. minting). Use with_variable_output_policy.

Q: How can I simulate a call without spending gas or changing state? Use .simulate(Execution::realistic()) (validates as if real) or .simulate(Execution::state_read_only()) (read state with an unfunded account).

Q: How do I forward coins with a call? Use CallParameters via call_params(...). The target Sway method must be #[payable] to receive a non‑zero amount.

Q: Can I bundle several contract calls into one transaction? Yes — CallHandler::new_multi_call(wallet).add_call(...).add_call(...). Provide explicit type annotations to decode the bundled return values.

Q: How do I deploy a contract that's too big for one transaction? Convert it to a loader (convert_to_loader) or use smart_deploy, which uploads the code as blobs and deploys a small loader.

Q: What's the difference between TxPolicies and CallParameters? TxPolicies are transaction‑level (tip, gas limit, maturity, expiration, max fee). CallParameters are call‑level (amount, asset, forwarded gas) for what the contract call receives.

Q: Why are my test wallet addresses always the same? Wallets from launch_* helpers are deterministic by design, for reproducible tests.

Q: How do I store keys safely? Use the Keystore (feature accounts-keystore) to encrypt with a master password, or sign via AWS/Google KMS so the key never enters your process.

Q: Does the SDK work with no_std / WASM? Parts do. Disable default features for no_std, and use #[NoStd] on derives. The node client and test helpers require std. WASM tests live in packages/wasm-tests.

Q: Where is the authoritative API reference? docs.rs/fuels — always synced with the released code.

Q: How do I read a wallet's balance for a single asset? wallet.get_asset_balance(&asset_id).await? returns a u128 (the sum of that asset's spendable coins).

Q: My contract call needs another contract's logs decoded — what do I use? with_contracts(&[&other_instance]) (instances enable log/require‑revert decoding). with_contract_ids does not decode logs.

Q: Can I run a contract call at a past block height? Yes, for simulate, with Execution::…​.at_height(h), if the node uses rocksdb and was started with --historical-execution.


22. Glossary

Contract — An abstraction representing a connection to a deployed smart contract. Usable as a regular Rust object whose methods mirror the on‑chain ABI methods.

Provider — A struct providing read‑only access to a Fuel node. Used directly or through a wallet.

Signer — Anything implementing the Signer trait that can produce a signature (PrivateKeySigner, KMS signers, FakeSigner).

Account — The Account/ViewOnlyAccount traits, implemented by Wallet and Predicate, providing balances and transfers.

Coin / UTXO — An unspent transaction output holding an amount of one AssetId. Fuel's balance model is the sum of a holder's unspent coins per asset.

Base asset — The fee‑paying asset, AssetId::zeroed() (0x000…0).

Configurable — A Sway constant that can be overridden at deploy/run time via a generated with_<NAME> method.

Predicate data — The arguments supplied to a predicate's main function when spending its funds.

Loader / Blob — A blob is a chunk of uploaded code; a loader is a small program that loads blobs at runtime, used to deploy/run code larger than one transaction allows.


23. Quick Reference

Prelude essentials

Symbol What it gives you
Provider Connection to a node
Wallet, PrivateKeySigner, FakeSigner Accounts and signers
abigen!, setup_program_test! Code generation & test setup
Contract, LoadConfiguration, StorageConfiguration Deployment
CallHandler, CallParameters, Execution Calling
TxPolicies, VariableOutputPolicy Transaction configuration
AssetId, Address, ContractId, Bytes, Salt Core types
Result, Error Error handling
launch_provider_and_get_wallet, launch_custom_provider_and_get_wallets, setup_test_provider Test node launching

Common chains

Goal Chain
Basic call contract.methods().m(args).call().await?
Forward coins .call_params(CallParameters::default().with_amount(n))?
Set gas/tip .with_tx_policies(TxPolicies::default().with_script_gas_limit(n))
Variable outputs .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
External contract .with_contracts(&[&other]) / .with_contract_ids(&[id])
Simulate .simulate(Execution::realistic())
Estimate cost .estimate_transaction_cost(tolerance, horizon).await?
Different wallet contract.with_account(w).methods()…

Key default constants

Constant Value
DEFAULT_NUM_WALLETS 10
DEFAULT_NUM_COINS 1
DEFAULT_COIN_AMOUNT 1_000_000_000
DEFAULT_DERIVATION_PATH m/44'/1179993420'/0'/0/0
Base asset AssetId 0x000…0 (AssetId::zeroed())

For the complete, version‑synced API, see docs.rs/fuels and the fuels‑rs book.