The Fuel Rust SDK

Rust SDK for Fuel. It can be used for a variety of things, including but not limited to:

  • Compiling, deploying, and testing Sway contracts;
  • Use the Testnet or run a local Fuel node;
  • Crafting and signing transactions with hand-crafted scripts or contract calls;
  • Generating type-safe Rust bindings of contract ABI methods;
  • And more. fuels-rs is still in active development.

This book is an overview of the different things one can achieve using the Rust SDK, and how to implement them. Keep in mind that both the SDK and the documentation are works-in-progress!

Getting Started

This section gives some pointers for using the Fuel SDK for smart contract development.

Setting up and running the Fuel Rust SDK

Dependencies

forc is Sway equivalent of Rust's cargo. fuel-core is a Fuel full node implementation.

There are two main ways you can use the Fuel Rust SDK:

  1. Creating a new Sway project with forc and running the tests
  2. Creating a standalone project and importing the fuels-rs crate

Creating a new project with Forc

You can create a new Sway project with

forc new <Project name>

Or you can initialize a project within an existing folder with

forc init

Adding a Rust integration test to the Sway project

Now that we have a new project, we can add a Rust integration test using a cargo generate template. If cargo generate is not already installed, you can instal it with:

cargo install cargo-generate

Note You can learn more about cargo generate by visiting its repository.

Let's generate the default test harness with the following command:

cargo generate --init fuellabs/sway templates/sway-test-rs --name <Project name> --force

--force forces your --name input to retain your desired casing for the {{project-name}} placeholder in the template. Otherwise, cargo-generate automatically converts it to kebab-case. With --force, this means that both my_fuel_project and my-fuel-project are valid project names, depending on your needs.

Before running test, we need to build the Sway project with:

forc build

Afterwards, we can run the test with:

cargo test

Note If you need to capture output from the tests, use one of the following commands:

cargo test -- --nocapture

Importing the Fuel Rust SDK

Add these dependencies on your Cargo.toml:

fuels = "0.32"

Note We're using version 0.32 of the SDK, which is the latest version at the time of this writing.

And then, in your Rust file that's going to make use of the SDK:

use fuels::prelude::*;

The Fuel Rust SDK source code

Another way to experience the SDK is to look at the source code. The packages/fuels/tests/ folder is full of integration tests that go through almost all aspects of the SDK.

Note Before running the tests, we need to build all the Sway test projects. The SDK has a binary that will go through all projects and build them for us. You can use it with the following command.

cargo run --bin test-projects -- build

The test-projects binary can also be used to clean and format the test projects. Check the help output for more info.

After building the projects, we can run the tests with

cargo test

If you need all targets and all features, you can run

cargo test --all-targets --all-features

Note If you need to capture output from the tests, you can run

cargo test -- --nocapture

More in-depth Fuel and Sway knowledge

Read The Sway Book for more in-depth knowledge about Sway, the official smart contract language for the Fuel Virtual Machine.

Terminology

These are the common terms you will find across this documentation and while using the SDK.

Contract

A contract, in the SDK, is an abstraction that represents a connection to a specific smart contract deployed on the Fuel Network. This contract instance can be used as a regular Rust object, with methods attached to it that reflect those in its smart contract equivalent.

Provider

A Provider is a struct that provides an abstraction for a connection to a Fuel node. It provides read-only access to the node. You can use this provider as-is or through the wallet.

Wallet and signer

A Wallet is a struct with direct or indirect access to a private key. You can use a Wallet to sign messages and transactions to authorize the network to charge your account to perform operations. The terms wallet and signer in the SDK are often used interchangeably, but, technically, a Signer is simply a Rust trait to enable the signing of transactions and messages; the Wallet implements the Signer trait.

Connecting to a Fuel node

At a high level, you can use the Fuel Rust SDK to build Rust-based applications that can run computations on the Fuel Virtual Machine through interactions with smart contracts written in Sway.

For this interaction to work, the SDK must be able to communicate with a fuel-core node; you have two options at your disposal:

  1. Use the Testnet or run a Fuel node (using fuel-core) and instantiate a provider that points to that node's IP and port.
  2. Use the SDK's native launch_provider_and_get_wallet() that runs a short-lived test Fuel node;

The second option is ideal for smart contract testing, as you can quickly spin up and tear down nodes between specific test cases.

For application building, you should use the first option.

Connecting to the Testnet or an external node

We can interact with the Testnet node by using the following example.

        use fuels::prelude::*;
        use fuels::signers::fuel_crypto::SecretKey;
        use std::str::FromStr;

        // Create a provider pointing to the testnet.
        let provider = Provider::connect("node-beta-1.fuel.network").await.unwrap();

        // Setup a private key
        let secret =
            SecretKey::from_str("a1447cd75accc6b71a976fd3401a1f6ce318d27ba660b0315ee6ac347bf39568")
                .unwrap();

        // Create the wallet
        let wallet = WalletUnlocked::new_from_private_key(secret, Some(provider));

        // Get the wallet address. Used later with the faucet
        dbg!(wallet.address().to_string());

In the code example, we connected a new provider to the Testnet node and created a new wallet from a private key.

Note: New wallets on the Testnet will not have any assets! They can be obtained by providing the wallet address to the faucet at

faucet-beta-1.fuel.network

Once the assets have been transferred to the wallet, you can reuse it in other tests by providing the private key!

In addition to the faucet, there is a block explorer for the Tesnet at

block-explorer

If you want to connect to another node just change the url or IP and port. For example, to connect to a local node that was created with fuel-core you can use:

        let _provider = Provider::connect("127.0.0.1:4000").await.unwrap();

Running a short-lived Fuel node with the SDK

You can use the SDK to spin up a local, ideally short-lived Fuel node. Then, you can instantiate a Fuel client, pointing to this node.

        use fuels::client::FuelClient;
        use fuels::fuel_node::{Config, FuelService};

        // Run the fuel node.
        let server = FuelService::new_node(Config::local_node()).await?;

        // Create a client that will talk to the node created above.
        let client = FuelClient::from(server.bound_address);
        assert!(client.health().await?);

This approach is ideal for contract testing.

You can also use the test helper setup_test_provider() for this:

        use fuels::prelude::*;

        // Use the test helper to setup a test provider.
        let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await;

        // Create the wallet.
        let _wallet = WalletUnlocked::new_random(Some(provider));

You can also use launch_provider_and_get_wallet(), which abstracts away the setup_test_provider() and the wallet creation, all in one single method:

let wallet = launch_provider_and_get_wallet().await;

Features

Fuel-core lib

The fuel-core-lib is a feature defined in the fuels library, allowing us to run a fuel-core node without installing the fuel-core binary on the local machine. Using the fuel-core-lib feature flag entails downloading all the dependencies needed to run the fuel-core node.

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

Interacting with the blockchain

Once you have set up a provider, you're ready to interact with the Fuel blockchain. Here are a few examples of what you can do with a provider; for a more in-depth overview of the API, check the official provider API documentation.

Set up

You might need to set up a test blockchain first. You can skip this step if you're connecting to an external blockchain.

        use fuels::prelude::*;

        // Set up our test blockchain.

        // Create a random wallet (more on wallets later).
        let wallet = WalletUnlocked::new_random(None);

        // How many coins in our wallet.
        let number_of_coins = 1;

        // The amount/value in each coin in our wallet.
        let amount_per_coin = 3;

        let coins = setup_single_asset_coins(
            wallet.address(),
            BASE_ASSET_ID,
            number_of_coins,
            amount_per_coin,
        );

        let (provider, _) = setup_test_provider(coins.clone(), vec![], None, None).await;

Get all coins from an address

This method returns all coins (of a given asset ID) from a wallet, including spent ones.

        let coins = provider.get_coins(wallet.address(), BASE_ASSET_ID).await?;
        assert_eq!(coins.len(), 1);

Get spendable resources from an address

The last argument says how much you want to spend. This method returns only spendable, i.e., unspent coins (of a given asset ID) or messages. If you ask for more spendable resources than the amount of resources you have, it returns an error.

        let spendable_resources = provider
            .get_spendable_resources(wallet.address(), BASE_ASSET_ID, 1)
            .await?;
        assert_eq!(spendable_resources.len(), 1);

Get balances from an address

Get all the spendable balances of all assets for an address. This is different from getting the coins because we only return the numbers (the sum of UTXOs coins amount for each asset id) and not the UTXOs coins themselves.

        let _balances = provider.get_balances(wallet.address()).await?;

Managing wallets

You can use wallets for many important things, for instance:

  1. Checking your balance;
  2. Transferring coins to a destination address or contract;
  3. Signing messages and transactions;
  4. Paying for network fees when sending transactions or deploying smart contracts.

The SDK gives you many different ways to create and access wallets. Let's explore these different approaches in the following sub-chapters.

Note: Keep in mind that you should never share your private/secret key. And in the case of wallets that were derived from a mnemonic phrase, never share your mnemonic phrase. If you're planning on storing the wallet on disk, do not store the plain private/secret key and do not store the plain mnemonic phrase. Instead, use Wallet::encrypt to encrypt its content first before saving it to disk.

Creating a wallet from a private key

A new wallet with a randomly generated private key can be created by supplying Option<Provider>.

        use fuels::prelude::*;

        // Use the test helper to setup a test provider.
        let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await;

        // Create the wallet.
        let _wallet = WalletUnlocked::new_random(Some(provider));

Alternatively, you can create a wallet from a predefined SecretKey.

        use fuels::prelude::*;
        use fuels::signers::fuel_crypto::SecretKey;
        use std::str::FromStr;

        // Use the test helper to setup a test provider.
        let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await;

        // Setup the private key.
        let secret = SecretKey::from_str(
            "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
        )?;

        // Create the wallet.
        let _wallet = WalletUnlocked::new_from_private_key(secret, Some(provider));

Note: if None is supplied instead of a provider, any transaction related to the wallet will result in an error until a provider is linked with set_provider(). The optional parameter enables defining owners (wallet addresses) of genesis coins before a provider is launched.

Creating a wallet from mnemonic phrases

A mnemonic phrase is a cryptographically-generated sequence of words that's used to derive a private key. For instance: "oblige salon price punch saddle immune slogan rare snap desert retire surprise"; would generate the address 0xdf9d0e6c6c5f5da6e82e5e1a77974af6642bdb450a10c43f0c6910a212600185.

In addition to that, we also support Hierarchical Deterministic Wallets and derivation paths. You may recognize the string "m/44'/60'/0'/0/0" from somewhere; that's a derivation path. In simple terms, it's a way to derive many wallets from a single root wallet.

The SDK gives you two wallets from mnemonic instantiation methods: one that takes a derivation path (Wallet::new_from_mnemonic_phrase_with_path) and one that uses the default derivation path, in case you don't want or don't need to configure that (Wallet::new_from_mnemonic_phrase).

Here's how you can create wallets with both mnemonic phrases and derivation paths:

        use fuels::prelude::*;

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

        // Use the test helper to setup a test provider.
        let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await;

        // Create first account from mnemonic phrase.
        let _wallet = WalletUnlocked::new_from_mnemonic_phrase_with_path(
            phrase,
            Some(provider.clone()),
            "m/44'/1179993420'/0'/0/0",
        )?;

        // Or with the default derivation path
        let wallet = WalletUnlocked::new_from_mnemonic_phrase(phrase, Some(provider))?;

        let expected_address = "fuel17x9kg3k7hqf42396vqenukm4yf59e5k0vj4yunr4mae9zjv9pdjszy098t";

        assert_eq!(wallet.address().to_string(), expected_address);

Wallet Access

The kinds of operations we can perform with a Wallet instance depend on whether or not we have access to the wallet's private key.

In order to differentiate between Wallet instances that know their private key and those that do not, we use the WalletUnlocked and Wallet types respectively.

Wallet States

