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
sinfuelsis intentional. The SDK family (fuels-rs,fuels-ts, …) is modelled on The Ethers Project, so the crate isfuels, notfuel.
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, andResultall come from the prelude. Importingfuels::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-corebinary on yourPATHwhen launching test nodes (unless you enable thefuel-core-libfeature — see §2.3). Make surefuel-coreis installed and discoverable, or test setup will fail withno '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-libwhen you cannot guarantee afuel-corebinary on the machine (e.g. CI withoutfuelup). 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
--forcepreserves the casing of your--namefor the{{project-name}}placeholder; without it,cargo-generatelowercases 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:
- Connect to a real node (testnet, mainnet, or your own
fuel-core) — for building applications. - 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_walletandlaunch_custom_provider_and_get_walletshave 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-corebinary present, or both thefuel-core-libandrocksdbfeatures 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 ViewOnlyAccountfor 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 type1179993420'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
SecretKeyis 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
FakeSigneronly 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_contracttransfers 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, usetx.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=…):
ProgramTypeisContract,Script, orPredicate.nameis the Rust name for the generated binding.abiis 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 callingabigen!multiple times in one namespace. If two unrelated types share a name, only uniquely‑named types get apub use; qualify the rest asabigen_bindings::some_contract_mod::TheType.
Note
Everything
abigen!emits is generated code — you never write it. Predicate names getEncoderandConfigurablessuffixes appended automatically:name = "MyPredicate"yieldsMyPredicateEncoderandMyPredicateConfigurables.
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
Saltso the same binary yields a newcontract_id(enables redeploying the same code). - Override storage slots.
- Set
configurableconstants.
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_idand fails with a collision. Usewith_salt(...)(orLoadConfiguration::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.jsonnext to the binary,load_fromfails 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 aResultyou 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>,
// ...
}
valueis the method's return value, decoded to the matching Rust type (a Swayu64→u64, a Sway struct → a generated Rust struct,b256→[u8; 32], …).receiptsholds every receipt the call produced.gas_usedis the gas the call consumed.tx_idis 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_paramsreturns aResultso 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 defaultCallParametersforwards0of 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_addresscallstransfer_to_addressunder 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 needwith_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
TxPoliciesare 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-coreinstances 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 defaultEncoderConfig.
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(addadd_signerfor signed coins). - Logs —
decode_logs()anddecode_logs_with_type::<T>()(see §10). - Calling contracts —
with_contracts(&[…])/with_contract_ids(&[…]). - Configurable constants — each
configurablegets awith_<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 astr[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
AssetIdis 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
Tokenizableon enums requires every variant to also implementParameterize. Iffuelsisn't reachable as::fuels, point the derive at the right paths with#[FuelsCorePath = "…"]/#[FuelsTypesPath = "…"]. Forno_stdoutput, 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
DecoderConfigandEncoderConfigare 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_coinsunder ~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
Abigenmust 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.