The WalletUnlocked type represents a wallet whose private key is known and stored internally in memory. A wallet must be of type WalletUnlocked in order to perform operations that involve signing messages or transactions.

The Wallet type represents a wallet whose private key is not known or stored in memory. Instead, Wallet only knows its public address. A Wallet cannot be used to sign transactions, however it may still perform a whole suite of useful operations including listing transactions, assets, querying balances, and so on.

Note that the WalletUnlocked type provides a Deref implementation targeting its inner Wallet type. This means that all methods available on the Wallet type are also available on the WalletUnlocked type. In other words, WalletUnlocked can be thought of as a thin wrapper around Wallet that provides greater access via its private key.

Transitioning States

A Wallet instance can be unlocked by providing the private key:

let wallet_unlocked = wallet_locked.unlock(private_key);

A WalletUnlocked instance can be locked using the lock method:

let wallet_locked = wallet_unlocked.lock();

Most wallet constructors that create or generate a new wallet are provided on the WalletUnlocked type. Consider locking the wallet after the new private key has been handled in order to reduce the scope in which the wallet's private key is stored in memory.

Design Guidelines

When designing APIs that accept a wallet as an input, we should think carefully about the kind of access that we require. API developers should aim to minimise their usage of WalletUnlocked in order to ensure private keys are stored in memory no longer than necessary to reduce the surface area for attacks and vulnerabilities in downstream libraries and applications.

Encrypting and storing wallets

Creating a wallet and storing an encrypted JSON wallet on disk

You can also manage a wallet using JSON wallets that are securely encrypted and stored on the disk. This makes it easier to manage multiple wallets, especially for testing purposes.

You can create a random wallet and, at the same time, encrypt and store it. Then, later, you can recover the wallet if you know the master password:

        use fuels::prelude::*;

        let dir = std::env::temp_dir();
        let mut rng = rand::thread_rng();

        // Use the test helper to setup a test provider.
        let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await;

        let password = "my_master_password";

        // Create a wallet to be stored in the keystore.
        let (_wallet, uuid) =
            WalletUnlocked::new_from_keystore(&dir, &mut rng, password, Some(provider.clone()))?;

        let path = dir.join(uuid);

        let _recovered_wallet = WalletUnlocked::load_keystore(&path, password, Some(provider))?;

Encrypting and storing a wallet created from a mnemonic or private key

If you have already created a wallet using a mnemonic phrase or a private key, you can also encrypt it and save it to disk:

        use fuels::prelude::*;

        let dir = std::env::temp_dir();

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

        // Use the test helper to setup a test provider.
        let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await;

        // Create first account from mnemonic phrase.
        let wallet = WalletUnlocked::new_from_mnemonic_phrase(phrase, Some(provider))?;

        let password = "my_master_password";

        // Encrypts and stores it on disk. Can be recovered using `Wallet::load_keystore`.
        let _uuid = wallet.encrypt(&dir, password)?;

Checking balances and coins

First, one should remember that, with UTXOs, each coin is unique. Each UTXO corresponds to a unique coin, and said coin has a corresponding amount (the same way a dollar bill has either 10$ or 5$ face value). So, when you want to query the balance for a given asset ID, you want to query the sum of the amount in each unspent coin. This querying is done very easily with a wallet:

        let asset_id: AssetId = BASE_ASSET_ID;
        let balance: u64 = wallet.get_asset_balance(&asset_id).await?;

If you want to query all the balances (i.e., get the balance for each asset ID in that wallet), then it is as simple as:

        let balances: HashMap<String, u64> = wallet.get_balances().await?;

The return type is a HashMap, where the key is the asset ID's hex string, and the value is the corresponding balance. For example, we can get the base asset balance with:

        let asset_id_key = format!("{:#x}", asset_id);
        let asset_balance = balances.get(&asset_id_key).unwrap();

Setting up test wallets

You'll often want to create one or more test wallets when testing your contracts. Here's how to do it.

Setting up multiple test wallets

If you need multiple test wallets, they can be set up as follows:

        use fuels::prelude::*;
        // This helper will launch a local node and provide 10 test wallets linked to it.
        // The initial balance defaults to 1 coin per wallet with an amount of 1_000_000_000
        let wallets =
            launch_custom_provider_and_get_wallets(WalletsConfig::default(), None, None).await;

You can customize your test wallets via WalletsConfig.

        let num_wallets = 5;
        let coins_per_wallet = 3;
        let amount_per_coin = 100;

        let config = WalletsConfig::new(
            Some(num_wallets),
            Some(coins_per_wallet),
            Some(amount_per_coin),
        );
        // Launches a local node and provides test wallets as specified by the config
        let wallets = launch_custom_provider_and_get_wallets(config, None, None).await;

Note Wallets generated with launch_provider_and_get_wallet or launch_custom_provider_and_get_wallets will have deterministic addresses.

Setting up a test wallet with multiple random assets

You can create a test wallet containing multiple assets (including the base asset to pay for gas).

        use fuels::prelude::*;
        let mut wallet = WalletUnlocked::new_random(None);
        let num_assets = 5; // 5 different assets
        let coins_per_asset = 10; // Per asset id, 10 coins in the wallet
        let amount_per_coin = 15; // For each coin (UTXO) of the asset, amount of 15

        let (coins, asset_ids) = setup_multiple_assets_coins(
            wallet.address(),
            num_assets,
            coins_per_asset,
            amount_per_coin,
        );
        let (provider, _socket_addr) = setup_test_provider(coins.clone(), vec![], None, None).await;
        wallet.set_provider(provider);
  • coins: Vec<(UtxoId, Coin)> has num_assets * coins_per_assets coins (UTXOs)
  • asset_ids: Vec<AssetId> contains the num_assets randomly generated AssetIds (always includes the base asset)

Setting up a test wallet with multiple custom assets

You can also create assets with specific AssetIds, coin amounts, and number of coins.

        use fuels::prelude::*;
        use rand::Fill;

        let mut wallet = WalletUnlocked::new_random(None);
        let mut rng = rand::thread_rng();

        let asset_base = AssetConfig {
            id: BASE_ASSET_ID,
            num_coins: 2,
            coin_amount: 4,
        };

        let mut asset_id_1 = AssetId::zeroed();
        asset_id_1.try_fill(&mut rng)?;
        let asset_1 = AssetConfig {
            id: asset_id_1,
            num_coins: 6,
            coin_amount: 8,
        };

        let mut asset_id_2 = AssetId::zeroed();
        asset_id_2.try_fill(&mut rng)?;
        let asset_2 = AssetConfig {
            id: asset_id_2,
            num_coins: 10,
            coin_amount: 12,
        };

        let assets = vec![asset_base, asset_1, asset_2];

        let coins = setup_custom_assets_coins(wallet.address(), &assets);
        let (provider, _socket_addr) = setup_test_provider(coins, vec![], None, None).await;
        wallet.set_provider(provider);

This can also be achieved directly with the WalletsConfig.

        let num_wallets = 1;
        let wallet_config = WalletsConfig::new_multiple_assets(num_wallets, assets);
        let wallets = launch_custom_provider_and_get_wallets(wallet_config, None, None).await;

Note In this case, you need to manually add the base asset and the corresponding number of coins and coin amount

Setting up assets

The Fuel blockchain holds many different assets; you can create your asset with its unique AssetId or create random assets for testing purposes.

You can use only one asset to pay for transaction fees and gas: the base asset, whose AssetId is 0x000...0, a 32-byte zeroed value.

For testing purposes, you can configure coins and amounts for assets. You can use setup_multiple_assets_coins:

        use fuels::prelude::*;
        let mut wallet = WalletUnlocked::new_random(None);
        let num_assets = 5; // 5 different assets
        let coins_per_asset = 10; // Per asset id, 10 coins in the wallet
        let amount_per_coin = 15; // For each coin (UTXO) of the asset, amount of 15

        let (coins, asset_ids) = setup_multiple_assets_coins(
            wallet.address(),
            num_assets,
            coins_per_asset,
            amount_per_coin,
        );

Note If setting up multiple assets, one of these assets will always be the base asset.

If you want to create coins only with the base asset, then you can use:

        let wallet = WalletUnlocked::new_random(None);

        // How many coins in our wallet.
        let number_of_coins = 1;

        // The amount/value in each coin in our wallet.
        let amount_per_coin = 3;

        let coins = setup_single_asset_coins(
            wallet.address(),
            BASE_ASSET_ID,
            number_of_coins,
            amount_per_coin,
        );

Note Choosing a large number of coins and assets for setup_multiple_assets_coins or setup_single_asset_coins can lead to considerable runtime for these methods. This will be improved in the future but for now, we recommend using up to 1_000_000 coins, or 1000 coins and assets simultaneously.

Signing

Once you've instantiated your wallet in an unlocked state using one of the previously discussed methods, you can sign a message with wallet.sign_message. Below is a full example of how to sign and recover a message.

        let mut rng = StdRng::seed_from_u64(2322u64);
        let mut secret_seed = [0u8; 32];
        rng.fill_bytes(&mut secret_seed);

        let secret = unsafe { SecretKey::from_bytes_unchecked(secret_seed) };

        // Create a wallet using the private key created above.
        let wallet = WalletUnlocked::new_from_private_key(secret, None);

        let message = "my message";

        let signature = wallet.sign_message(message).await?;

        // Check if signature is what we expect it to be
        assert_eq!(signature, Signature::from_str("0x8eeb238db1adea4152644f1cd827b552dfa9ab3f4939718bb45ca476d167c6512a656f4d4c7356bfb9561b14448c230c6e7e4bd781df5ee9e5999faa6495163d")?);

        // Recover address that signed the message
        let message = Message::new(message);
        let recovered_address = signature.recover(&message)?;

        assert_eq!(wallet.address().hash(), recovered_address.hash());

        // Verify signature
        signature.verify(&recovered_address, &message)?;
        Ok(())

You can also sign a transaction by using wallet.sign_transaction. Below is a full example of how to sign and recover a transaction.

        let secret = SecretKey::from_str(
            "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
        )?;
        let wallet = WalletUnlocked::new_from_private_key(secret, None);

        // Set up a dummy transaction.
        let input_coin = Input::coin_signed(
            UtxoId::new(Bytes32::zeroed(), 0),
            Address::from_str(
                "0xf1e92c42b90934aa6372e30bc568a326f6e66a1a0288595e6e3fbd392a4f3e6e",
            )?,
            10000000,
            AssetId::from([0u8; 32]),
            TxPointer::default(),
            0,
            0,
        );

        let output_coin = Output::coin(
            Address::from_str(
                "0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077",
            )?,
            1,
            AssetId::from([0u8; 32]),
        );

        let mut tx = Transaction::script(
            0,
            1000000,
            0,
            hex::decode("24400000")?,
            vec![],
            vec![input_coin],
            vec![output_coin],
            vec![],
        );

        // Sign the transaction.
        let signature = wallet.sign_transaction(&mut tx).await?;
        let message = unsafe { Message::from_bytes_unchecked(*tx.id()) };

        // Check if signature is what we expect it to be
        assert_eq!(signature, Signature::from_str("34482a581d1fe01ba84900581f5321a8b7d4ec65c3e7ca0de318ff8fcf45eb2c793c4b99e96400673e24b81b7aa47f042cad658f05a84e2f96f365eb0ce5a511")?);

        // Recover address that signed the transaction
        let recovered_address = signature.recover(&message)?;

        assert_eq!(wallet.address().hash(), recovered_address.hash());

        // Verify signature
        signature.verify(&recovered_address, &message)?;
        Ok(())

Transferring assets

With wallet.transfer you can initiate a transaction to transfer an asset from your wallet to a target address.

        use fuels::prelude::*;

        // Setup 2 test wallets with 1 coin each
        let num_wallets = Some(2);
        let coins_per_wallet = Some(1);
        let coin_amount = Some(1);

        let wallets = launch_custom_provider_and_get_wallets(
            WalletsConfig::new(num_wallets, coins_per_wallet, coin_amount),
            None,
            None,
        )
        .await;

        // Transfer the base asset with amount 1 from wallet 1 to wallet 2
        let asset_id = Default::default();
        let (_tx_id, _receipts) = wallets[0]
            .transfer(wallets[1].address(), 1, asset_id, TxParameters::default())
            .await?;

        let wallet_2_final_coins = wallets[1].get_coins(BASE_ASSET_ID).await?;

        // Check that wallet 2 now has 2 coins
        assert_eq!(wallet_2_final_coins.len(), 2);

You can transfer assets to a contract via wallet.force_transfer_to_contract.

        // Check the current balance of the contract with id 'contract_id'
        let contract_balances = wallet
            .get_provider()?
            .get_contract_balances(&contract_id)
            .await?;
        assert!(contract_balances.is_empty());

        // Transfer an amount of 300 to the contract
        let amount = 300;
        let asset_id = random_asset_id;
        let (_tx_id, _receipts) = wallet
            .force_transfer_to_contract(&contract_id, amount, asset_id, TxParameters::default())
            .await?;

        // Check that the contract now has 1 coin
        let contract_balances = wallet
            .get_provider()?
            .get_contract_balances(&contract_id)
            .await?;
        assert_eq!(contract_balances.len(), 1);

        let random_asset_id_key = format!("{:#x}", random_asset_id);
        let random_asset_balance = contract_balances.get(&random_asset_id_key).unwrap();
        assert_eq!(*random_asset_balance, 300);

For transferring assets to the base layer chain, you can use wallet.withdraw_to_base_layer.

        use fuels::prelude::*;
        use std::str::FromStr;

        let wallet = launch_provider_and_get_wallet().await;

        let amount = 1000;
        let base_layer_address =
            Address::from_str("0x4710162c2e3a95a6faff05139150017c9e38e5e280432d546fae345d6ce6d8fe")
                .expect("Invalid address.");
        let base_layer_address = Bech32Address::from(base_layer_address);
        // Transfer an amount of 1000 to the specified base layer address
        let (tx_id, msg_id, _receipts) = wallet
            .withdraw_to_base_layer(&base_layer_address, amount, TxParameters::default())
            .await?;

        // Retrieve a message proof from the provider
        let proof = wallet
            .get_provider()?
            .get_message_proof(&tx_id, &msg_id)
            .await?
            .expect("Failed to retrieve message proof.");

        // Verify the amount and recipient
        assert_eq!(proof.amount, amount);
        assert_eq!(proof.recipient, base_layer_address);

The above example creates an Address from a string and converts it to a Bech32Address. Next, it calls wallet.withdraw_to_base_layer by providing the address, the amount to be transferred, and the transaction parameters. Lastly, to verify that the transfer succeeded, the relevant message proof is retrieved with provider.get_message_proof, and the amount and the recipient is verified.

Deploying contracts

There are two main ways of working with contracts in the SDK: deploying a contract with SDK or using the SDK to interact with existing contracts.

Deploying a contract binary

Once you've written a contract in Sway and compiled it with forc build (read here for more on how to work with Sway), you'll have in your hands two important artifacts: the compiled binary file and the JSON ABI file.

Below is how you can deploy your contracts using the SDK. For more details about each component in this process, read The abigen macro, The FuelVM binary file, and The JSON ABI file.

The deploy functions

There are two intended ways to deploy a contract

  • deploy
  • deploy_with_parameters

If you are only interested in a single instance of your contract, then use deploy

        // This will generate your contract's methods onto `MyContract`.
        // This means an instance of `MyContract` will have access to all
        // your contract's methods that are running on-chain!
        abigen!(
            MyContract,
            // This path is relative to the workspace (repository) root
            "packages/fuels/tests/contracts/contract_test/out/debug/contract_test-abi.json"
        );

        // This helper will launch a local node and provide a test wallet linked to it
        let wallet = launch_provider_and_get_wallet().await;

        // Optional: Configure deployment parameters or use `TxParameters::default()`
        let gas_price = 0;
        let gas_limit = 1_000_000;
        let maturity = 0;

        // This will deploy your contract binary onto the chain so that its ID can
        // be used to initialize the instance
        let contract_id = Contract::deploy(
            // This path is relative to the current crate (examples/contracts)
            "../../packages/fuels/tests/contracts/contract_test/out/debug/contract_test.bin",
            &wallet,
            TxParameters::new(Some(gas_price), Some(gas_limit), Some(maturity)),
            StorageConfiguration::default(),
        )
        .await?;

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

You can then use the contract methods very simply:

        // This is an instance of your contract which you can use to make calls to your functions
        let contract_instance = MyContract::new(contract_id, wallet);

        let response = contract_instance
            .methods()
            .initialize_counter(42) // Build the ABI call
            .call() // Perform the network call
            .await?;

        assert_eq!(42, response.value);

        let response = contract_instance
            .methods()
            .increment_counter(10)
            .call()
            .await?;

        assert_eq!(52, response.value);

Alternatively, if you want multiple instances of the same contract then use deploy_with_parameters and set the salt parameter.

        use fuels::prelude::*;
        use rand::prelude::{Rng, SeedableRng, StdRng};

        abigen!(
            MyContract,
            "packages/fuels/tests/contracts/contract_test/out/debug/contract_test-abi.json"
        );

        let wallet = launch_provider_and_get_wallet().await;

        let contract_id_1 = Contract::deploy(
            "../../packages/fuels/tests/contracts/contract_test/out/debug/contract_test.bin",
            &wallet,
            TxParameters::default(),
            StorageConfiguration::default(),
        )
        .await?;

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

        let rng = &mut StdRng::seed_from_u64(2322u64);
        let salt: [u8; 32] = rng.gen();

        let contract_id_2 = Contract::deploy_with_parameters(
            "../../packages/fuels/tests/contracts/contract_test/out/debug/contract_test.bin",
            &wallet,
            TxParameters::default(),
            StorageConfiguration::default(),
            Salt::from(salt),
        )
        .await?;

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

        assert_ne!(contract_id_1, contract_id_2);

Interacting with contracts

If you already have a deployed contract and want to call its methods using the SDK, but without deploying it again, all you need is the contract ID of your deployed contract. You can skip the whole deployment setup and call ::new(contract_id, wallet) directly. For example:

        // Replace with your contract ABI.json path
        abigen!(
            MyContract,
            "packages/fuels/tests/contracts/contract_test/out/debug/contract_test-abi.json"
        );
        let wallet_original = launch_provider_and_get_wallet().await;

        let wallet = wallet_original.clone();
        // Your bech32m encoded contract ID.
        let contract_id: Bech32ContractId =
            "fuel1vkm285ypjesypw7vhdlhnty3kjxxx4efckdycqh3ttna4xvmxtfs6murwy"
                .parse()
                .expect("Invalid ID");

        let connected_contract_instance = MyContract::new(contract_id, wallet);
        // You can now use the `connected_contract_instance` just as you did above!

The above example assumes that your contract id string is encoded in the bech32m format. You can recognize it by the human-readable-part "fuel" followed by the separator "1". However, when using other Fuel tools, you might end up with a hex-encoded contract id string. In that case, you can create your contract instance as follows:

        let contract_id: ContractId =
            "0x65b6a3d081966040bbccbb7f79ac91b48c635729c59a4c02f15ae7da999b32d3"
                .parse()
                .expect("Invalid ID");
        let connected_contract_instance = MyContract::new(contract_id.into(), wallet);

You can learn more about the Fuel SDK bech32 types here.

The FuelVM binary file

The command forc build compiles your Sway code and generates the bytecode: the binary code that the Fuel Virtual Machine will interpret. For instance, the smart contract below:

contract;

abi MyContract {
    fn test_function() -> bool;
}

impl MyContract for Contract {
    fn test_function() -> bool {
        true
    }
}

After forc build, will have a binary file that contains:

$ cat out/debug/my-test.bin
G4]�]D`I]C�As@
           6]C�$@!QK%

This seems very unreadable! But, forc has a nice interpreter for this bytecode: forc parse-bytecode`, which will interpret that binary data and output the equivalent FuelVM assembly:

$ forc parse-bytecode out/debug/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
        2   8      Undefined         00 00 00 00   data section offset lo (0)
        3   12     Undefined         00 00 00 34   data section offset hi (52)
        4   16     LW(63, 12, 1)     5d fc c0 01
        5   20     ADD(63, 63, 12)   10 ff f3 00
        6   24     LW(17, 6, 73)     5d 44 60 49
        7   28     LW(16, 63, 1)     5d 43 f0 01
        8   32     EQ(16, 17, 16)    13 41 14 00
        9   36     JNZI(16, 11)      73 40 00 0b   conditionally jump to byte 44
       10   40     RVRT(0)           36 00 00 00
       11   44     LW(16, 63, 0)     5d 43 f0 00
       12   48     RET(16)           24 40 00 00
       13   52     Undefined         00 00 00 00
       14   56     Undefined         00 00 00 01
       15   60     Undefined         00 00 00 00
       16   64     XOR(20, 27, 53)   21 51 bd 4b

If you want to deploy your smart contract using the SDK, this binary file is important; it's what we'll be sending to the FuelVM in a transaction.

The JSON ABI file

Whether you want to deploy or connect to a pre-existing smart contract, the JSON ABI file is extremely important: it's what tells the SDK about the ABI methods in your smart contracts.

For the same example Sway code as above:

contract;

abi MyContract {
    fn test_function() -> bool;
}

impl MyContract for Contract {
    fn test_function() -> bool {
        true
    }
}

The JSON ABI file looks like this:

$ cat out/debug/my-test-abi.json
[
  {
    "type": "function",
    "inputs": [],
    "name": "test_function",
    "outputs": [
      {
        "name": "",
        "type": "bool",
        "components": null
      }
    ]
  }
]

The Fuel Rust SDK will take this file as input and generate equivalent methods (and custom types if applicable) that you can call from your Rust code.

The abigen! macro

You might have noticed this section in the previous example:

        abigen!(
            MyContract,
            "packages/fuels/tests/contracts/contract_test/out/debug/contract_test-abi.json"
        );

The SDK lets you transform ABI methods of a smart contract, specified as JSON objects (which you can get from Forc), into Rust structs and methods that are type-checked at compile time.

For instance, a contract with two methods: initialize_counter(arg: u64) -> u64 and increment_counter(arg: u64) -> u64, with the following JSON ABI:

{
  "types": [
    {
      "typeId": 0,
      "type": "u64",
      "components": null,
      "typeParameters": null
    }
  ],
  "functions": [
    {
      "inputs": [
        {
          "name": "value",
          "type": 0,
          "typeArguments": null
        }
      ],
      "name": "initialize_counter",
      "output": {
        "name": "",
        "type": 0,
        "typeArguments": null
      }
    },
    {
      "inputs": [
        {
          "name": "value",
          "type": 0,
          "typeArguments": null
        }
      ],
      "name": "increment_counter",
      "output": {
        "name": "",
        "type": 0,
        "typeArguments": null
      }
    }
  ]
}

Can become this (shortened for brevity's sake):

pub struct MyContract {
    contract_id: ContractId,
    wallet: WalletUnlocked,
}
impl MyContract {
    pub fn new(contract_id: String, wallet: WalletUnlocked) -> Self {
        let contract_id = ContractId::from_str(&contract_id).expect("Invalid contract id");
        Self {
            contract_id,
            wallet,
        }
    }
    #[doc = "Calls the contract's `initialize_counter` (0x00000000ab64e5f2) function"]
    pub fn initialize_counter(&self, arg: u64) -> ContractCallHandler<u64> {
        Contract::method_hash(
            &self.wallet.get_provider().expect("Provider not set up"),
            self.contract_id,
            &self.wallet,
            [0, 0, 0, 0, 171, 100, 229, 242],
            &[ParamType::U64],
            &[arg.into_token()],
        )
        .expect("method not found (this should never happen)")
    }
    #[doc = "Calls the contract's `increment_counter` (0x00000000faf90dd3) function"]
    pub fn increment_counter(&self, arg: u64) -> ContractCallHandler<u64> {
        Contract::method_hash(
            &self.wallet.get_provider().expect("Provider not set up"),
            self.contract_id,
            &self.wallet,
            [0, 0, 0, 0, 250, 249, 13, 211],
            &[ParamType::U64],
            &[arg.into_token()],
        )
        .expect("method not found (this should never happen)")
    }
}

Note: that is all generated code. No need to write any of that. Ever. The generated code might look different from one version to another, this is just an example to give you an idea of what it looks like.

Then, you're able to use it to call the actual methods on the deployed contract:

        // This is an instance of your contract which you can use to make calls to your functions
        let contract_instance = MyContract::new(contract_id, wallet);

        let response = contract_instance
            .methods()
            .initialize_counter(42) // Build the ABI call
            .call() // Perform the network call
            .await?;

        assert_eq!(42, response.value);

        let response = contract_instance
            .methods()
            .increment_counter(10)
            .call()
            .await?;

        assert_eq!(52, response.value);

To generate these bindings, all you have to do is:

        use fuels::prelude::*;
        // Replace with your own JSON abi path (relative to the root of your crate)
        abigen!(MyContractName, "examples/rust_bindings/src/abi.json");

And this abigen! macro will expand the code with the type-safe Rust bindings. It takes two arguments:

  1. The name of the struct that will be generated (MyContractName);
  2. Either a path as a string to the JSON ABI file or the JSON ABI as a multiline string directly.

The same as the example above but passing the ABI definition directly:

        // Don't forget to import the `abigen` macro as above
        abigen!(
            MyContract,
            r#"
            {
                "types": [
                  {
                    "typeId": 0,
                    "type": "u64",
                    "components": null,
                    "typeParameters": null
                  }
                ],
                "functions": [
                  {
                    "inputs": [
                      {
                        "name": "value",
                        "type": 0,
                        "typeArguments": null
                      }
                    ],
                    "name": "initialize_counter",
                    "output": {
                      "name": "",
                      "type": 0,
                      "typeArguments": null
                    }
                  },
                  {
                    "inputs": [
                      {
                        "name": "value",
                        "type": 0,
                        "typeArguments": null
                      }
                    ],
                    "name": "increment_counter",
                    "output": {
                      "name": "",
                      "type": 0,
                      "typeArguments": null
                    }
                  }
                ]
              }
            "#
        );

The setup_contract_test! macro

When deploying contracts with the abigen! macro, as shown in the previous sections, the user can:

  • change the default configuration parameters
  • launch several providers
  • create multiple wallets
  • create specific assets, etc.

However, it is often the case that we want to test only the contract methods and we want to simply deploy the contract with the default configuration parameters. The setup_contract_test! macro does exactly that. When expanded, the setup_contract_test! macro will:

  1. run the abigen
  2. launch a local provider
  3. setup one wallet
  4. deploy the selected contract

The setup code that you have seen in previous sections gets reduced to:

        setup_contract_test!(
            contract_instance,
            wallet,
            "packages/fuels/tests/contracts/contract_test"
        );

        let response = contract_instance
            .methods()
            .initialize_counter(42)
            .call()
            .await?;

        assert_eq!(42, response.value);

The input of the macro are the contract instance variable name, wallet variable name and the forc project path. Both the contract instance and wallet variables get brought into context and they can be used further in the code.

Note The same contract can be deployed several times as the macro deploys the contracts with salt. You can also deploy different contracts to the same provider using a shared wallet.

If you want to deploy contracts to the same provider, you have to set the wallet name of the first macro to wallet and all the remaining wallet names to None. The first macro will create wallet and bring it into context, and the other macros will use it instead of creating new ones. Let's see it in an example.

    // The first wallet name must be `wallet`
    setup_contract_test!(
        foo_contract_instance,
        wallet,
        "packages/fuels/tests/contracts/foo_contract"
    );
    let foo_contract_id = foo_contract_instance.get_contract_id();

    // The macros that want to use the `wallet` have to set
    // the wallet name to `None`
    setup_contract_test!(
        foo_caller_contract_instance,
        None,
        "packages/fuels/tests/contracts/foo_caller_contract"
    );
    let foo_caller_contract_id = foo_caller_contract_instance.get_contract_id();

    setup_contract_test!(
        foo_caller_contract_instance2,
        None,
        "packages/fuels/tests/contracts/foo_caller_contract"
    );
    let foo_caller_contract_id2 = foo_caller_contract_instance2.get_contract_id();

    // Because we deploy with salt, we can deploy the same contract multiple times
    assert_ne!(foo_caller_contract_id, foo_caller_contract_id2);

    // The first contract can be called because they were deployed on the same provider
    let bits = *foo_contract_id.hash();
    let res = foo_caller_contract_instance
        .methods()
        .call_foo_contract(Bits256(bits), true)
        .set_contracts(&[foo_contract_id.clone()]) // Sets the external contract
        .call()
        .await?;
    assert!(res.value);

    let res = foo_caller_contract_instance2
        .methods()
        .call_foo_contract(Bits256(bits), true)
        .set_contracts(&[foo_contract_id.clone()]) // Sets the external contract
        .call()
        .await?;
    assert!(res.value);

In this example, three contracts are deployed on the same provider using the wallet. The second and third macro use the same contract but have different IDs because of the deployment with salt. Both of them can call the first contract by using its ID.

In addition, you can manually create the wallet variable and then use it inside the macro. This is useful if you want to create custom wallets or providers, but still want to use the macro to reduce boilerplate code. Below is an example of this approach.

    let config = WalletsConfig::new(Some(2), Some(1), Some(DEFAULT_COIN_AMOUNT));

    let mut wallets = launch_custom_provider_and_get_wallets(config, None, None).await;
    let wallet = wallets.pop().unwrap();
    let wallet_2 = wallets.pop().unwrap();

    setup_contract_test!(
        contract_instance,
        None,
        "packages/fuels/tests/contracts/contract_test"
    );

Calling contracts

Once you've deployed your contract, as seen in the previous sections, you'll likely want to:

  1. Call contract methods;
  2. Configure call and transaction parameters such as gas price, byte price, and gas limit;
  3. Forward coins and gas in your contract calls;
  4. Read and interpret returned values and logs.

Here's an example. Suppose your Sway contract has two ABI methods called initialize_counter(u64) and increment_counter(u64). Once you've deployed it the contract, you can call these methods like this:

        // This is an instance of your contract which you can use to make calls to your functions
        let contract_instance = MyContract::new(contract_id, wallet);

        let response = contract_instance
            .methods()
            .initialize_counter(42) // Build the ABI call
            .call() // Perform the network call
            .await?;

        assert_eq!(42, response.value);

        let response = contract_instance
            .methods()
            .increment_counter(10)
            .call()
            .await?;

        assert_eq!(52, response.value);

The example above uses all the default configurations and performs a simple contract call.

Next, we'll see how we can further configure the many different parameters in a contract call

Calls with different wallets

You can use the with_wallet() method on an existing contract instance as a shorthand for creating a new instance connected to the provided wallet. This lets you make contracts calls with different wallets in a chain like fashion.

        // Create contract instance with wallet_1
        let contract_instance = MyContract::new(contract_id, wallet_1.clone());

        // Perform contract call with wallet_2
        let response = contract_instance
            .with_wallet(wallet_2)? // Connect wallet_2
            .methods() // Get contract methods
            .get_msg_amount() // Our contract method
            .call() // Perform the contract call.
            .await?; // This is an async call, `.await` for it.

Note: connecting a different wallet to an existing instance ignores its set provider in favor of the provider used to deploy the contract. If you have two wallets connected to separate providers (each communicating with a separate fuel-core), the one assigned to the deploying wallet will also be used for contract calls. This behavior is only relevant if multiple providers (i.e. fuel-core instances) are present and can otherwise be ignored.

Transaction parameters

Transaction parameters are:

  1. Gas price;
  2. Gas limit;
  3. Maturity.

You can configure these parameters by creating an instance of TxParameters and passing it to a chain method called tx_params:

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

        // In order: gas_price, gas_limit, and maturity
        let my_tx_params = TxParameters::new(None, Some(1_000_000), None);

        let response = contract_methods
            .initialize_counter(42) // Our contract method.
            .tx_params(my_tx_params) // Chain the tx params setting method.
            .call() // Perform the contract call.
            .await?; // This is an async call, `.await` for it.

You can also use TxParameters::default() to use the default values:

pub const DEFAULT_GAS_LIMIT: u64 = 1_000_000;
pub const DEFAULT_GAS_PRICE: u64 = 0;
pub const DEFAULT_MATURITY: u64 = 0;

This way:

        let response = contract_methods
            .initialize_counter(42)
            .tx_params(TxParameters::default())
            .call()
            .await?;

As you might have noticed already, TxParameters can also be specified when deploying contracts or transfering assets by passing it to the respective methods.

Note: whenever you perform an action that results in a transaction (contract deployment, contract call, asset transfer), the SDK will automatically estimate the fee based on the set gas limit and the transaction's byte size. This estimation is used when building the transaction. A side-effect of this is that your wallet must at least own a single coin of the base asset of any amount.

Call parameters

Call parameters are:

  1. Amount;
  2. Asset ID;
  3. Gas forwarded.

You can use these to forward coins to a contract. You can configure these parameters by creating an instance of CallParameters and passing it to a chain method called call_params.

For instance, suppose the following contract that uses Sway's msg_amount() to return the amount sent in that transaction.

    fn get_msg_amount() -> u64 {
        msg_amount()
    }

Then, in Rust, after setting up and deploying the above contract, you can configure the amount being sent in the transaction like this:

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

        let tx_params = TxParameters::default();

        // Forward 1_000_000 coin amount of base asset_id
        // this is a big number for checking that amount can be a u64
        let call_params = CallParameters::new(Some(1_000_000), None, None);

        let response = contract_methods
            .get_msg_amount() // Our contract method.
            .tx_params(tx_params) // Chain the tx params setting method.
            .call_params(call_params) // Chain the call params setting method.
            .call() // Perform the contract call.
            .await?;

You can also use CallParameters::default() to use the default values:

pub const DEFAULT_CALL_PARAMS_AMOUNT: u64 = 0;
// Bytes representation of the asset ID of the "base" asset used for gas fees.
pub const BASE_ASSET_ID: AssetId = AssetId::BASE;

This way:

        let response = contract_methods
            .initialize_counter(42)
            .call_params(CallParameters::default())
            .call()
            .await?;

The gas_forwarded parameter defines the limit for the actual contract call as opposed to the gas limit for the whole transaction. This means that it is constrained by the transaction limit. If it is set to an amount greater than the available gas, all available gas will be forwarded.

        // Set the transaction `gas_limit` to 10000 and `gas_forwarded` to 4300 to specify that the
        // contract call transaction may consume up to 10000 gas, while the actual call may only use 4300
        // gas
        let tx_params = TxParameters::new(None, Some(10000), None);
        let call_params = CallParameters::new(None, None, Some(4300));

        let response = contract_methods
            .get_msg_amount() // Our contract method.
            .tx_params(tx_params) // Chain the tx params setting method.
            .call_params(call_params) // Chain the call params setting method.
            .call() // Perform the contract call.
            .await?;

If you don't set the call parameters or use CallParameters::default(), the transaction gas limit will be forwarded instead.

Call response

You've probably noticed that you're often chaining .call().await.unwrap(). That's because:

  1. You have to choose between .call() and .simulate() (more on this in the next section);
  2. Contract calls are asynchronous, so you can choose to either .await it or perform concurrent tasks, making full use of Rust's async;
  3. .unwrap() the Result<FuelCallResponse, Error> returned by the contract call.

Once you unwrap the FuelCallResponse, you have access to this struct:

pub struct FuelCallResponse<D> {
    pub value: D,
    pub receipts: Vec<Receipt>,
    pub gas_used: u64,
    pub log_decoder: LogDecoder,
}

Where value will hold the value returned by its respective contract method, represented by the exact type returned by the FuelVM, E.g., if your contract returns a FuelVM's u64, value's D will be a u64. If it's a FuelVM's tuple (u8,bool), then D will be a (u8,bool). If it's a custom type, for instance, a Sway struct MyStruct containing two components, a u64, and a b256, D will be a struct generated at compile-time, called MyStruct with u64 and a [u8; 32] (the equivalent of b256 in Rust-land).

  • receipts will hold all receipts generated by that specific contract call.
  • gas_used is the amount of gas it consumed by the contract call.

Logs

Whenever you log a value within a contract method, the resulting log entry is added to the log receipt and the variable type is recorded in the contract's ABI. The SDK lets you parse those values into Rust types.

Consider the following contract method:

    fn produce_logs_variables() -> () {
        let f: u64 = 64;
        let u: b256 = 0xef86afa9696cf0dc6385e2c407a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a;
        let e: str[4] = "Fuel";
        let l: [u8; 3] = [1u8, 2u8, 3u8];

        log(f);
        log(u);
        log(e);
        log(l);
    }

You can access the logged values in Rust by calling logs_with_type::<T> from a FuelCallResponse, where T is the type of the logged variables you want to retrieve. The result will be a Vec<T>:

    let contract_methods = contract_instance.methods();
    let response = contract_methods.produce_logs_variables().call().await?;

    let log_u64 = response.get_logs_with_type::<u64>()?;
    let log_bits256 = response.get_logs_with_type::<Bits256>()?;
    let log_string = response.get_logs_with_type::<SizedAsciiString<4>>()?;
    let log_array = response.get_logs_with_type::<[u8; 3]>()?;

    let expected_bits256 = Bits256([
        239, 134, 175, 169, 105, 108, 240, 220, 99, 133, 226, 196, 7, 166, 225, 89, 161, 16, 60,
        239, 183, 226, 174, 6, 54, 251, 51, 211, 203, 42, 158, 74,
    ]);

    assert_eq!(log_u64, vec![64]);
    assert_eq!(log_bits256, vec![expected_bits256]);
    assert_eq!(log_string, vec!["Fuel"]);
    assert_eq!(log_array, vec![[1, 2, 3]]);

You can also get a vector of all the logged values as strings using get_logs():

    let contract_methods = contract_instance.methods();
    let response = contract_methods.produce_multiple_logs().call().await?;
    let logs = response.get_logs()?;

Due to possible performance hits, it is not recommended to use get_logs() outside of a debugging scenario.

Variable outputs

In some cases, you might want to send funds to the output of a transaction. Sway has a specific method for that: transfer_to_address(coins, asset_id, recipient)`. So, if you have a contract that does something like this:

    fn transfer_coins_to_output(coins: u64, asset_id: ContractId, recipient: Address) {
        transfer_to_address(coins, asset_id, recipient);
    }

With the SDK, you can call transfer_coins_to_output by chaining append_variable_outputs(amount) to your contract call. Like this:

        let address = wallet.address();

        // withdraw some tokens to wallet
        let response = contract_methods
            .transfer_coins_to_output(1_000_000, contract_id.into(), address.into())
            .append_variable_outputs(1)
            .call()
            .await?;

append_variable_outputs effectively appends a given amount of Output::Variables to the transaction's list of outputs. This output type indicates that the output's amount and the owner may vary based on transaction execution.

Note that the Sway lib-std function mint_to_address calls transfer_to_address under the hood, so you need to call append_variable_outputs in the Rust SDK tests like you would for transfer_to_address.

Read-only calls

Sometimes you want to call a contract method that doesn't change the state of the blockchain. For instance, a method that only reads a value from storage and returns it.

In this case, there's no need to generate an actual blockchain transaction; you only want to read a value quickly.

You can do this with the SDK. Instead of .call()ing the method, use .simulate():

        // you would mint 100 coins if the transaction wasn't simulated
        let counter = contract_methods.mint_coins(100).simulate().await?;

Note that if you use .simulate() on a method that does change the state of the blockchain, it won't work properly; it will just dry-run it.

At the moment, it's up to you to know whether a contract method changes state or not, and use .call() or .simulate() accordingly.

Calling other contracts

Your contract method might be calling other contracts. To do so, you must feed the external contract IDs that your contract depends on to the method you're calling. You do it by chaining .set_contracts(&[external_contract_id, ...]) to the method you want to call. For instance:

    let bits = *foo_contract_id.hash();
    let res = foo_caller_contract_instance
        .methods()
        .call_foo_contract(Bits256(bits), true)
        .set_contracts(&[foo_contract_id.clone()]) // Sets the external contract
        .call()
        .await?;

Multiple contract calls

With ContractMultiCallHandler, you can execute multiple contract calls within a single transaction. To achieve this, you first prepare all the contract calls that you want to bundle:

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

        let call_handler_1 = contract_methods.initialize_counter(42);
        let call_handler_2 = contract_methods.get_array([42; 2]);

You can also set call parameters, variable outputs, or external contracts for every contract call, as long as you don't execute it with call() or simulate().

Next, you provide the prepared calls to your ContractMultiCallHandler and optionally configure transaction parameters:

        let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone());

        multi_call_handler
            .add_call(call_handler_1)
            .add_call(call_handler_2);

Note: any transaction parameters configured on separate contract calls are disregarded in favor of the parameters provided to ContractMultiCallHandler.

Output values

To get the output values of the bundled calls, you need to provide explicit type annotations when saving the result of call() or simulate() to a variable:

        let (counter, array): (u64, [u64; 2]) = multi_call_handler.call().await?.value;

You can also interact with the FuelCallResponse by moving the type annotation to the invoked method:

        let response = multi_call_handler.call::<(u64, [u64; 2])>().await?;

Estimating contract call cost

With with the function estimate_transaction_cost(tolerance: Option<f64>) provided by ContractCallHandler and ContractMultiCallHandler, you can get a cost estimation for a specific call. The return type, TransactionCost, is a struct that contains relevant information for the estimation:

TransactionCost {
    min_gas_price: u64,
    min_byte_price: u64,
    gas_price: u64,
    gas_used: u64,
    metered_bytes_size: u64,
    total_fee: f64, // where total_fee is the sum of the gas and byte fees
}

Below are examples that show how to get the estimated transaction cost from single and multi call transactions.

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

        let tolerance = 0.0;
        let transaction_cost = contract_instance
            .methods()
            .initialize_counter(42) // Build the ABI call
            .estimate_transaction_cost(Some(tolerance)) // Get estimated transaction cost
            .await?;
        let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone());

        let call_handler_1 = contract_methods.initialize_counter(42);
        let call_handler_2 = contract_methods.get_array([42; 2]);

        multi_call_handler
            .add_call(call_handler_1)
            .add_call(call_handler_2);

        let tolerance = 0.0;
        let transaction_cost = multi_call_handler
            .estimate_transaction_cost(Some(tolerance)) // Get estimated transaction cost
            .await?;

The transaction cost estimation can be used to set the gas limit for an actual call, or to show the user the estimated cost.

Running scripts

You can run a script using its JSON-ABI and the path to its binary file. You can run the scripts with arguments. For this, you have to use the script_abigen! macro, which is similar to the abigen! macro seen previously.

    // The abigen is used for the same purpose as with contracts (Rust bindings)
    script_abigen!(
        MyScript,
        "packages/fuels/tests/scripts/script_with_arguments/out/debug/script_with_arguments-abi.json"
    );
    let wallet = launch_provider_and_get_wallet().await;
    let bin_path =
        "../fuels/tests/scripts/script_with_arguments/out/debug/script_with_arguments.bin";
    let instance = MyScript::new(wallet, bin_path);

    let bim = Bimbam { val: 90 };
    let bam = SugarySnack {
        twix: 100,
        mars: 1000,
    };
    let result = instance.main(bim, bam).call().await?;
    let expected = Bimbam { val: 2190 };
    assert_eq!(result.value, expected);

Running scripts with transaction parameters

The method for passing transaction parameters is the same as with contracts. As a reminder, the workflow would look like this:

    let parameters = TxParameters {
        gas_price: 1,
        gas_limit: 10000,
        ..Default::default()
    };
    let result = instance.main(a, b).tx_params(parameters).call().await?;

Logs

Script calls provide the same logging functions, get_logs() and get_logs_with_type<T>(), as contract calls. As a reminder, the workflow looks like this:

    script_abigen!(
        log_script,
        "packages/fuels/tests/logs/script_logs/out/debug/script_logs-abi.json"
    );

    let wallet = launch_provider_and_get_wallet().await;
    let bin_path = "../fuels/tests/logs/script_logs/out/debug/script_logs.bin";
    let instance = log_script::new(wallet.clone(), bin_path);

    let response = instance.main().call().await?;

    let logs = response.get_logs()?;
    let log_u64 = response.get_logs_with_type::<u64>()?;

Predicates

Predicates, in Sway, are programs that return a Boolean value, and they do not have any side effects (they are pure).

Instantiating predicates

Similar to contracts, once you've written a predicate in Sway and compiled it with forc build (read here for more on how to work with Sway), you'll get the predicate binary. Using the binary, you can instantiate a predicate as shown in the code snippet below:

        let predicate = Predicate::load_from(
            "../../packages/fuels/tests/predicates/predicate_signatures/out/debug/predicate_signatures.bin",
        )?;

        let predicate_code = predicate.code();
        let predicate_address = predicate.address();

The created predicate instance has two fields. The predicate byte code and the predicate address. This address is generated from the byte code and is the same as the P2SH address used in Bitcoin. Users can seamlessly send assets to the predicate address as they do for any other address on the chain. To spend the predicate funds, the user has to provide the original byte code of the predicate together with the predicate data. The predicate data will be used when executing the byte code, and if the predicate is validated successfully, the funds will be accessible.

In the next section, we show how to interact with a predicate and explore an example where specific signatures are needed to spend the predicate funds.

Send and spend funds from predicates

Let's consider the following predicate example:

predicate;

use std::{b512::B512, constants::ZERO_B256, ecr::ec_recover_address, inputs::input_predicate_data};

fn extract_pulic_key_and_match(signature: B512, expected_public_key: b256) -> u64 {
    if let Result::Ok(pub_key_sig) = ec_recover_address(signature, ZERO_B256)
    {
        if pub_key_sig.value == expected_public_key {
            return 1;
        }
    }
    0
}

fn main() -> bool {
    let signatures: [B512; 3] = input_predicate_data(0);

    let public_keys = [
        0xd58573593432a30a800f97ad32f877425c223a9e427ab557aab5d5bb89156db0,
        0x14df7c7e4e662db31fe2763b1734a3d680e7b743516319a49baaa22b2032a857,
        0x3ff494fb136978c3125844625dad6baf6e87cdb1328c8a51f35bda5afe72425c,
    ];

    let mut matched_keys = 0;

    matched_keys = extract_pulic_key_and_match(signatures[0], public_keys[0]);
    matched_keys = matched_keys + extract_pulic_key_and_match(signatures[1], public_keys[1]);
    matched_keys = matched_keys + extract_pulic_key_and_match(signatures[2], public_keys[2]);

    matched_keys > 1
}

This predicate accepts three signatures and matches them to three predefined public keys. The ec_recover_address function is used to recover the public key from the signatures. If two of three extracted public keys match the predefined public keys, the funds can be spent. Note that the signature order has to match the order of the predefined public keys.

Let's use the SDK to interact with the predicate. First, let's create three wallets with specific keys. Their hashed public keys are already hard-coded in the predicate.

        let secret_key1: SecretKey =
            "0x862512a2363db2b3a375c0d4bbbd27172180d89f23f2e259bac850ab02619301"
                .parse()
                .unwrap();

        let secret_key2: SecretKey =
            "0x37fa81c84ccd547c30c176b118d5cb892bdb113e8e80141f266519422ef9eefd"
                .parse()
                .unwrap();

        let secret_key3: SecretKey =
            "0x976e5c3fa620092c718d852ca703b6da9e3075b9f2ecb8ed42d9f746bf26aafb"
                .parse()
                .unwrap();

        let mut wallet = WalletUnlocked::new_from_private_key(secret_key1, None);
        let mut wallet2 = WalletUnlocked::new_from_private_key(secret_key2, None);
        let mut wallet3 = WalletUnlocked::new_from_private_key(secret_key3, None);
        let receiver = WalletUnlocked::new_random(None);

Next, let's add some coins, start a provider and connect it with the wallets.

        let all_coins = [&wallet, &wallet2, &wallet3]
            .iter()
            .flat_map(|wallet| {
                setup_single_asset_coins(wallet.address(), AssetId::default(), 10, 1_000_000)
            })
            .collect::<Vec<_>>();

        let (provider, _) = setup_test_provider(
            all_coins,
            vec![],
            Some(Config {
                utxo_validation: true,
                ..Config::local_node()
            }),
            None,
        )
        .await;

        [&mut wallet, &mut wallet2, &mut wallet3]
            .iter_mut()
            .for_each(|wallet| wallet.set_provider(provider.clone()));

Now we can load the predicate binary, and prepare some transaction variables.

        let predicate = Predicate::load_from(
            "../../packages/fuels/tests/predicates/predicate_signatures/out/debug/predicate_signatures.bin",
        )?;

        let predicate_code = predicate.code();
        let predicate_address = predicate.address();
        let amount_to_predicate = 1000;
        let asset_id = AssetId::default();

After the predicate address is generated we can send funds to it. Note that we are using the same transfer function as we used when sending funds to other wallets. We also make sure that the funds are indeed transferred.

        wallet
            .transfer(
                predicate_address,
                amount_to_predicate,
                asset_id,
                TxParameters::default(),
            )
            .await?;

        let predicate_balance = provider
            .get_asset_balance(predicate.address(), asset_id)
            .await?;
        assert_eq!(predicate_balance, amount_to_predicate);

To spend the funds that are now locked in the predicate, we have to provide two out of three signatures whose public keys match the ones we defined in the predicate. In this example, the signatures are generated from an array of zeros.

        let data_to_sign = [0; 32];
        let signature1 = wallet.sign_message(data_to_sign).await?.to_vec();
        let signature2 = wallet2.sign_message(data_to_sign).await?.to_vec();
        let signature3 = wallet3.sign_message(data_to_sign).await?.to_vec();

        let signatures = vec![signature1, signature2, signature3];

After generating the signatures, we can send a transaction to spend the predicate funds. We use the receiver wallet as the recipient. We have to provide the predicate byte code and the required signatures. As we provide the correct data, we receive the funds and verify that the amount is correct.

        let predicate_data = signatures.into_iter().flatten().collect();
        wallet
            .spend_predicate(
                predicate_address,
                predicate_code,
                amount_to_predicate,
                asset_id,
                receiver.address(),
                Some(predicate_data),
                TxParameters::default(),
            )
            .await?;

        let receiver_balance_after = provider
            .get_asset_balance(receiver.address(), asset_id)
            .await?;
        assert_eq!(amount_to_predicate, receiver_balance_after);

        let predicate_balance = provider
            .get_asset_balance(predicate.address(), asset_id)
            .await?;
        assert_eq!(predicate_balance, 0);

Predicate data

Let's consider the following predicate example:

predicate;

use std::inputs::input_predicate_data;

fn main() -> bool {
    let guessed_number: u64 = input_predicate_data(0);
    if guessed_number == 42 {
        return true;
    }
    false
}

With the Fuel Rust SDK, You can encode and send the predicate data through Predicate's encode_data():

        let predicate_data: Vec<u8> = predicate.encode_data(42_u64)?;

Keep on reading for the full example.

Notice how this predicate uses input_predicate_data(), a way for the predicate code to read the data the caller passed to it.

Like everything else in the FuelVM, this data follows the ABI encoding/decoding specification. When using the Fuel Rust SDK to pass data to this predicate, you must encode it properly.

Here's how you can do it. First, we set up the wallets, node, and predicate code:

        let provider_config = Config {
            utxo_validation: true,
            ..Config::local_node()
        };

        let wallets_config = WalletsConfig::new_multiple_assets(
            2,
            vec![AssetConfig {
                id: AssetId::default(),
                num_coins: 1,
                coin_amount: 1_000,
            }],
        );

        let wallets =
            &launch_custom_provider_and_get_wallets(wallets_config, Some(provider_config), None)
                .await;

        let first_wallet = &wallets[0];
        let second_wallet = &wallets[1];

        let predicate = Predicate::load_from( "../../packages/fuels/tests/predicates/predicate_data_example/out/debug/predicate_data_example.bin")?;

        let predicate_code = predicate.code();
        let predicate_address = predicate.address();

Next, we lock some assets in this predicate using the first wallet:

        // First wallet transfers amount to predicate.
        let _result = first_wallet
            .transfer(
                predicate_address,
                500,
                AssetId::default(),
                TxParameters::default(),
            )
            .await?;

        // Check predicate balance.
        let balance = first_wallet
            .get_provider()?
            .get_asset_balance(predicate_address, AssetId::default())
            .await?;

        assert_eq!(balance, 500);

Then, we try to unlock the amount and spend it using the second wallet, effectively sending the previously locked value to itself.

The predicate expects the data sent to it to be a u64 type with the value 42.

        // We use the Predicate's `encode_data()` to encode the data we want to
        // send to the predicate.

        let predicate_data: Vec<u8> = predicate.encode_data(42_u64)?;

        let amount_to_unlock = 500;

        let _result = second_wallet
            .spend_predicate(
                predicate_address,
                predicate_code,
                amount_to_unlock,
                AssetId::default(),
                second_wallet.address(),
                Some(predicate_data),
                TxParameters::default(),
            )
            .await?;

        // Predicate balance is zero.
        let balance = first_wallet
            .get_provider()?
            .get_asset_balance(predicate_address, AssetId::default())
            .await?;

        assert_eq!(balance, 0);

        // Second wallet balance is updated.
        let balance = second_wallet.get_asset_balance(&AssetId::default()).await?;
        assert_eq!(balance, 1500);

Note: if the data you're encoding is already a Vec<u8>, e.g., in the send and spend examples, then you don't need to call encode_predicate_data(), passing it as-is works.

Scripts

note This page is still a work in progress.

Running scripts

Types

The FuelVM and Sway have many internal types. These types have equivalents in the SDK. This section discusses these types, how to use them, and how to convert them.

Bytes32

In Sway and the FuelVM, Bytes32 represents hashes. They hold a 256-bit (32-byte) value. Bytes32 is a wrapper on a 32-sized slice of u8: pub struct Bytes32([u8; 32]);.

These are the main ways of creating a Bytes32:

        use fuels::tx::Bytes32;
        use std::str::FromStr;

        // Zeroed Bytes32
        let b256 = Bytes32::zeroed();

        // Grab the inner `[u8; 32]` from
        // `Bytes32` by dereferencing (i.e. `*`) it.
        assert_eq!([0u8; 32], *b256);

        // From a `[u8; 32]`.
        let my_slice = [1u8; 32];
        let b256 = Bytes32::new(my_slice);
        assert_eq!([1u8; 32], *b256);

        // From a hex string.
        let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
        let b256 = Bytes32::from_str(hex_str).expect("failed to create Bytes32 from string");
        assert_eq!([0u8; 32], *b256);

Bytes32 also implements fmt's Debug, Display, LowerHex and UpperHex traits. For example, you can get the display and hex representations with:

        let b256_string = b256.to_string();
        let b256_hex_string = format!("{:#x}", b256);

For a full list of implemented methods and traits, see the fuel-types documentation.

Note: In Fuel, there's a special type called b256, which is similar to Bytes32; also used to represent hashes, and it holds a 256-bit value. In Rust, through the SDK, this is represented as Bits256(value) where value is a [u8; 32]. If your contract method takes a b256 as input, all you need to do is pass a Bits256([u8; 32]) when calling it from the SDK.

Address

Like Bytes32, Address is a wrapper on [u8; 32] with similar methods and implements the same traits (see fuel-types documentation).

These are the main ways of creating an Address:

        use fuels::tx::Address;
        use std::str::FromStr;

        // Zeroed Bytes32
        let address = Address::zeroed();

        // Grab the inner `[u8; 32]` from
        // `Bytes32` by dereferencing (i.e. `*`) it.
        assert_eq!([0u8; 32], *address);

        // From a `[u8; 32]`.
        let my_slice = [1u8; 32];
        let address = Address::new(my_slice);
        assert_eq!([1u8; 32], *address);

        // From a string.
        let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
        let address = Address::from_str(hex_str).expect("failed to create Address from string");
        assert_eq!([0u8; 32], *address);

ContractId

Like Bytes32, ContractId is a wrapper on [u8; 32] with similar methods and implements the same traits (see fuel-types documentation).

These are the main ways of creating a ContractId:

        use fuels::tx::ContractId;
        use std::str::FromStr;

        // Zeroed Bytes32
        let contract_id = ContractId::zeroed();

        // Grab the inner `[u8; 32]` from
        // `Bytes32` by dereferencing (i.e. `*`) it.
        assert_eq!([0u8; 32], *contract_id);

        // From a `[u8; 32]`.
        let my_slice = [1u8; 32];
        let contract_id = ContractId::new(my_slice);
        assert_eq!([1u8; 32], *contract_id);

        // From a string.
        let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
        let contract_id =
            ContractId::from_str(hex_str).expect("failed to create ContractId from string");
        assert_eq!([0u8; 32], *contract_id);

AssetId

Like Bytes32, AssetId is a wrapper on [u8; 32] with similar methods and implements the same traits (see fuel-types documentation).

These are the main ways of creating an AssetId:

        use fuels::tx::AssetId;
        use std::str::FromStr;

        // Zeroed Bytes32
        let asset_id = AssetId::zeroed();

        // Grab the inner `[u8; 32]` from
        // `Bytes32` by dereferencing (i.e. `*`) it.
        assert_eq!([0u8; 32], *asset_id);

        // From a `[u8; 32]`.
        let my_slice = [1u8; 32];
        let asset_id = AssetId::new(my_slice);
        assert_eq!([1u8; 32], *asset_id);

        // From a string.
        let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
        let asset_id = AssetId::from_str(hex_str).expect("failed to create AssetId from string");
        assert_eq!([0u8; 32], *asset_id);

Converting native types

You might want to convert between the native types (Bytes32, Address, ContractId, and AssetId). Because these types are wrappers on [u8; 32], converting is a matter of dereferencing one and instantiating the other using the dereferenced value. Here's an example:

        use fuels::tx::{AssetId, ContractId};

        let contract_id = ContractId::new([1u8; 32]);

        let asset_id: AssetId = AssetId::new(*contract_id);

        assert_eq!([1u8; 32], *asset_id);

Bech32

Bech32Address and Bech32ContractId enable the use of addresses and contract ids in the bech32 format. They can easily be converted to their counterparts Address and ContractId.

Here are the main ways of creating a Bech32Address, but note that the same applies to Bech32ContractId:

        use fuels::prelude::Bech32Address;
        use fuels::tx::{Address, Bytes32};

        // New from HRP string and a hash
        let hrp = "fuel";
        let my_slice = [1u8; 32];
        let _bech32_address = Bech32Address::new(hrp, my_slice);

        // Note that you can also pass a hash stored as Bytes32 to new:
        let my_hash = Bytes32::new([1u8; 32]);
        let _bech32_address = Bech32Address::new(hrp, my_hash);

        // From a string.
        let address = "fuel1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsx2mt2";
        let bech32_address =
            Bech32Address::from_str(address).expect("failed to create Bech32 address from string");
        assert_eq!([0u8; 32], *bech32_address.hash());

        // From Address
        let plain_address = Address::new([0u8; 32]);
        let bech32_address = Bech32Address::from(plain_address);
        assert_eq!([0u8; 32], *bech32_address.hash());

        // Convert to Address
        let _plain_address: Address = bech32_address.into();

Note: when creating a Bech32Address from Address or Bech32ContractId from ContractId the HRP (Human-Readable Part) is set to "fuel" per default.

Structs and enums

The structs and enums you define in your Sway code have equivalents automatically generated by the SDK's abigen! macro.

For instance, if in your Sway code you have a struct called CounterConfig that looks like this:

struct CounterConfig {
  dummy: bool,
  initial_value: u64,
}

After using the abigen! macro, CounterConfig will be accessible in your Rust file! Here's an example:

    abigen!(
        MyContract,
        "packages/fuels/tests/types/complex_types_contract/out/debug/complex_types_contract-abi.json"
    );

    // Here we can use `CounterConfig`, a struct originally
    // defined in the contract.
    let counter_config = CounterConfig {
        dummy: true,
        initial_value: 42,
    };

You can freely use your custom types (structs or enums) within this scope. That also means passing custom types to functions and receiving custom types from function calls.

Manual decoding

Suppose you wish to decode raw bytes into a type used in your contract and the abigen! generated this type, then you can use try_into:

    let shaker_in_bytes: Vec<u8> = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2];

    let expected = Shaker::Mojito(2);

    // as slice
    let actual: Shaker = shaker_in_bytes[..].try_into()?;
    assert_eq!(actual, expected);

    // as ref
    let actual: Shaker = (&shaker_in_bytes).try_into()?;
    assert_eq!(actual, expected);

    // as value
    let actual: Shaker = shaker_in_bytes.try_into()?;
    assert_eq!(actual, expected);

Otherwise, for native types such as u8, u32,...,ContractId and others, you must use ::fuels::core::try_from_bytes:

        let contract_id_bytes = [0xFF; 32];
        let contract_id = ContractId::new(contract_id_bytes);

        let asset_id_bytes = [0xFF; 32];
        let asset_id = AssetId::new(asset_id_bytes);

        let bytes: Vec<u8> = [contract_id_bytes, asset_id_bytes].concat();
        let expected: (ContractId, AssetId) = try_from_bytes(&bytes)?;

        assert_eq!(expected, (contract_id, asset_id));

Generics

The Fuel Rust SDK supports both generic enums and generic structs. If you're already familiar with Rust, it's your typical struct MyStruct<T> type of generics support.

For instance, your Sway contract could look like this:

contract;

use std::hash::sha256;

struct SimpleGeneric<T> {
    single_generic_param: T,
}

abi MyContract {
  fn struct_w_generic(arg1: SimpleGeneric<u64>) -> SimpleGeneric<u64>;
}

impl MyContract for Contract {
    fn struct_w_generic(arg1: SimpleGeneric<u64>) -> SimpleGeneric<u64> {
        let expected = SimpleGeneric {
            single_generic_param: 123u64,
        };

        assert(arg1.single_generic_param == expected.single_generic_param);

        expected
    }
}

Your Rust code would look like this:

        // simple struct with a single generic param
        let arg1 = SimpleGeneric {
            single_generic_param: 123u64,
        };

        let result = contract_methods
            .struct_w_generic(arg1.clone())
            .call()
            .await?
            .value;

        assert_eq!(result, arg1);

String

The Rust SDK represents Fuel's Strings as SizedAsciiString<LEN>, where the generic parameter LEN is the length of a given string. This abstraction is necessary because all strings in Fuel and Sway are statically-sized, i.e., you must know the size of the string beforehand.

Here's how you can create a simple string using SizedAsciiString:

        let ascii_data = "abc".to_string();

        SizedAsciiString::<3>::new(ascii_data)
            .expect("Should have succeeded since we gave ascii data of correct length!");

To make working with SizedAsciiStrings easier, you can use try_into() to convert from Rust's String to SizedAsciiString, and you can use into() to convert from SizedAsciiString to Rust's String. Here are a few examples:

    #[test]
    fn can_be_constructed_from_str_ref() {
        let _: SizedAsciiString<3> = "abc".try_into().expect("Should have succeeded");
    }

    #[test]
    fn can_be_constructed_from_string() {
        let _: SizedAsciiString<3> = "abc".to_string().try_into().expect("Should have succeeded");
    }

    #[test]
    fn can_be_converted_into_string() {
        let sized_str = SizedAsciiString::<3>::new("abc".to_string()).unwrap();

        let str: String = sized_str.into();

        assert_eq!(str, "abc");
    }

If your contract's method takes and returns, for instance, a Sway's str[23]. When using the SDK, this method will take and return a SizedAsciiString<23>, and you can pass a string to it like this:

    let call_handler = contract_instance.methods().takes_string(
        "This is a full sentence"
            .try_into()
            .expect("failed to convert string into SizedAsciiString"),
    );

Bits256

In Fuel, a type called b256 represents hashes and holds a 256-bit value. The Rust SDK represents b256 as Bits256(value) where value is a [u8; 32]. If your contract method takes a b256 as input, you must pass a Bits256([u8; 32]) when calling it from the SDK.

Here's an example:

    let arg: [u8; 32] = hasher.finalize().into();

    let call_handler = contract_instance.methods().takes_b256(Bits256(arg));

If you have a hexadecimal value as a string and wish to convert it to Bits256, you may do so with from_hex_str:

        let hex_str = "0101010101010101010101010101010101010101010101010101010101010101";

        let bits256 = Bits256::from_hex_str(hex_str)?;

        assert_eq!(bits256.0, [1u8; 32]);

        // With the `0x0` prefix
        let hex_str = "0x0101010101010101010101010101010101010101010101010101010101010101";

        let bits256 = Bits256::from_hex_str(hex_str)?;

        assert_eq!(bits256.0, [1u8; 32]);

B512

In the Rust SDK, the B512 definition matches the Sway standard library type with the same name and will be converted accordingly when interacting with contracts:

pub struct B512 {
    pub bytes: [Bits256; 2],
}

Here's an example:

    let hi_bits = Bits256::from_hex_str(
        "0xbd0c9b8792876713afa8bff383eebf31c43437823ed761cc3600d0016de5110c",
    )?;
    let lo_bits = Bits256::from_hex_str(
        "0x44ac566bd156b4fc71a4a4cb2655d3dd360c695edb17dc3b64d611e122fea23d",
    )?;
    let b512 = B512::from((hi_bits, lo_bits));

EvmAddress

In the Rust SDK, Ethereum Virtual Machine (EVM) addresses can be represented with the 'EvmAddress' type. Its definition matches with the Sway standard library type with the same name and will be converted accordingly when interacting with contracts:

pub struct EvmAddress {
    // An evm address is only 20 bytes, the first 12 bytes should be set to 0
    pub value: Bits256,
}

Here's an example:

    let b256 = Bits256(hasher.finalize().into());
    let arg = EvmAddress::from(b256);

    let call_handler = contract_instance.methods().takes_evm_address(arg);

Note: when creating an EvmAddress from Bits256, the first 12 bytes will be cleared because an evm address is only 20 bytes long.

Vectors

Passing in vectors

You can pass a Rust std::vec::Vec into your contract method transparently. The following code calls a Sway contract method which accepts a Vec<SomeStruct<u32>>.

        let arg = vec![SomeStruct { a: 0 }, SomeStruct { a: 1 }];
        methods.struct_in_vec(arg.clone()).call().await?;

You can use a vector just like you would use any other type -- e.g. a [Vec<u32>; 2] or a SomeStruct<Vec<Bits256>> etc.

Returning vectors

This is currently not supported. If you try returning a type that is or contains a vector you will get a compile time error.

API

For a more in-depth look at the APIs provided by the Fuel Rust SDK, head over to the official documentation. In the actual rust docs, you can see the most up-to-date information about the API, which is synced with the code as it changes.

Debugging

note This page is still a work in progress.

Debugging abigen errors

Debugging contract call errors

Debugging network errors

Function selector

Whenever you call a contract method the SDK will generate a function selector according to the fuel specs which will be used by the node to identify which method we wish to execute.

If, for whatever reason, you wish to generate the function selector yourself you can do so:

        // fn some_fn_name(arg1: Vec<str[3]>, arg2: u8)
        let fn_name = "some_fn_name";
        let inputs = [Vec::<SizedAsciiString<3>>::param_type(), u8::param_type()];

        let selector = resolve_fn_selector(fn_name, &inputs);

        assert_eq!(selector, [0, 0, 0, 0, 7, 161, 3, 203]);

If you don't have the ParamType

If you won't or can't run the abigen! macro and all you have is the JSON ABI of you contract, you can still get the fn selector, but you have to jump through an extra hoop to get the ParamTypes:

        let abi: ProgramABI = serde_json::from_str(&abi_file_contents)?;

        let type_lookup = abi
            .types
            .into_iter()
            .map(|a_type| (a_type.type_id, a_type))
            .collect::<HashMap<_, _>>();

        let a_fun = abi
            .functions
            .into_iter()
            .find(|fun| fun.name == "array_of_structs")
            .unwrap();

        let inputs = a_fun
            .inputs
            .into_iter()
            .map(|type_appl| ParamType::try_from_type_application(&type_appl, &type_lookup))
            .collect::<Result<Vec<_>, _>>()?;

        let selector = resolve_fn_selector(&a_fun.name, &inputs);

        assert_eq!(selector, [0, 0, 0, 0, 39, 152, 108, 146,]);

fuels-rs Testing

Note This page is still a work in progress

Increasing the block height

You can use produce_blocks to help achieve an arbitrary block height; this is useful when you want to do any testing regarding transaction maturity.

Note: For the produce_blocks API to work, it is imperative to have manual_blocks_enabled = true in the config for the running node. See example below.

    let config = Config {
        manual_blocks_enabled: true, // Necessary so the `produce_blocks` API can be used locally
        ..Config::local_node()
    };
    let wallets =
        launch_custom_provider_and_get_wallets(WalletsConfig::default(), Some(config), None).await;
    let wallet = &wallets[0];
    let provider = wallet.get_provider()?;

    assert_eq!(provider.latest_block_height().await?, 0);

    provider.produce_blocks(3, None).await?;

    assert_eq!(provider.latest_block_height().await?, 3);

You can also set a custom block time by providing TimeParameters as the second, optional argument. TimeParameters is defined as:

pub struct TimeParameters {
    // The time to set on the first block
    pub start_time: DateTime<Utc>,
    // The time interval between subsequent blocks
    pub block_time_interval: Duration,
}

And here is an example:

    let config = Config {
        manual_blocks_enabled: true, // Necessary so the `produce_blocks` API can be used locally
        ..Config::local_node()
    };
    let wallets =
        launch_custom_provider_and_get_wallets(WalletsConfig::default(), Some(config), None).await;
    let wallet = &wallets[0];
    let provider = wallet.get_provider()?;

    assert_eq!(provider.latest_block_height().await?, 0);

    let time = TimeParameters {
        start_time: Utc.timestamp_opt(100, 0).unwrap(),
        block_time_interval: Duration::seconds(10),
    };
    provider.produce_blocks(3, Some(time)).await?;

    assert_eq!(provider.latest_block_height().await?, 3);

    let req = PaginationRequest {
        cursor: None,
        results: 10,
        direction: PageDirection::Forward,
    };
    let blocks: Vec<Block> = provider.get_blocks(req).await?.results;

    assert_eq!(blocks[1].header.time.unwrap().timestamp(), 100);
    assert_eq!(blocks[2].header.time.unwrap().timestamp(), 110);
    assert_eq!(blocks[3].header.time.unwrap().timestamp(), 120);

Cookbook

This section covers more advanced use cases that can be satisfied by combining various features of the Rust SDK. As such, it assumes that you have already made yourself familiar with the previous chapters of this book.

note This section is still a work in progress and more recipes may be added in the future.

Custom chain

This example demonstrates how to start a short-lived Fuel node with custom consensus parameters for the underlying chain.

First, we have to import ConsensusParameters from the fuels-tx crate:

        use fuels::tx::ConsensusParameters;

Next, we can define some values for the consensus parameters:

        let consensus_parameters_config = ConsensusParameters::DEFAULT
            .with_max_gas_per_tx(1000)
            .with_gas_price_factor(10)
            .with_max_inputs(2);

Before we can start a node, we probably also want to define some genesis coins and assign them to an address:

        let wallet = WalletUnlocked::new_random(None);
        let coins = setup_single_asset_coins(
            wallet.address(),
            Default::default(),
            DEFAULT_NUM_COINS,
            DEFAULT_COIN_AMOUNT,
        );

Finally, we call setup_test_client(), which starts a node with the given configs and returns a client:

        let node_config = Config::local_node();
        let (client, _) = setup_test_client(
            coins,
            vec![],
            Some(node_config),
            None,
            Some(consensus_parameters_config),
        )
        .await;
        let _provider = Provider::new(client);

Deposit and withdraw

Consider the following contract:

contract;

use std::{
    call_frames::{
        contract_id,
        msg_asset_id,
    },
    context::msg_amount,
    token::{
        mint_to_address,
        transfer_to_address,
    },
};

abi LiquidityPool {
    fn deposit(recipient: Address);
    fn withdraw(recipient: Address);
}

const BASE_TOKEN: b256 = 0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c;

impl LiquidityPool for Contract {
    fn deposit(recipient: Address) {
        assert(ContractId::from(BASE_TOKEN) == msg_asset_id());
        assert(0 < msg_amount());

        // Mint two times the amount.
        let amount_to_mint = msg_amount() * 2;

        // Mint some LP token based upon the amount of the base token.
        mint_to_address(amount_to_mint, recipient);
    }

    fn withdraw(recipient: Address) {
        assert(contract_id() == msg_asset_id());
        assert(0 < msg_amount());

        // Amount to withdraw.
        let amount_to_transfer = msg_amount() / 2;

        // Transfer base token to recipient.
        transfer_to_address(amount_to_transfer, ContractId::from(BASE_TOKEN), recipient);
    }
}

As its name suggests, it represents a simplified example of a liquidity pool contract. The method deposit() expects you to supply an arbitrary amount of the BASE_TOKEN. As a result, it mints double the amount of the liquidity asset to the calling address. Analogously, if you call withdraw() supplying it with the liquidity asset, it will transfer half that amount of the BASE_TOKEN back to the calling address except for deducting it from the contract balance instead of minting it.

The first step towards interacting with any contract in the Rust SDK is calling the abigen! macro to generate type-safe Rust bindings for the contract methods:

        abigen!(
            MyContract,
            "packages/fuels/tests/contracts/liquidity_pool/out/debug/liquidity_pool-abi.json"
        );

Next, we set up a wallet with custom-defined assets. We give our wallet some of the contracts BASE_TOKEN and the default asset (required for contract deployment):

        let base_asset_id: AssetId =
            "0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c"
                .parse()
                .unwrap();

        let asset_ids = [AssetId::default(), base_asset_id];
        let asset_configs = asset_ids
            .map(|id| AssetConfig {
                id,
                num_coins: 1,
                coin_amount: 1_000_000,
            })
            .into();

        let wallet_config = WalletsConfig::new_multiple_assets(1, asset_configs);
        let wallets = launch_custom_provider_and_get_wallets(wallet_config, None, None).await;
        let wallet = &wallets[0];

Having launched a provider and created the wallet, we can deploy our contract and create an instance of its methods:

        let contract_id = Contract::deploy(
            "../../packages/fuels/tests/contracts/liquidity_pool/out/debug/liquidity_pool.bin",
            wallet,
            TxParameters::default(),
            StorageConfiguration::default(),
        )
        .await?;

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

With the preparations out of the way, we can finally deposit to the liquidity pool by calling deposit() via the contract instance. Since we have to transfer assets to the contract, we create the appropriate CallParameters and chain them to the method call. To receive the minted liquidity pool asset, we have to append a variable output to our contract call.

        let deposit_amount = 1_000_000;
        let call_params = CallParameters::new(Some(deposit_amount), Some(base_asset_id), None);
        contract_methods
            .deposit(wallet.address().into())
            .call_params(call_params)
            .append_variable_outputs(1)
            .call()
            .await?;

As a final demonstration, let's use all our liquidity asset balance to withdraw from the pool and confirm we retrieved the initial amount. For this, we get our liquidity asset balance and supply it to the withdraw() call via CallParameters.

        let lp_asset_id = AssetId::from(*contract_id.hash());
        let lp_token_balance = wallet.get_asset_balance(&lp_asset_id).await?;

        let call_params = CallParameters::new(Some(lp_token_balance), Some(lp_asset_id), None);
        contract_methods
            .withdraw(wallet.address().into())
            .call_params(call_params)
            .append_variable_outputs(1)
            .call()
            .await?;

        let base_balance = wallet.get_asset_balance(&base_asset_id).await?;
        assert_eq!(base_balance, deposit_amount);

Transfer all assets

The transfer() method lets you transfer a single asset, but what if you needed to move all of your assets to a different wallet? You could repeatably call transfer(), initiating a transaction each time, or you bundle all the transfers into a single transaction. This chapter guides you through crafting your custom transaction for transferring all assets owned by a wallet.

Lets quickly go over the setup:

        let mut wallet_1 = WalletUnlocked::new_random(None);
        let mut wallet_2 = WalletUnlocked::new_random(None);

        const NUM_ASSETS: u64 = 5;
        const AMOUNT: u64 = 100_000;
        const NUM_COINS: u64 = 1;
        let (coins, _) =
            setup_multiple_assets_coins(wallet_1.address(), NUM_ASSETS, NUM_COINS, AMOUNT);

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

        wallet_1.set_provider(provider.clone());
        wallet_2.set_provider(provider.clone());

We prepare two wallets with randomized addresses. Next, we want one of our wallets to have some random assets, so we set them up with setup_multiple_assets_coins(). Having created the coins, we can start a provider and assign it to the previously created wallets.

Transactions require us to define input and output coins. Let's assume we do not know the assets owned by wallet_1. We retrieve its balances, i.e. tuples consisting of a string representing the asset id and the respective amount. This lets us use the helpers get_asset_inputs_for_amount(), get_asset_outputs_for_amount() to create the appropriate inputs and outputs.

For the sake of simplicity, we avoid transferring the base asset so we don't have to worry about transaction 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).unwrap();

            // leave the base asset to cover transaction fees
            if id == BASE_ASSET_ID {
                continue;
            }
            let input = wallet_1.get_asset_inputs_for_amount(id, amount, 0).await?;
            inputs.extend(input);

            let output = wallet_1.get_asset_outputs_for_amount(wallet_2.address(), id, amount);
            outputs.extend(output);
        }

All that is left is to build the transaction with the helper build_transfer_transaction(), have wallet_1 sign it, and we can send it. We confirm this by checking the number of balances present in the receiving wallet and their amount:

        let mut tx = Wallet::build_transfer_tx(&inputs, &outputs, TxParameters::default());
        wallet_1.sign_transaction(&mut tx).await?;

        let _receipts = provider.send_transaction(&tx).await?;

        let balances = wallet_2.get_balances().await?;

        assert_eq!(balances.len(), (NUM_ASSETS - 1) as usize);
        for (_, balance) in balances {
            assert_eq!(balance, AMOUNT);
        }

Contributing to fuels-rs

note This page is still a work in progress.

Integration tests structure in fuels-rs

The integration tests of fuels-rs cover almost all aspects of the SDK and have grown significantly as more functionality was added. To make the tests and associated Sway projects more manageable they were split into several categories. A category consist of a .rs file for the tests and, if needed, a separate directory for the Sway projects.

Currently have the following structure:

  .
  ├─  bindings/
  ├─  contracts/
  ├─  logs/
  ├─  predicates/
  ├─  storage/
  ├─  types/
  ├─  bindings.rs
  ├─  contracts.rs
  ├─  from_token.rs
  ├─  logs.rs
  ├─  predicates.rs
  ├─  providers.rs
  ├─  scripts.rs
  ├─  storage.rs
  ├─  types.rs
  └─  wallets.rs

Even though test organization is subjective, please consider these guidelines before adding a new category:

  • Add a new category when creating a new section in the Fuels Rust SDK book - e.g. Types
  • Add a new category if there are more than 3 test and more than 100 lines of code and they form a group of tests - e.g. storage.rs

Otherwise, we recommend putting the integration test inside the existing categories above.

fuels-rs Rust Workspaces

This section gives you a little overview of the role and function of every workspace in the fuels-rs repository.

fuels-abi-cli

Simple CLI program to encode Sway function calls and decode their output. The ABI being encoded and decoded is specified here.

Usage

sway-abi-cli 0.1.0
FuelVM ABI coder

USAGE:
    sway-abi-cli <SUBCOMMAND>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

SUBCOMMANDS:
    codegen   Output Rust types file
    decode    Decode ABI call result
    encode    Encode ABI call
    help      Prints this message or the help of the given subcommand(s)

Examples

You can choose to encode only the given params or you can go a step further and have a full JSON ABI file and encode the whole input to a certain function call defined in the JSON file.

Encoding params only

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

Note that for every param you want to encode, you must pass a -v flag followed by the type, and then the value: -v <type_1> <value_1> -v <type_2> <value_2> -v <type_n> <value_n>

Encoding function call

example/simple.json:

[
  {
    "type":"function",
    "inputs":[
      {
        "name":"arg",
        "type":"u32"
      }
    ],
    "name":"takes_u32_returns_bool",
    "outputs":[
      {
        "name":"",
        "type":"bool"
      }
    ]
  }
]
$ cargo run -- encode function examples/simple.json takes_u32_returns_bool -p 4
000000006355e6ee0000000000000004

example/array.json

[
  {
    "type":"function",
    "inputs":[
      {
        "name":"arg",
        "type":"u16[3]"
      }
    ],
    "name":"takes_array",
    "outputs":[
      {
        "name":"",
        "type":"u16[2]"
      }
    ]
  }
]
$ cargo run -- encode function examples/array.json takes_array -p '[1,2]'
00000000f0b8786400000000000000010000000000000002

Note that the first word (8 bytes) of the output is reserved for the function selector, which is captured in the last 4 bytes, which is simply the 256hash of the function signature.

Example with nested struct:

[
  {
    "type":"contract",
    "inputs":[
      {
        "name":"MyNestedStruct",
        "type":"struct",
        "components":[
          {
            "name":"x",
            "type":"u16"
          },
          {
            "name":"y",
            "type":"struct",
            "components":[
              {
                "name":"a",
                "type":"bool"
              },
              {
                "name":"b",
                "type":"u8[2]"
              }
            ]
          }
        ]
      }
    ],
    "name":"takes_nested_struct",
    "outputs":[

    ]
  }
]
$ cargo run -- encode function examples/nested_struct.json takes_nested_struct -p '(10, (true, [1,2]))'
00000000e8a04d9c000000000000000a000000000000000100000000000000010000000000000002

Decoding params only

Similar to encoding parameters only:

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

Decoding function output

$ cargo run -- decode function examples/simple.json takes_u32_returns_bool 0000000000000001
Bool(true)