Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/v0.66.10/.nojekyll b/v0.66.10/.nojekyll new file mode 100644 index 0000000000..f17311098f --- /dev/null +++ b/v0.66.10/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/v0.66.10/404.html b/v0.66.10/404.html new file mode 100644 index 0000000000..e3230c4137 --- /dev/null +++ b/v0.66.10/404.html @@ -0,0 +1,170 @@ + + +
+ + +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +You might have noticed this snippet in the previous sections:
+ abigen!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/contract_test/out/release/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. +In order to call your contracts, scripts or predicates, you first need to generate the Rust bindings for them.
+ +The following subsections contain more details about the abigen!
syntax and the code generated from it.
abigen!
is a procedural macro -- it generates code. It accepts inputs in the format of:
ProgramType(name="MyProgramType", abi="my_program-abi.json")...
+
+where:
+ProgramType
is one of: Contract
, Script
or Predicate
,
name
is the name that will be given to the generated bindings,
abi
is either a path to the JSON ABI file or its actual contents.
So, an abigen!
which generates bindings for two contracts and one script looks like this:
abigen!(
+ Contract(
+ name = "ContractA",
+ abi = "e2e/sway/bindings/sharing_types/contract_a/out/release/contract_a-abi.json"
+ ),
+ Contract(
+ name = "ContractB",
+ abi = "e2e/sway/bindings/sharing_types/contract_b/out/release/contract_b-abi.json"
+ ),
+ Script(
+ name = "MyScript",
+ abi = "e2e/sway/scripts/arguments/out/release/arguments-abi.json"
+ ),
+ Predicate(
+ name = "MyPredicateEncoder",
+ abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
+ ),
+ );
+
+A rough overview:
+pub mod abigen_bindings {
+ pub mod contract_a_mod {
+ struct SomeCustomStruct{/*...*/};
+ // other custom types used in the contract
+
+ struct ContractA {/*...*/};
+ impl ContractA {/*...*/};
+ // ...
+ }
+ pub mod contract_b_mod {
+ // ...
+ }
+ pub mod my_script_mod {
+ // ...
+ }
+ pub mod my_predicate_mod{
+ // ...
+ }
+ pub mod shared_types{
+ // ...
+ }
+}
+
+pub use contract_a_mod::{/*..*/};
+pub use contract_b_mod::{/*..*/};
+pub use my_predicate_mod::{/*..*/};
+pub use shared_types::{/*..*/};
+
+Each ProgramType
gets its own mod
based on the name
given in the abigen!
. Inside the respective mods, the custom types used by that program are generated, and the bindings through which the actual calls can be made.
One extra mod
called shared_types
is generated if abigen!
detects that the given programs share types. Instead of each mod
regenerating the type for itself, the type is lifted out into the shared_types
module, generated only once, and then shared between all program bindings that use it. Reexports are added to each mod so that even if a type is deemed shared, you can still access it as though each mod
had generated the type for itself (i.e. my_contract_mod::SharedType
).
A type is deemed shared if its name and definition match up. This can happen either because you've used the same library (a custom one or a type from the stdlib
) or because you've happened to define the exact same type.
Finally, pub use
statements are inserted, so you don't have to fully qualify the generated types. To avoid conflict, only types that have unique names will get a pub use
statement. If you find rustc
can't find your type, it might just be that there is another generated type with the same name. To fix the issue just qualify the path by doing abigen_bindings::whatever_contract_mod::TheType
.
++Note: +It is highly encouraged that you generate all your bindings in one
+abigen!
call. Doing it in this manner will allow type sharing and avoid name collisions you'd normally get when callingabigen!
multiple times inside the same namespace. If you choose to proceed otherwise, keep in mind the generated code overview presented above and appropriately separate theabigen!
calls into different modules to resolve the collision.
Let's look at a contract with two methods: initialize_counter(arg: u64) -> u64
and increment_counter(arg: u64) -> u64
, with the following JSON ABI:
{
+ "programType": "contract",
+ "specVersion": "1",
+ "encodingVersion": "1",
+ "concreteTypes": [
+ {
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0",
+ "type": "u64"
+ }
+ ],
+ "functions": [
+ {
+ "inputs": [
+ {
+ "name": "value",
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "name": "initialize_counter",
+ "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ },
+ {
+ "inputs": [
+ {
+ "name": "value",
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "name": "increment_counter",
+ "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "metadataTypes": []
+}
+
+By doing this:
+ use fuels::prelude::*;
+ // Replace with your own JSON abi path (relative to the root of your crate)
+ abigen!(Contract(
+ name = "MyContractName",
+ abi = "examples/rust_bindings/src/abi.json"
+ ));
+
+or this:
+ use fuels::prelude::*;
+ abigen!(Contract(
+ name = "MyContract",
+ abi = r#"
+ {
+ "programType": "contract",
+ "specVersion": "1",
+ "encodingVersion": "1",
+ "concreteTypes": [
+ {
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0",
+ "type": "u64"
+ }
+ ],
+ "functions": [
+ {
+ "inputs": [
+ {
+ "name": "value",
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "name": "initialize_counter",
+ "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ },
+ {
+ "inputs": [
+ {
+ "name": "value",
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "name": "increment_counter",
+ "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "metadataTypes": []
+ }
+ "#
+ ));
+
+you'll generate this (shortened for brevity's sake):
+pub mod abigen_bindings {
+ pub mod my_contract_mod {
+ #[derive(Debug, Clone)]
+ pub struct MyContract<A: ::fuels::accounts::Account> {
+ contract_id: ::fuels::types::bech32::Bech32ContractId,
+ account: A,
+ log_decoder: ::fuels::core::codec::LogDecoder,
+ encoder_config: ::fuels::core::codec::EncoderConfig,
+ }
+ impl<A: ::fuels::accounts::Account> MyContract<A> {
+ pub fn new(
+ contract_id: impl ::core::convert::Into<::fuels::types::bech32::Bech32ContractId>,
+ account: A,
+ ) -> Self {
+ let contract_id: ::fuels::types::bech32::Bech32ContractId = contract_id.into();
+ let log_decoder = ::fuels::core::codec::LogDecoder::new(
+ ::fuels::core::codec::log_formatters_lookup(vec![], contract_id.clone().into()),
+ );
+ let encoder_config = ::fuels::core::codec::EncoderConfig::default();
+ Self {
+ contract_id,
+ account,
+ log_decoder,
+ encoder_config,
+ }
+ }
+ pub fn contract_id(&self) -> &::fuels::types::bech32::Bech32ContractId {
+ &self.contract_id
+ }
+ pub fn account(&self) -> A {
+ self.account.clone()
+ }
+ pub fn with_account<U: ::fuels::accounts::Account>(self, account: U) -> MyContract<U> {
+ MyContract {
+ contract_id: self.contract_id,
+ account,
+ log_decoder: self.log_decoder,
+ encoder_config: self.encoder_config,
+ }
+ }
+ pub fn with_encoder_config(
+ mut self,
+ encoder_config: ::fuels::core::codec::EncoderConfig,
+ ) -> MyContract<A> {
+ self.encoder_config = encoder_config;
+ self
+ }
+ pub async fn get_balances(
+ &self,
+ ) -> ::fuels::types::errors::Result<
+ ::std::collections::HashMap<::fuels::types::AssetId, u64>,
+ > {
+ ::fuels::accounts::ViewOnlyAccount::try_provider(&self.account)?
+ .get_contract_balances(&self.contract_id)
+ .await
+ .map_err(::std::convert::Into::into)
+ }
+ pub fn methods(&self) -> MyContractMethods<A> {
+ MyContractMethods {
+ contract_id: self.contract_id.clone(),
+ account: self.account.clone(),
+ log_decoder: self.log_decoder.clone(),
+ encoder_config: self.encoder_config.clone(),
+ }
+ }
+ }
+ pub struct MyContractMethods<A: ::fuels::accounts::Account> {
+ contract_id: ::fuels::types::bech32::Bech32ContractId,
+ account: A,
+ log_decoder: ::fuels::core::codec::LogDecoder,
+ encoder_config: ::fuels::core::codec::EncoderConfig,
+ }
+ impl<A: ::fuels::accounts::Account> MyContractMethods<A> {
+ #[doc = " This method will read the counter from storage, increment it"]
+ #[doc = " and write the incremented value to storage"]
+ pub fn increment_counter(
+ &self,
+ value: ::core::primitive::u64,
+ ) -> ::fuels::programs::calls::CallHandler<
+ A,
+ ::fuels::programs::calls::ContractCall,
+ ::core::primitive::u64,
+ > {
+ ::fuels::programs::calls::CallHandler::new_contract_call(
+ self.contract_id.clone(),
+ self.account.clone(),
+ ::fuels::core::codec::encode_fn_selector("increment_counter"),
+ &[::fuels::core::traits::Tokenizable::into_token(value)],
+ self.log_decoder.clone(),
+ false,
+ self.encoder_config.clone(),
+ )
+ }
+ pub fn initialize_counter(
+ &self,
+ value: ::core::primitive::u64,
+ ) -> ::fuels::programs::calls::CallHandler<
+ A,
+ ::fuels::programs::calls::ContractCall,
+ ::core::primitive::u64,
+ > {
+ ::fuels::programs::calls::CallHandler::new_contract_call(
+ self.contract_id.clone(),
+ self.account.clone(),
+ ::fuels::core::codec::encode_fn_selector("initialize_counter"),
+ &[::fuels::core::traits::Tokenizable::into_token(value)],
+ self.log_decoder.clone(),
+ false,
+ self.encoder_config.clone(),
+ )
+ }
+ }
+ impl<A: ::fuels::accounts::Account> ::fuels::programs::calls::ContractDependency for MyContract<A> {
+ fn id(&self) -> ::fuels::types::bech32::Bech32ContractId {
+ self.contract_id.clone()
+ }
+ fn log_decoder(&self) -> ::fuels::core::codec::LogDecoder {
+ self.log_decoder.clone()
+ }
+ }
+ #[derive(Clone, Debug, Default)]
+ pub struct MyContractConfigurables {
+ offsets_with_data: ::std::vec::Vec<(u64, ::std::vec::Vec<u8>)>,
+ encoder: ::fuels::core::codec::ABIEncoder,
+ }
+ impl MyContractConfigurables {
+ pub fn new(encoder_config: ::fuels::core::codec::EncoderConfig) -> Self {
+ Self {
+ encoder: ::fuels::core::codec::ABIEncoder::new(encoder_config),
+ ..::std::default::Default::default()
+ }
+ }
+ }
+ impl From<MyContractConfigurables> for ::fuels::core::Configurables {
+ fn from(config: MyContractConfigurables) -> Self {
+ ::fuels::core::Configurables::new(config.offsets_with_data)
+ }
+ }
+ }
+}
+pub use abigen_bindings::my_contract_mod::MyContract;
+pub use abigen_bindings::my_contract_mod::MyContractConfigurables;
+pub use abigen_bindings::my_contract_mod::MyContractMethods;
+
+++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 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!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
+ ));
+
+ // This is an instance of your contract which you can use to make calls to your functions
+ let contract_instance = MyContract::new(contract_id_2, 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);
+
+
+ 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/release/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 ViewOnlyAccount
trait provides a common interface to query balances.
The Account
trait, in addition to the above, also provides a common interface to retrieve spendable resources or transfer assets. When performing actions in the SDK that lead to a transaction, you will typically need to provide an account that will be used to allocate resources required by the transaction, including transaction fees.
The traits are implemented by the following types:
+ +An account implements the following methods for transferring assets:
+transfer
force_transfer_to_contract
withdraw_to_base_layer
The following examples are provided for a Wallet
account. A Predicate
account would work similarly, but you might need to set its predicate data before attempting to spend resources owned by it.
With wallet.transfer
you can initiate a transaction to transfer an asset from your account to a target address.
use fuels::prelude::*;
+
+ // Setup 2 test wallets with 1 coin each
+ let num_wallets = 2;
+ let coins_per_wallet = 1;
+ let coin_amount = 2;
+
+ let wallets = launch_custom_provider_and_get_wallets(
+ WalletsConfig::new(Some(num_wallets), Some(coins_per_wallet), Some(coin_amount)),
+ None,
+ None,
+ )
+ .await?;
+
+ // Transfer the base asset with amount 1 from wallet 1 to wallet 2
+ let transfer_amount = 1;
+ let asset_id = Default::default();
+ let (_tx_id, _receipts) = wallets[0]
+ .transfer(
+ wallets[1].address(),
+ transfer_amount,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+ let wallet_2_final_coins = wallets[1].get_coins(AssetId::zeroed()).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
+ .try_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, TxPolicies::default())
+ .await?;
+
+ // Check that the contract now has 1 coin
+ let contract_balances = wallet
+ .try_provider()?
+ .get_contract_balances(&contract_id)
+ .await?;
+ assert_eq!(contract_balances.len(), 1);
+
+ let random_asset_balance = contract_balances.get(&random_asset_id).unwrap();
+ assert_eq!(*random_asset_balance, 300);
+
+For transferring assets to the base layer chain, you can use wallet.withdraw_to_base_layer
.
use std::str::FromStr;
+
+ use fuels::prelude::*;
+
+ let wallets = launch_custom_provider_and_get_wallets(
+ WalletsConfig::new(Some(1), None, None),
+ None,
+ None,
+ )
+ .await?;
+ let wallet = wallets.first().unwrap();
+
+ let amount = 1000;
+ let base_layer_address = Address::from_str(
+ "0x4710162c2e3a95a6faff05139150017c9e38e5e280432d546fae345d6ce6d8fe",
+ )?;
+ 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, TxPolicies::default())
+ .await?;
+
+ let _block_height = wallet.try_provider()?.produce_blocks(1, None).await?;
+
+ // Retrieve a message proof from the provider
+ let proof = wallet
+ .try_provider()?
+ .get_message_proof(&tx_id, &msg_id, None, Some(2))
+ .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 policies. Lastly, to verify that the transfer succeeded, the relevant message proof is retrieved with provider.get_message_proof,
and the amount and the recipient are verified.
To facilitate account impersonation, the Rust SDK provides the ImpersonatedAccount
struct. Since it implements Account
, we can use it to simulate ownership of assets held by an account with a given address. This also implies that we can impersonate contract calls from that address. ImpersonatedAccount
will only succeed in unlocking assets if the network is set up with utxo_validation = false
.
// create impersonator for an address
+ let address =
+ Address::from_str("0x17f46f562778f4bb5fe368eeae4985197db51d80c83494ea7f84c530172dedd1")
+ .unwrap();
+ let address = Bech32Address::from(address);
+ let impersonator = ImpersonatedAccount::new(address, Some(provider.clone()));
+
+ let contract_instance = MyContract::new(contract_id, impersonator.clone());
+
+ let response = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .call()
+ .await?;
+
+ assert_eq!(42, response.value);
+
+
+ The parameters for a contract call are:
+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.
#[payable]
+ 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_policies = TxPolicies::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::default().with_amount(1_000_000);
+
+ let response = contract_methods
+ .get_msg_amount() // Our contract method.
+ .with_tx_policies(tx_policies) // Chain the tx policies.
+ .call_params(call_params)? // Chain the call parameters.
+ .call() // Perform the contract call.
+ .await?;
+
+
+
+call_params
returns a result to ensure you don't forward assets to a contract method that isn't payable.
In the following example, we try to forward an amount of 100
of the base asset to non_payable
. As its name suggests, non_payable
isn't annotated with #[payable]
in the contract code. Passing CallParameters
with an amount other than 0
leads to an error:
let err = contract_methods
+ .non_payable()
+ .call_params(CallParameters::default().with_amount(100))
+ .expect_err("should return error");
+
+ assert!(matches!(err, Error::Other(s) if s.contains("assets forwarded to non-payable method")));
+
+++Note: forwarding gas to a contract call is always possible, regardless of the contract method being non-payable.
+
You can also use CallParameters::default()
to use the default values:
pub const DEFAULT_CALL_PARAMS_AMOUNT: u64 = 0;
+
+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 1_000_000 and `gas_forwarded` to 4300 to specify that
+ // the contract call transaction may consume up to 1_000_000 gas, while the actual call may
+ // only use 4300 gas
+ let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000);
+ let call_params = CallParameters::default().with_gas_forwarded(4300);
+
+ let response = contract_methods
+ .get_msg_amount() // Our contract method.
+ .with_tx_policies(tx_policies) // Chain the tx policies.
+ .call_params(call_params)? // Chain the call parameters.
+ .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.
You've probably noticed that you're often chaining .call().await.unwrap()
. That's because:
.call()
and .simulate()
(more on this in the next section)..await
it or perform concurrent tasks, making full use of Rust's async..unwrap()
the Result<CallResponse, Error>
returned by the contract call.Once you unwrap the CallResponse
, you have access to this struct:
pub struct CallResponse<D> {
+ pub value: D,
+ pub receipts: Vec<Receipt>,
+ pub gas_used: u64,
+ pub log_decoder: LogDecoder,
+ pub tx_id: Option<Bytes32>,
+}
+
+
+
+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).
receipts
will hold all receipts generated by that specific contract call.gas_used
is the amount of gas consumed by the contract call.tx_id
will hold the ID of the corresponding submitted transaction.You can use the is_ok
and is_err
methods to check if a contract call Result
is Ok
or contains an error. These methods will return either true
or false
.
let is_ok = response.is_ok();
+let is_error = response.is_err();
+
+
+
+
+If is_err
returns true
, you can use the unwrap_err
method to unwrap the error message.
if response.is_err() {
+ let err = response.unwrap_err();
+ println!("ERROR: {:?}", err);
+};
+
+
+
+ You can use the with_account()
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_account(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.
+
With the function estimate_transaction_cost(tolerance: Option<f64>, block_horizon: Option<u32>)
provided by CallHandler
, you can get a cost estimation for a specific call. The return type, TransactionCost
, is a struct that contains relevant information for the estimation:
pub struct TransactionCost {
+ pub gas_price: u64,
+ pub gas_used: u64,
+ pub metered_bytes_size: u64,
+ pub total_fee: u64,
+}
+
+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 = Some(0.0);
+ let block_horizon = Some(1);
+ let transaction_cost = contract_instance
+ .methods()
+ .initialize_counter(42) // Build the ABI call
+ .estimate_transaction_cost(tolerance, block_horizon) // Get estimated transaction cost
+ .await?;
+
+ let call_handler_1 = contract_methods.initialize_counter(42);
+ let call_handler_2 = contract_methods.get_array([42; 2]);
+
+ let multi_call_handler = CallHandler::new_multi_call(wallet.clone())
+ .add_call(call_handler_1)
+ .add_call(call_handler_2);
+
+ let tolerance = Some(0.0);
+ let block_horizon = Some(1);
+ let transaction_cost = multi_call_handler
+ .estimate_transaction_cost(tolerance, block_horizon) // 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.
+++ +Note The same estimation interface is available for scripts.
+
The SDK provides the option to transfer assets within the same transaction, when making a contract call. By using add_custom_asset()
you specify the asset ID, the amount, and the destination address:
let amount = 1000;
+ let _ = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .add_custom_asset(
+ AssetId::zeroed(),
+ amount,
+ Some(other_wallet.address().clone()),
+ )
+ .call()
+ .await?;
+
+
+ Once you've deployed your contract, as seen in the previous sections, you'll likely want to:
+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 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!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
+ ));
+
+ // This is an instance of your contract which you can use to make calls to your functions
+ let contract_instance = MyContract::new(contract_id_2, 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.
+Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows:
+ let response = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .submit()
+ .await?;
+
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let value = response.response().await?.value;
+
+
+Next, we'll see how we can further configure the many different parameters in a contract call.
+ +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] = __to_str_array("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 decode_logs_with_type::<T>
from a CallResponse
, 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.decode_logs_with_type::<u64>()?;
+ let log_bits256 = response.decode_logs_with_type::<Bits256>()?;
+ let log_string = response.decode_logs_with_type::<SizedAsciiString<4>>()?;
+ let log_array = response.decode_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 use the decode_logs()
function to retrieve a LogResult
struct containing a results
field that is a vector of Result<String>
values representing the success or failure of decoding each log.
let contract_methods = contract_instance.methods();
+ let response = contract_methods.produce_multiple_logs().call().await?;
+ let logs = response.decode_logs();
+
+Due to possible performance hits, it is not recommended to use decode_logs()
outside of a debugging scenario.
++ +Note: String slices cannot be logged directly. Use the
+__to_str_array()
function to convert it to astr[N]
first.
With low-level calls, you can specify the parameters of your calls at runtime and make indirect calls through other contracts.
+Your caller contract should call std::low_level_call::call_with_function_selector
, providing:
Bytes
Bytes
u64
)std::low_level_call::CallParams
fn call_low_level_call(
+ target: ContractId,
+ function_selector: Bytes,
+ calldata: Bytes,
+ ) {
+ let call_params = CallParams {
+ coins: 0,
+ asset_id: AssetId::from(ZERO_B256),
+ gas: 10_000,
+ };
+
+ call_with_function_selector(target, function_selector, calldata, call_params);
+ }
+
+On the SDK side, you can construct an encoded function selector using fuels::core::encode_fn_selector
, and encoded calldata using the fuels::core::calldata!
macro.
E.g. to call the following function on the target contract:
+ #[storage(write)]
+ fn set_value_multiple_complex(a: MyStruct, b: str[4]);
+
+you would construct the function selector and the calldata as such, and provide them to the caller contract (like the one above):
+ let function_selector = encode_fn_selector("set_value_multiple_complex");
+ let call_data = calldata!(
+ MyStruct {
+ a: true,
+ b: [1, 2, 3],
+ },
+ SizedAsciiString::<4>::try_from("fuel")?
+ )?;
+
+ caller_contract_instance
+ .methods()
+ .call_low_level_call(
+ target_contract_instance.id(),
+ Bytes(function_selector),
+ Bytes(call_data),
+ )
+ .determine_missing_contracts(None)
+ .await?
+ .call()
+ .await?;
+
+++ +Note: the
+calldata!
macro uses the defaultEncoderConfig
configuration under the hood.
With CallHandler
, 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 CallHandler
and optionally configure transaction policies:
let multi_call_handler = CallHandler::new_multi_call(wallet.clone())
+ .add_call(call_handler_1)
+ .add_call(call_handler_2);
+
+++Note: any transaction policies configured on separate contract calls are disregarded in favor of the parameters provided to the multi-call
+CallHandler
.
Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows:
+ let submitted_tx = multi_call_handler.submit().await?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let (counter, array): (u64, [u64; 2]) = submitted_tx.response().await?.value;
+
+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 CallResponse
by moving the type annotation to the invoked method:
let response = multi_call_handler.call::<(u64, [u64; 2])>().await?;
+
+
+ If your contract method is calling other contracts you will have to add the appropriate Inputs
and Outputs
to your transaction. For your convenience, the CallHandler
provides methods that prepare those inputs and outputs for you. You have two methods that you can use: with_contracts(&[&contract_instance, ...])
and with_contract_ids(&[&contract_id, ...])
.
with_contracts(&[&contract_instance, ...])
requires contract instances that were created using the abigen
macro. When setting the external contracts with this method, logs and require revert errors originating from the external contract can be propagated and decoded by the calling contract.
let response = contract_caller_instance
+ .methods()
+ .increment_from_contract(lib_contract_id, 42)
+ .with_contracts(&[&lib_contract_instance])
+ .call()
+ .await?;
+
+If however, you do not need to decode logs or you do not have a contract instance that was generated using the abigen
macro you can use with_contract_ids(&[&contract_id, ...])
and provide the required contract ids.
let response = contract_caller_instance
+ .methods()
+ .increment_from_contract(lib_contract_id, 42)
+ .with_contract_ids(&[lib_contract_id.clone()])
+ .call()
+ .await?;
+
+
+ Sometimes you want to simulate a call to a contract without changing the state of the blockchain. This can be achieved by calling .simulate
instead of .call
and passing in the desired execution context:
.simulate(Execution::Realistic)
simulates the transaction in a manner that closely resembles a real call. You need a wallet with base assets to cover the transaction cost, even though no funds will be consumed. This is useful for validating that a real call would succeed if made at that moment. It allows you to debug issues with your contract without spending gas. // you would mint 100 coins if the transaction wasn't simulated
+ let counter = contract_methods
+ .mint_coins(100)
+ .simulate(Execution::Realistic)
+ .await?;
+
+.simulate(Execution::StateReadOnly)
disables many validations, adds fake gas, extra variable outputs, blank witnesses, etc., enabling you to read state even with an account that has no funds. // you don't need any funds to read state
+ let balance = contract_methods
+ .get_balance(contract_id, AssetId::zeroed())
+ .simulate(Execution::StateReadOnly)
+ .await?
+ .value;
+
+
+ Previously, we mentioned that a contract call might require you to manually specify external contracts, variable outputs, or output messages. The SDK can also attempt to estimate and set these dependencies for you at the cost of running multiple simulated calls in the background.
+The following example uses a contract call that calls an external contract and later mints assets to a specified address. Calling it without including the dependencies will result in a revert:
+ let address = wallet.address();
+ let amount = 100;
+
+ let response = contract_methods
+ .mint_then_increment_from_contract(called_contract_id, amount, address.into())
+ .call()
+ .await;
+
+ assert!(matches!(
+ response,
+ Err(Error::Transaction(Reason::Reverted { .. }))
+ ));
+
+As mentioned in previous chapters, you can specify the external contract and add an output variable to resolve this:
+ let response = contract_methods
+ .mint_then_increment_from_contract(called_contract_id, amount, address.into())
+ .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
+ .with_contract_ids(&[called_contract_id.into()])
+ .call()
+ .await?;
+
+But this requires you to know the contract ID of the external contract and the needed number of output variables. Alternatively, by chaining .estimate_tx_dependencies()
instead, the dependencies will be estimated by the SDK and set automatically. The optional parameter is the maximum number of simulation attempts:
let response = contract_methods
+ .mint_then_increment_from_contract(called_contract_id, amount, address.into())
+ .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
+ .determine_missing_contracts(Some(2))
+ .await?
+ .call()
+ .await?;
+
+The minimal number of attempts corresponds to the number of external contracts and output variables needed and defaults to 10.
+++ +Note:
+estimate_tx_dependencies()
can also be used when working with script calls or multi calls.estimate_tx_dependencies()
does not currently resolve the dependencies needed for logging from an external contract. For more information, see here. If no resolution was found after exhausting all simulation attempts, the last received error will be propagated. The same will happen if an error is unrelated to transaction dependencies.
Transaction policies are defined as follows:
+pub struct TxPolicies {
+ tip: Option<u64>,
+ witness_limit: Option<u64>,
+ maturity: Option<u64>,
+ max_fee: Option<u64>,
+ script_gas_limit: Option<u64>,
+}
+
+Where:
+When the Script Gas Limit is not set, the Rust SDK will estimate the consumed gas in the background and set it as the limit.
+If the Witness Limit is not set, the SDK will set it to the size of all witnesses and signatures defined in the transaction builder.
+You can configure these parameters by creating an instance of TxPolicies
and passing it to a chain method called with_tx_policies
:
let contract_methods = MyContract::new(contract_id.clone(), wallet.clone()).methods();
+
+ let tx_policies = TxPolicies::default()
+ .with_tip(1)
+ .with_script_gas_limit(1_000_000)
+ .with_maturity(0);
+
+ let response = contract_methods
+ .initialize_counter(42) // Our contract method
+ .with_tx_policies(tx_policies) // Chain the tx policies
+ .call() // Perform the contract call
+ .await?; // This is an async call, `.await` it.
+
+
+
+You can also use TxPolicies::default()
to use the default values.
This way:
+ let response = contract_methods
+ .initialize_counter(42)
+ .with_tx_policies(TxPolicies::default())
+ .call()
+ .await?;
+
+As you might have noticed, TxPolicies
can also be specified when deploying contracts or transferring assets by passing it to the respective methods.
Sometimes, the contract you call might transfer funds to a specific address, depending on its execution. The underlying transaction for such a contract call has to have the appropriate number of variable outputs to succeed.
+ +Let's say you deployed a contract with the following method:
+ fn transfer(coins: u64, asset_id: AssetId, recipient: Identity) {
+ transfer(recipient, asset_id, coins);
+ }
+
+When calling transfer_coins_to_output
with the SDK, you can specify the number of variable outputs:
let address = wallet.address();
+ let asset_id = contract_id.asset_id(&Bits256::zeroed());
+
+ // withdraw some tokens to wallet
+ let response = contract_methods
+ .transfer(1_000_000, asset_id, address.into())
+ .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
+ .call()
+ .await?;
+
+
+
+with_variable_output_policy
sets the policy regarding variable outputs. You can either set the number of variable outputs yourself by providing VariableOutputPolicy::Exactly(n)
or let the SDK estimate it for you with VariableOutputPolicy::EstimateMinimum
. A variable output indicates that the amount and the owner may vary based on transaction execution.
++ +Note: that the Sway
+lib-std
functionmint_to_address
callstransfer_to_address
under the hood, so you need to callwith_variable_output_policy
in the Rust SDK tests like you would fortransfer_to_address
.
fuels-abi-cli
Simple CLI program to encode Sway function calls and decode their output. The ABI being encoded and decoded is specified here.
+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)
+
+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.
+$ 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 parameter 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>
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
+
+Similar to encoding parameters only:
+$ cargo run -- decode params -t bool -t u32 -t u32 0000000000000001000000000000002a0000000000000064
+Bool(true)
+U32(42)
+U32(100)
+
+$ cargo run -- decode function examples/simple.json takes_u32_returns_bool 0000000000000001
+Bool(true)
+
+
+ fuels-rs
Rust WorkspacesThis section gives you a little overview of the role and function of every workspace in the fuels-rs
repository.
Be sure to read the prerequisites to decoding.
+Decoding is done via the ABIDecoder
:
use fuels::{
+ core::{
+ codec::ABIDecoder,
+ traits::{Parameterize, Tokenizable},
+ },
+ macros::{Parameterize, Tokenizable},
+ types::Token,
+ };
+
+ #[derive(Parameterize, Tokenizable)]
+ struct MyStruct {
+ field: u64,
+ }
+
+ let bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 101];
+
+ let token: Token = ABIDecoder::default().decode(&MyStruct::param_type(), bytes)?;
+
+ let _: MyStruct = MyStruct::from_token(token)?;
+
+First into a Token
, then via the Tokenizable
trait, into the desired type.
If the type came from abigen!
(or uses the ::fuels::macros::TryFrom
derivation) then you can also use try_into
to convert bytes into a type that implements both Parameterize
and Tokenizable
:
use fuels::macros::{Parameterize, Tokenizable, TryFrom};
+
+ #[derive(Parameterize, Tokenizable, TryFrom)]
+ struct MyStruct {
+ field: u64,
+ }
+
+ let bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 101];
+
+ let _: MyStruct = bytes.try_into()?;
+
+Under the hood, try_from_bytes
is being called, which does what the preceding example did.
The decoder can be configured to limit its resource expenditure:
+
+ use fuels::core::codec::ABIDecoder;
+
+ ABIDecoder::new(DecoderConfig {
+ max_depth: 5,
+ max_tokens: 100,
+ });
+
+
+
+For an explanation of each configuration value visit the DecoderConfig
.
The default values for the DecoderConfig
are:
impl Default for DecoderConfig {
+ fn default() -> Self {
+ Self {
+ max_depth: 45,
+ max_tokens: 10_000,
+ }
+ }
+}
+
+You can also configure the decoder used to decode the return value of the contract method:
+ let _ = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .with_decoder_config(DecoderConfig {
+ max_depth: 10,
+ max_tokens: 2_000,
+ })
+ .call()
+ .await?;
+
+The same method is available for script calls.
+ +Be sure to read the prerequisites to encoding.
+Encoding is done via the ABIEncoder
:
use fuels::{
+ core::{codec::ABIEncoder, traits::Tokenizable},
+ macros::Tokenizable,
+ };
+
+ #[derive(Tokenizable)]
+ struct MyStruct {
+ field: u64,
+ }
+
+ let instance = MyStruct { field: 101 };
+ let _encoded: Vec<u8> = ABIEncoder::default().encode(&[instance.into_token()])?;
+
+There is also a shortcut-macro that can encode multiple types which implement Tokenizable
:
use fuels::{core::codec::calldata, macros::Tokenizable};
+
+ #[derive(Tokenizable)]
+ struct MyStruct {
+ field: u64,
+ }
+ let _: Vec<u8> = calldata!(MyStruct { field: 101 }, MyStruct { field: 102 })?;
+
+The encoder can be configured to limit its resource expenditure:
+ use fuels::core::codec::ABIEncoder;
+
+ ABIEncoder::new(EncoderConfig {
+ max_depth: 5,
+ max_tokens: 100,
+ });
+
+The default values for the EncoderConfig
are:
impl Default for EncoderConfig {
+ fn default() -> Self {
+ Self {
+ max_depth: 45,
+ max_tokens: 10_000,
+ }
+ }
+}
+
+You can also configure the encoder used to encode the arguments of the contract method:
+ let _ = contract_instance
+ .with_encoder_config(EncoderConfig {
+ max_depth: 10,
+ max_tokens: 2_000,
+ })
+ .methods()
+ .initialize_counter(42)
+ .call()
+ .await?;
+
+The same method is available for script calls.
+ +Encoding and decoding are done as per the fuel spec. To this end, fuels
makes use of the ABIEncoder
and the ABIDecoder
.
To encode a type, you must first convert it into a Token
. This is commonly done by implementing the Tokenizable
trait.
To decode, you also need to provide a ParamType
describing the schema of the type in question. This is commonly done by implementing the Parameterize trait.
All types generated by the abigen!
macro implement both the Tokenizable
and Parameterize
traits.
fuels
also contains implementations for:
Tokenizable
for the fuels
-owned types listed here as well as for some foreign types (such as u8
, u16
, std::vec::Vec<T: Tokenizable>
, etc.).Parameterize
for the fuels
-owned types listed here as well as for some foreign types (such as u8
, u16
, std::vec::Vec<T: Parameterize>
, etc.).Both Tokenizable
and Parameterize
can be derived for struct
s and enum
s if all inner types implement the derived traits:
use fuels::macros::{Parameterize, Tokenizable};
+
+ #[derive(Parameterize, Tokenizable)]
+ struct MyStruct {
+ field_a: u8,
+ }
+
+ #[derive(Parameterize, Tokenizable)]
+ enum SomeEnum {
+ A(MyStruct),
+ B(Vec<u64>),
+ }
+
+++Note: +Deriving
+Tokenizable
onenum
s requires that all variants also implementParameterize
.
The derived code expects that the fuels
package is accessible through ::fuels
. If this is not the case then the derivation macro needs to be given the locations of fuels::types
and fuels::core
.
#[derive(Parameterize, Tokenizable)]
+ #[FuelsCorePath = "fuels_core_elsewhere"]
+ #[FuelsTypesPath = "fuels_types_elsewhere"]
+ pub struct SomeStruct {
+ field_a: u64,
+ }
+
+If you want no-std
generated code:
use fuels::macros::{Parameterize, Tokenizable};
+ #[derive(Parameterize, Tokenizable)]
+ #[NoStd]
+ pub struct SomeStruct {
+ field_a: u64,
+ }
+
+
+ We can interact with the Testnet
node by using the following example.
use std::str::FromStr;
+
+ use fuels::{crypto::SecretKey, prelude::*};
+
+ // Create a provider pointing to the testnet.
+ let provider = Provider::connect("testnet.fuel.network").await.unwrap();
+
+ // Setup a private key
+ let secret = SecretKey::from_str(
+ "a1447cd75accc6b71a976fd3401a1f6ce318d27ba660b0315ee6ac347bf39568",
+ )?;
+
+ // 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());
+
+++For detailed information about various testnet networks and their optimal toolchain configurations for your project, please visit the following link:
+ +
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
+ +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 Testnet at
+ +
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(format!("127.0.0.1:{port}")).await?;
+
+
+ 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:
fuel-core
) and instantiate a provider that points to that node's IP and port.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.
+ + +Once you set up a provider, you can 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.
+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(),
+ AssetId::zeroed(),
+ number_of_coins,
+ amount_per_coin,
+ );
+
+ let retry_config = RetryConfig::new(3, Backoff::Fixed(Duration::from_secs(2)))?;
+ let provider = setup_test_provider(coins.clone(), vec![], None, None)
+ .await?
+ .with_retry_config(retry_config);
+
+This method returns all unspent coins (of a given asset ID) from a wallet.
+ let coins = provider
+ .get_coins(wallet.address(), *provider.base_asset_id())
+ .await?;
+ assert_eq!(coins.len(), 1);
+
+The following example shows how to fetch resources owned by an address. First, you create a ResourceFilter
which specifies the target address, asset ID, and amount. You can also define UTXO IDs and message IDs that should be excluded when retrieving the resources:
pub struct ResourceFilter {
+ pub from: Bech32Address,
+ pub asset_id: Option<AssetId>,
+ pub amount: u64,
+ pub excluded_utxos: Vec<UtxoId>,
+ pub excluded_message_nonces: Vec<Nonce>,
+}
+
+The example uses default values for the asset ID and the exclusion lists. This resolves to the base asset ID and empty vectors for the ID lists respectively:
+ let filter = ResourceFilter {
+ from: wallet.address().clone(),
+ amount: 1,
+ ..Default::default()
+ };
+ let spendable_resources = provider.get_spendable_resources(filter).await?;
+ assert_eq!(spendable_resources.len(), 1);
+
+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?;
+
+
+ The Provider
can be configured to retry a request upon receiving a io::Error
.
++Note: Currently all node errors are received as
+io::Error
s. So, if configured, a retry will happen even if, for example, a transaction failed to verify.
We can configure the number of retry attempts and the retry strategy as detailed below.
+RetryConfig
The retry behavior can be altered by giving a custom RetryConfig
. It allows for configuring the maximum number of attempts and the interval strategy used.
#[derive(Clone, Debug)]
+pub struct RetryConfig {
+ max_attempts: NonZeroU32,
+ interval: Backoff,
+}
+
+ let retry_config = RetryConfig::new(3, Backoff::Fixed(Duration::from_secs(2)))?;
+ let provider = setup_test_provider(coins.clone(), vec![], None, None)
+ .await?
+ .with_retry_config(retry_config);
+
+Backoff
Backoff
defines different strategies for managing intervals between retry attempts.
+Each strategy allows you to customize the waiting time before a new attempt based on the number of attempts made.
Linear(Duration)
: Default
Increases the waiting time linearly with each attempt.Exponential(Duration)
: Doubles the waiting time with each attempt.Fixed(Duration)
: Uses a constant waiting time between attempts.#[derive(Debug, Clone)]
+pub enum Backoff {
+ Linear(Duration),
+ Exponential(Duration),
+ Fixed(Duration),
+}
+
+
+ RocksDB enables the preservation of the blockchain's state locally, facilitating its future utilization.
+To create or use a local database, follow these instructions:
+ let provider_config = NodeConfig {
+ database_type: DbType::RocksDb(Some(PathBuf::from("/tmp/.spider/db"))),
+ ..NodeConfig::default()
+ };
+
+++ +Note: If the specified database does not exist, a new database will be created at that path. To utilize the code snippets above, either the
+fuel-core
binary must be present, or both thefuel-core-lib
androcksdb
features need to be enabled.
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::prelude::{FuelService, Provider};
+
+ // Run the fuel node.
+ let server = FuelService::start(
+ NodeConfig::default(),
+ ChainConfig::default(),
+ StateConfig::default(),
+ )
+ .await?;
+
+ // Create a client that will talk to the node created above.
+ let client = Provider::from(server.bound_address()).await?;
+ assert!(client.healthy().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 = 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?;
+
+The fuel-core-lib
feature allows 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.66.10", features = ["fuel-core-lib"] }
+
+The rocksdb
is an additional feature that, when combined with fuel-core-lib
, provides persistent storage capabilities while using fuel-core
as a library.
fuels = { version = "0.66.10", features = ["rocksdb"] }
+
+
+ Thanks for your interest in contributing to the Fuel Rust SDK!
+This document outlines the process for installing dependencies, setting up for development, and conventions for contributing.`
+If you run into any difficulties getting started, you can always ask questions on our Discourse.
+You may contribute to the project in many ways, some of which involve coding knowledge and some which do not. A few examples include:
+Check out our Help Wanted or Good First Issues to find a suitable task.
+If you are planning something big, for example, changes related to multiple components or changes to current behaviors, make sure to open an issue to discuss with us before starting on the implementation.
+This is a rough outline of what a contributor's workflow looks like:
+Thanks for your contributions!
+Pull requests should be linked to at least one issue in the same repo.
+If the pull request resolves the relevant issues, and you want GitHub to close these issues automatically after it merged into the default branch, you can use the syntax (KEYWORD #ISSUE-NUMBER
) like this:
close #123
+
+If the pull request links an issue but does not close it, you can use the keyword ref
like this:
ref #456
+
+Multiple issues should use full syntax for each issue and be separated by a comma, like:
+close #123, ref #456
+
+
+ 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:
+Fuels Rust SDK
book - e.g. Types
storage.rs
Otherwise, we recommend putting the integration test inside the existing categories above.
+ +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
and ChainConfig
:
use fuels::{
+ prelude::*,
+ tx::{ConsensusParameters, FeeParameters, TxParameters},
+ };
+
+Next, we can define some values for the consensus parameters:
+ let tx_params = TxParameters::default()
+ .with_max_gas_per_tx(1_000)
+ .with_max_inputs(2);
+ let fee_params = FeeParameters::default().with_gas_price_factor(10);
+
+ let mut consensus_parameters = ConsensusParameters::default();
+ consensus_parameters.set_tx_params(tx_params);
+ consensus_parameters.set_fee_params(fee_params);
+
+ let chain_config = ChainConfig {
+ consensus_parameters,
+ ..ChainConfig::default()
+ };
+
+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_provider()
, which starts a node with the given configurations and returns a
+provider attached to that node:
let node_config = NodeConfig::default();
+ let _provider =
+ setup_test_provider(coins, vec![], Some(node_config), Some(chain_config)).await?;
+
+
+ Consider the following contract:
+contract;
+
+use std::{
+ asset::{
+ mint_to,
+ transfer,
+ },
+ call_frames::{
+ msg_asset_id,
+ },
+ constants::ZERO_B256,
+ context::msg_amount,
+};
+
+abi LiquidityPool {
+ #[payable]
+ fn deposit(recipient: Identity);
+ #[payable]
+ fn withdraw(recipient: Identity);
+}
+
+const BASE_TOKEN: AssetId = AssetId::from(0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c);
+
+impl LiquidityPool for Contract {
+ #[payable]
+ fn deposit(recipient: Identity) {
+ assert(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(recipient, ZERO_B256, amount_to_mint);
+ }
+
+ #[payable]
+ fn withdraw(recipient: Identity) {
+ assert(0 < msg_amount());
+
+ // Amount to withdraw.
+ let amount_to_transfer = msg_amount() / 2;
+
+ // Transfer base token to recipient.
+ transfer(recipient, BASE_TOKEN, amount_to_transfer);
+ }
+}
+
+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!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/liquidity_pool/out/release/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()?;
+
+ let asset_ids = [AssetId::zeroed(), 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::load_from(
+ "../../e2e/sway/contracts/liquidity_pool/out/release/liquidity_pool.bin",
+ LoadConfiguration::default(),
+ )?
+ .deploy(wallet, TxPolicies::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::default()
+ .with_amount(deposit_amount)
+ .with_asset_id(base_asset_id);
+
+ contract_methods
+ .deposit(wallet.address().into())
+ .call_params(call_params)?
+ .with_variable_output_policy(VariableOutputPolicy::Exactly(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 = contract_id.asset_id(&Bits256::zeroed());
+ let lp_token_balance = wallet.get_asset_balance(&lp_asset_id).await?;
+
+ let call_params = CallParameters::default()
+ .with_amount(lp_token_balance)
+ .with_asset_id(lp_asset_id);
+
+ contract_methods
+ .withdraw(wallet.address().into())
+ .call_params(call_params)?
+ .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
+ .call()
+ .await?;
+
+ let base_balance = wallet.get_asset_balance(&base_asset_id).await?;
+ assert_eq!(base_balance, deposit_amount);
+
+
+ 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.
+
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.
We transfer only a part of the base asset balance so that the rest can cover 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)?;
+
+ let input = wallet_1
+ .get_asset_inputs_for_amount(id, amount, None)
+ .await?;
+ inputs.extend(input);
+
+ // we don't transfer the full base asset so we can cover fees
+ let output = if id == *provider.base_asset_id() {
+ wallet_1.get_asset_outputs_for_amount(wallet_2.address(), id, amount / 2)
+ } else {
+ wallet_1.get_asset_outputs_for_amount(wallet_2.address(), id, amount)
+ };
+
+ outputs.extend(output);
+ }
+
+All that is left is to build the transaction via ScriptTransactionBuilder
, 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 tb =
+ ScriptTransactionBuilder::prepare_transfer(inputs, outputs, TxPolicies::default());
+ tb.add_signer(wallet_1.clone())?;
+
+ let tx = tb.build(&provider).await?;
+
+ provider.send_transaction_and_await_commit(tx).await?;
+
+ let balances = wallet_2.get_balances().await?;
+
+ assert_eq!(balances.len(), NUM_ASSETS as usize);
+ for (id, balance) in balances {
+ if id == provider.base_asset_id().to_string() {
+ assert_eq!(balance, AMOUNT / 2);
+ } else {
+ assert_eq!(balance, AMOUNT);
+ }
+ }
+
+
+ When preparing a contract call via CallHandler
, the Rust SDK uses a transaction builder in the background. You can fetch this builder and customize it before submitting it to the network. After the transaction is executed successfully, you can use the corresponding CallHandler
to generate a call response. The call response can be used to decode return values and logs. Below are examples for both contract and script calls.
let call_handler = contract_instance.methods().initialize_counter(counter);
+
+ let mut tb = call_handler.transaction_builder().await?;
+
+ // customize the builder...
+
+ wallet.adjust_for_fee(&mut tb, 0).await?;
+ tb.add_signer(wallet.clone())?;
+
+ let tx = tb.build(provider).await?;
+
+ let tx_id = provider.send_transaction(tx).await?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
+
+ let tx_status = provider.tx_status(&tx_id).await?;
+
+ let response = call_handler.get_response_from(tx_status)?;
+
+ assert_eq!(counter, response.value);
+
+ let script_call_handler = script_instance.main(1, 2);
+
+ let mut tb = script_call_handler.transaction_builder().await?;
+
+ // customize the builder...
+
+ wallet.adjust_for_fee(&mut tb, 0).await?;
+ tb.add_signer(wallet.clone())?;
+
+ let tx = tb.build(provider).await?;
+
+ let tx_id = provider.send_transaction(tx).await?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let tx_status = provider.tx_status(&tx_id).await?;
+
+ let response = script_call_handler.get_response_from(tx_status)?;
+
+ assert_eq!(response.value, "hello");
+
+
+ Until now, we have used helpers to create transactions, send them with a provider, and parse the results. However, sometimes we must make custom transactions with specific inputs, outputs, witnesses, etc. In the next chapter, we will show how to use the Rust SDKs transaction builders to accomplish this.
+ +The Rust SDK simplifies the creation of Create and Script transactions through two handy builder structs CreateTransactionBuilder
, ScriptTransactionBuilder
, and the TransactionBuilder
trait.
Calling build(&provider)
on a builder will result in the corresponding CreateTransaction
or ScriptTransaction
that can be submitted to the network.
++Note This section contains additional information about the inner workings of the builders. If you are just interested in how to use them, you can skip to the next section.
+
The builders take on the heavy lifting behind the scenes, offering two standout advantages: handling predicate data offsets and managing witness indexing.
+When your transaction involves predicates with dynamic data as inputs, like vectors, the dynamic data contains a pointer pointing to the beginning of the raw data. This pointer's validity hinges on the order of transaction inputs, and any shifting could render it invalid. However, the transaction builders conveniently postpone the resolution of these pointers until you finalize the build process.
+Similarly, adding signatures for signed coins requires the signed coin input to hold an index corresponding to the signature in the witnesses array. These indexes can also become invalid if the witness order changes. The Rust SDK again defers the resolution of these indexes until the transaction is finalized. It handles the assignment of correct index witnesses behind the scenes, sparing you the hassle of dealing with indexing intricacies during input definition.
+Another added benefit of the builder pattern is that it guards against changes once the transaction is finalized. The transactions resulting from a builder don't permit any changes to the struct that could cause the transaction ID to be modified. This eliminates the headache of calculating and storing a transaction ID for future use, only to accidentally modify the transaction later, resulting in a different transaction ID.
+Here is an example outlining some of the features of the transaction builders.
+In this scenario, we have a predicate that holds some bridged asset with ID bridged_asset_id. It releases it's locked assets if the transaction sends ask_amount of the base asset to the receiver address:
+ let ask_amount = 100;
+ let locked_amount = 500;
+ let bridged_asset_id = AssetId::from([1u8; 32]);
+ let receiver = Bech32Address::from_str(
+ "fuel1p8qt95dysmzrn2rmewntg6n6rg3l8ztueqafg5s6jmd9cgautrdslwdqdw",
+ )?;
+
+Our goal is to create a transaction that will use our hot wallet to transfer the ask_amount to the receiver and then send the unlocked predicate assets to a second wallet that acts as our cold storage.
+Let's start by instantiating a builder. Since we don't plan to deploy a contract, the ScriptTransactionBuilder
is the appropriate choice:
let tb = ScriptTransactionBuilder::default();
+
+Next, we need to define transaction inputs of the base asset that sum up to ask_amount. We also need transaction outputs that will assign those assets to the predicate address and thereby unlock it. The methods get_asset_inputs_for_amount
and get_asset_outputs_for_amount
can help with that. We need to specify the asset ID, the target amount, and the target address:
let base_inputs = hot_wallet
+ .get_asset_inputs_for_amount(*provider.base_asset_id(), ask_amount, None)
+ .await?;
+ let base_outputs = hot_wallet.get_asset_outputs_for_amount(
+ &receiver,
+ *provider.base_asset_id(),
+ ask_amount,
+ );
+
+Let's repeat the same process but this time for transferring the assets held by the predicate to our cold storage:
+ let other_asset_inputs = predicate
+ .get_asset_inputs_for_amount(bridged_asset_id, locked_amount, None)
+ .await?;
+ let other_asset_outputs =
+ predicate.get_asset_outputs_for_amount(cold_wallet.address(), bridged_asset_id, 500);
+
+We combine all of the inputs and outputs and set them on the builder:
+ let inputs = base_inputs
+ .into_iter()
+ .chain(other_asset_inputs.into_iter())
+ .collect();
+ let outputs = base_outputs
+ .into_iter()
+ .chain(other_asset_outputs.into_iter())
+ .collect();
+
+ let mut tb = tb.with_inputs(inputs).with_outputs(outputs);
+
+As we have used coins that require a signature, we have to add the signer to the transaction builder with:
+ tb.add_signer(hot_wallet.clone())?;
+
+++Note The signature is not created until the transaction is finalized with
+build(&provider)
We need to do one more thing before we stop thinking about transaction inputs. Executing the transaction also incurs a fee that is paid with the base asset. Our base asset inputs need to be large enough so that the total amount covers the transaction fee and any other operations we are doing. The Account
trait lets us use adjust_for_fee()
for adjusting the transaction inputs if needed to cover the fee. The second argument to adjust_for_fee()
is the total amount of the base asset that we expect our transaction to spend regardless of fees. In our case, this is the ask_amount we are transferring to the predicate.
hot_wallet.adjust_for_fee(&mut tb, 100).await?;
+
+++Note It is recommended to add signers before calling
+adjust_for_fee()
as the estimation will include the size of the witnesses.
We can also define transaction policies. For example, we can limit the gas price by doing the following:
+ let tx_policies = TxPolicies::default().with_tip(1);
+ let tb = tb.with_tx_policies(tx_policies);
+
+Our builder needs a signature from the hot wallet to unlock its coins before we call build()
and submit the resulting transaction through the provider:
let tx = tb.build(&provider).await?;
+ let tx_id = provider.send_transaction(tx).await?;
+
+Finally, we verify the transaction succeeded and that the cold storage indeed holds the bridged asset now:
+ let status = provider.tx_status(&tx_id).await?;
+ assert!(matches!(status, TxStatus::Success { .. }));
+
+ let balance = cold_wallet.get_asset_balance(&bridged_asset_id).await?;
+ assert_eq!(balance, locked_amount);
+
+If you need to build the transaction without signatures, which is useful when estimating transaction costs or simulations, you can change the build strategy used:
+ let mut tx = tb
+ .with_build_strategy(ScriptBuildStrategy::NoSignatures)
+ .build(provider)
+ .await?;
+ tx.sign_with(&wallet, provider.chain_id()).await?;
+
+++ +Note In contrast to adding signers to a transaction builder, when signing a built transaction, you must ensure that the order of signatures matches the order of signed inputs. Multiple signed inputs with the same owner will have the same witness index.
+
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 selector = encode_fn_selector(fn_name);
+
+ assert_eq!(
+ selector,
+ [0, 0, 0, 0, 0, 0, 0, 12, 115, 111, 109, 101, 95, 102, 110, 95, 110, 97, 109, 101]
+ );
+
+
+ ++ + +note This section is still a work in progress.
+
In Sway, you can define configurable
constants which can be changed during the contract deployment in the SDK. Here is an example how the constants are defined.
contract;
+
+#[allow(dead_code)]
+enum EnumWithGeneric<D> {
+ VariantOne: D,
+ VariantTwo: (),
+}
+
+struct StructWithGeneric<D> {
+ field_1: D,
+ field_2: u64,
+}
+
+configurable {
+ BOOL: bool = true,
+ U8: u8 = 8,
+ U16: u16 = 16,
+ U32: u32 = 32,
+ U64: u64 = 63,
+ U256: u256 = 0x0000000000000000000000000000000000000000000000000000000000000008u256,
+ B256: b256 = 0x0101010101010101010101010101010101010101010101010101010101010101,
+ STR_4: str[4] = __to_str_array("fuel"),
+ TUPLE: (u8, bool) = (8, true),
+ ARRAY: [u32; 3] = [253, 254, 255],
+ STRUCT: StructWithGeneric<u8> = StructWithGeneric {
+ field_1: 8,
+ field_2: 16,
+ },
+ ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true),
+}
+//U128: u128 = 128, //TODO: add once https://github.com/FuelLabs/sway/issues/5356 is done
+
+abi TestContract {
+ fn return_configurables() -> (bool, u8, u16, u32, u64, u256, b256, str[4], (u8, bool), [u32; 3], StructWithGeneric<u8>, EnumWithGeneric<bool>);
+}
+
+impl TestContract for Contract {
+ fn return_configurables() -> (bool, u8, u16, u32, u64, u256, b256, str[4], (u8, bool), [u32; 3], StructWithGeneric<u8>, EnumWithGeneric<bool>) {
+ (BOOL, U8, U16, U32, U64, U256, B256, STR_4, TUPLE, ARRAY, STRUCT, ENUM)
+ }
+}
+
+Each of the configurable constants will get a dedicated with
method in the SDK. For example, the constant STR_4
will get the with_STR_4
method which accepts the same type as defined in the contract code. Below is an example where we chain several with
methods and deploy the contract with the new constants.
abigen!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/configurables/out/release/configurables-abi.json"
+ ));
+
+ let wallet = launch_provider_and_get_wallet().await?;
+
+ let str_4: SizedAsciiString<4> = "FUEL".try_into()?;
+ let new_struct = StructWithGeneric {
+ field_1: 16u8,
+ field_2: 32,
+ };
+ let new_enum = EnumWithGeneric::VariantTwo;
+
+ let configurables = MyContractConfigurables::default()
+ .with_BOOL(false)?
+ .with_U8(7)?
+ .with_U16(15)?
+ .with_U32(31)?
+ .with_U64(63)?
+ .with_U256(U256::from(8))?
+ .with_B256(Bits256([2; 32]))?
+ .with_STR_4(str_4.clone())?
+ .with_TUPLE((7, false))?
+ .with_ARRAY([252, 253, 254])?
+ .with_STRUCT(new_struct.clone())?
+ .with_ENUM(new_enum.clone())?;
+
+ let contract_id = Contract::load_from(
+ "sway/contracts/configurables/out/release/configurables.bin",
+ LoadConfiguration::default().with_configurables(configurables),
+ )?
+ .deploy_if_not_exists(&wallet, TxPolicies::default())
+ .await?;
+
+ let contract_instance = MyContract::new(contract_id, wallet.clone());
+
+
+ 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.
+Once you've written a contract in Sway and compiled it with forc build
, you'll have in your hands two important artifacts: the compiled binary file and the JSON ABI file.
++Note: Read here for more on how to work with Sway.
+
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.
+ + +First, the Contract::load_from
function is used to load a contract binary with a LoadConfiguration
. If you are only interested in a single instance of your contract, use the default configuration: LoadConfiguration::default()
. After the contract binary is loaded, you can use the deploy()
method to deploy the contract to the blockchain.
// This helper will launch a local node and provide a test wallet linked to it
+ let wallet = launch_provider_and_get_wallet().await?;
+
+ // This will load and deploy your contract binary to the chain so that its ID can
+ // be used to initialize the instance
+ let contract_id = Contract::load_from(
+ "../../e2e/sway/contracts/contract_test/out/release/contract_test.bin",
+ LoadConfiguration::default(),
+ )?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+ println!("Contract deployed @ {contract_id}");
+
+Alternatively, you can use LoadConfiguration
to configure how the contract is loaded. LoadConfiguration
let's you:
Salt
to get a new contract_id
++Note: The next section will give more information on how
+configurables
can be used.
Additionally, you can set custom TxParameters
when deploying the loaded contract.
// Optional: Add `Salt`
+ let rng = &mut StdRng::seed_from_u64(2322u64);
+ let salt: [u8; 32] = rng.gen();
+
+ // Optional: Configure storage
+ let key = Bytes32::from([1u8; 32]);
+ let value = Bytes32::from([2u8; 32]);
+ let storage_slot = StorageSlot::new(key, value);
+ let storage_configuration =
+ StorageConfiguration::default().add_slot_overrides([storage_slot]);
+ let configuration = LoadConfiguration::default()
+ .with_storage_configuration(storage_configuration)
+ .with_salt(salt);
+
+ // Optional: Configure deployment parameters
+ let tx_policies = TxPolicies::default()
+ .with_tip(1)
+ .with_script_gas_limit(1_000_000)
+ .with_maturity(0);
+
+ let contract_id_2 = Contract::load_from(
+ "../../e2e/sway/contracts/contract_test/out/release/contract_test.bin",
+ configuration,
+ )?
+ .deploy(&wallet, tx_policies)
+ .await?;
+
+ println!("Contract deployed @ {contract_id_2}");
+
+After the contract is deployed, you can use the contract's methods like this:
+ // 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!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
+ ));
+
+ // This is an instance of your contract which you can use to make calls to your functions
+ let contract_instance = MyContract::new(contract_id_2, 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);
+
+++ +Note: When redeploying an existing
+Contract
, ensure that you initialize it with a unique salt to prevent deployment failures caused by a contract ID collision. To accomplish this, utilize thewith_salt
method to clone the existingContract
with a new salt.
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:
abigen!(Contract(
+ name = "MyContract",
+ // Replace with your contract ABI.json path
+ abi = "e2e/sway/contracts/contract_test/out/release/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()?;
+
+ 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 bech32
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()?;
+
+ let connected_contract_instance = MyContract::new(contract_id, wallet);
+
+You can learn more about the Fuel SDK bech32
types here.
If your contract exceeds the size limit for a single deployment:
+ let contract = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?;
+ let max_allowed = provider
+ .consensus_parameters()
+ .contract_params()
+ .contract_max_size();
+
+ assert!(contract.code().len() as u64 > max_allowed);
+
+you can deploy it in segments using a partitioned approach:
+ let max_words_per_blob = 10_000;
+ let contract_id = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?
+ .convert_to_loader(max_words_per_blob)?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+When you convert a standard contract into a loader contract, the following changes occur:
+After deploying the loader contract, you can interact with it just as you would with a standard contract:
+ let contract_instance = MyContract::new(contract_id, wallet);
+ let response = contract_instance.methods().something().call().await?.value;
+ assert_eq!(response, 1001);
+
+A helper function is available to deploy your contract normally if it is within the size limit, or as a loader contract if it exceeds the limit:
+ let max_words_per_blob = 10_000;
+ let contract_id = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?
+ .smart_deploy(&wallet, TxPolicies::default(), max_words_per_blob)
+ .await?;
+
+You also have the option to separate the blob upload from the contract deployment for more granular control:
+ let contract_id = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?
+ .convert_to_loader(max_words_per_blob)?
+ .upload_blobs(&wallet, TxPolicies::default())
+ .await?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+Alternatively, you can manually split your contract code into blobs and then create and deploy a loader:
+ let chunk_size = 100_000;
+ assert!(
+ chunk_size % 8 == 0,
+ "all chunks, except the last, must be word-aligned"
+ );
+ let blobs = contract
+ .code()
+ .chunks(chunk_size)
+ .map(|chunk| Blob::new(chunk.to_vec()))
+ .collect();
+
+ let contract_id = Contract::loader_from_blobs(blobs, random_salt(), vec![])?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+Or you can upload the blobs yourself and proceed with just the loader deployment:
+ let max_words_per_blob = 10_000;
+ let blobs = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?
+ .convert_to_loader(max_words_per_blob)?
+ .blobs()
+ .to_vec();
+
+ let mut all_blob_ids = vec![];
+ let mut already_uploaded_blobs = HashSet::new();
+ for blob in blobs {
+ let blob_id = blob.id();
+ all_blob_ids.push(blob_id);
+
+ // uploading the same blob twice is not allowed
+ if already_uploaded_blobs.contains(&blob_id) {
+ continue;
+ }
+
+ let mut tb = BlobTransactionBuilder::default().with_blob(blob);
+ wallet.adjust_for_fee(&mut tb, 0).await?;
+ wallet.add_witnesses(&mut tb)?;
+
+ let tx = tb.build(&provider).await?;
+ provider
+ .send_transaction_and_await_commit(tx)
+ .await?
+ .check(None)?;
+
+ already_uploaded_blobs.insert(blob_id);
+ }
+
+ let contract_id = Contract::loader_from_blob_ids(all_blob_ids, random_salt(), vec![])?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+The size of a Blob transaction is constrained by three factors:
+ + + provider.consensus_parameters().tx_params().max_size();
+
+ provider.consensus_parameters().tx_params().max_gas_per_tx();
+
+To estimate an appropriate size for your blobs, you can run:
+ let max_blob_size = BlobTransactionBuilder::default()
+ .estimate_max_blob_size(&provider)
+ .await?;
+
+
+However, keep in mind the following limitations:
+Therefore, it is advisable to make your blobs a few percent smaller than the estimated maximum size.
+ +If you use storage in your contract, the default storage values will be generated in a JSON file (e.g. my_contract-storage_slots.json
) by the Sway compiler. These are loaded automatically for you when you load a contract binary. If you wish to override some of the defaults, you need to provide the corresponding storage slots manually:
use fuels::{programs::contract::Contract, tx::StorageSlot};
+ let slot_override = StorageSlot::new([1; 32].into(), [2; 32].into());
+ let storage_config =
+ StorageConfiguration::default().add_slot_overrides([slot_override]);
+
+ let load_config =
+ LoadConfiguration::default().with_storage_configuration(storage_config);
+ let _: Result<_> = Contract::load_from("...", load_config);
+
+If you don't have the slot storage file (my_contract-storage_slots.json
example from above) for some reason, or you don't wish to load any of the default values, you can disable the auto-loading of storage slots:
use fuels::programs::contract::Contract;
+ let storage_config = StorageConfiguration::default().with_autoload(false);
+
+ let load_config =
+ LoadConfiguration::default().with_storage_configuration(storage_config);
+ let _: Result<_> = Contract::load_from("...", load_config);
+
+
+ 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/release/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/release/my-test.bin
+half-word byte op raw notes
+ 0 0 JI(4) 90 00 00 04 jump to byte 16
+ 1 4 NOOP 47 00 00 00
+ 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.
+ +Please visit the Fuel installation guide to install the Fuel toolchain binaries and prerequisites.
+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:
+forc
and running the testsfuels-rs
crateYou can create a new Sway project with
+forc new <Project name>
+
+Or you can initialize a project within an existing folder with
+forc init
+
+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 install 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
+
+
+Add these dependencies on your Cargo.toml
:
fuels = "0.66.0"
+
+++Note We're using version
+0.66.0
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::*;
+
+Another way to experience the SDK is to look at the source code. The e2e/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 file
+packages/fuels/Forc.toml
contains a `[workspace], which members are the paths to all integration tests. +To build these tests, run the following command:
forc build --release --path e2e
+
++++
forc
can also be used to clean and format the test projects. Check thehelp
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
+
+Read The Sway Book for more in-depth knowledge about Sway, the official smart contract language for the Fuel Virtual Machine.
+ +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.
+ +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.
+ +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.
The Fuel Rust SDK can be used for a variety of things, including:
+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!
+ +Predicates, in Sway, are programs that return a Boolean value and do not have any side effects (they are pure). A predicate address can own assets. The predicate address is generated from the compiled 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. 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 the funds can be transferred if the predicate is validated successfully.
Let's consider the following predicate example:
+predicate;
+
+fn main(a: u32, b: u64) -> bool {
+ b == a.as_u64()
+}
+
+We will look at a complete example of using the SDK to send and receive funds from a predicate.
+First, we set up the wallets and a node instance. The call to the abigen!
macro will generate all the types specified in the predicate plus two custom structs:
encode_data
function that will conveniently encode all the arguments of the main function for us.++Note: The
+abigen!
macro will appendEncoder
andConfigurables
to the predicate'sname
field. Fox example,name="MyPredicate"
will result in two structs calledMyPredicateEncoder
andMyPredicateConfigurables
.
let asset_id = AssetId::zeroed();
+ let wallets_config = WalletsConfig::new_multiple_assets(
+ 2,
+ vec![AssetConfig {
+ id: asset_id,
+ num_coins: 1,
+ coin_amount: 1_000,
+ }],
+ );
+
+ let wallets = &launch_custom_provider_and_get_wallets(wallets_config, None, None).await?;
+
+ let first_wallet = &wallets[0];
+ let second_wallet = &wallets[1];
+
+ abigen!(Predicate(
+ name = "MyPredicate",
+ abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
+ ));
+
+Once we've compiled our predicate with forc build
, we can create a Predicate
instance via Predicate::load_from
. The resulting data from encode_data
can then be set on the loaded predicate.
let predicate_data = MyPredicateEncoder::default().encode_data(4096, 4096)?;
+ let code_path = "../../e2e/sway/predicates/basic_predicate/out/release/basic_predicate.bin";
+
+ let predicate: Predicate = Predicate::load_from(code_path)?
+ .with_provider(first_wallet.try_provider()?.clone())
+ .with_data(predicate_data);
+
+Next, we lock some assets in this predicate using the first wallet:
+ // First wallet transfers amount to predicate.
+ first_wallet
+ .transfer(predicate.address(), 500, asset_id, TxPolicies::default())
+ .await?;
+
+ // Check predicate balance.
+ let balance = predicate.get_asset_balance(&AssetId::zeroed()).await?;
+
+ assert_eq!(balance, 500);
+
+Then we can transfer assets owned by the predicate via the Account trait:
+ let amount_to_unlock = 300;
+
+ predicate
+ .transfer(
+ second_wallet.address(),
+ amount_to_unlock,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+ // Second wallet balance is updated.
+ let balance = second_wallet.get_asset_balance(&AssetId::zeroed()).await?;
+ assert_eq!(balance, 1300);
+
+Same as contracts and scripts, you can define configurable constants in predicates
, which can be changed during the predicate execution. Here is an example of how the constants are defined.
#[allow(dead_code)]
+enum EnumWithGeneric<D> {
+ VariantOne: D,
+ VariantTwo: (),
+}
+
+struct StructWithGeneric<D> {
+ field_1: D,
+ field_2: u64,
+}
+
+configurable {
+ BOOL: bool = true,
+ U8: u8 = 8,
+ TUPLE: (u8, bool) = (8, true),
+ ARRAY: [u32; 3] = [253, 254, 255],
+ STRUCT: StructWithGeneric<u8> = StructWithGeneric {
+ field_1: 8,
+ field_2: 16,
+ },
+ ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true),
+}
+
+fn main(
+ switch: bool,
+ u_8: u8,
+ some_tuple: (u8, bool),
+ some_array: [u32; 3],
+ some_struct: StructWithGeneric<u8>,
+ some_enum: EnumWithGeneric<bool>,
+) -> bool {
+ switch == BOOL && u_8 == U8 && some_tuple.0 == TUPLE.0 && some_tuple.1 == TUPLE.1 && some_array[0] == ARRAY[0] && some_array[1] == ARRAY[1] && some_array[2] == ARRAY[2] && some_struct == STRUCT && some_enum == ENUM
+}
+
+Each configurable constant will get a dedicated with
method in the SDK. For example, the constant U8
will get the with_U8
method which accepts the same type defined in sway. Below is an example where we chain several with
methods and update the predicate with the new constants.
abigen!(Predicate(
+ name = "MyPredicate",
+ abi = "e2e/sway/predicates/predicate_configurables/out/release/predicate_configurables-abi.json"
+ ));
+
+ let new_tuple = (16, false);
+ let new_array = [123, 124, 125];
+ let new_struct = StructWithGeneric {
+ field_1: 32u8,
+ field_2: 64,
+ };
+ let new_enum = EnumWithGeneric::VariantTwo;
+
+ let configurables = MyPredicateConfigurables::default()
+ .with_U8(8)?
+ .with_TUPLE(new_tuple)?
+ .with_ARRAY(new_array)?
+ .with_STRUCT(new_struct.clone())?
+ .with_ENUM(new_enum.clone())?;
+
+ let predicate_data = MyPredicateEncoder::default()
+ .encode_data(true, 8u8, new_tuple, new_array, new_struct, new_enum)?;
+
+ let mut predicate: Predicate = Predicate::load_from(
+ "sway/predicates/predicate_configurables/out/release/predicate_configurables.bin",
+ )?
+ .with_data(predicate_data)
+ .with_configurables(configurables);
+
+
+ This is a more involved example where the 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 the 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.
predicate;
+
+use std::{b512::B512, constants::ZERO_B256, ecr::ec_recover_address, inputs::input_predicate_data};
+
+fn extract_public_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 == Address::from(expected_public_key) {
+ return 1;
+ }
+ }
+
+ 0
+}
+
+fn main(signatures: [B512; 3]) -> bool {
+ let public_keys = [
+ 0xd58573593432a30a800f97ad32f877425c223a9e427ab557aab5d5bb89156db0,
+ 0x14df7c7e4e662db31fe2763b1734a3d680e7b743516319a49baaa22b2032a857,
+ 0x3ff494fb136978c3125844625dad6baf6e87cdb1328c8a51f35bda5afe72425c,
+ ];
+
+ let mut matched_keys = 0;
+
+ matched_keys = extract_public_key_and_match(signatures[0], public_keys[0]);
+ matched_keys = matched_keys + extract_public_key_and_match(signatures[1], public_keys[1]);
+ matched_keys = matched_keys + extract_public_key_and_match(signatures[2], public_keys[2]);
+
+ matched_keys > 1
+}
+
+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. Then we create the receiver wallet, which we will use to spend the predicate funds.
+ let secret_key1: SecretKey =
+ "0x862512a2363db2b3a375c0d4bbbd27172180d89f23f2e259bac850ab02619301".parse()?;
+
+ let secret_key2: SecretKey =
+ "0x37fa81c84ccd547c30c176b118d5cb892bdb113e8e80141f266519422ef9eefd".parse()?;
+
+ let secret_key3: SecretKey =
+ "0x976e5c3fa620092c718d852ca703b6da9e3075b9f2ecb8ed42d9f746bf26aafb".parse()?;
+
+ 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 mut receiver = WalletUnlocked::new_random(None);
+
+Next, let's add some coins, start a provider and connect it with the wallets.
+ let asset_id = AssetId::zeroed();
+ let num_coins = 32;
+ let amount = 64;
+ let initial_balance = amount * num_coins;
+ let all_coins = [&wallet, &wallet2, &wallet3, &receiver]
+ .iter()
+ .flat_map(|wallet| {
+ setup_single_asset_coins(wallet.address(), asset_id, num_coins, amount)
+ })
+ .collect::<Vec<_>>();
+
+ let provider = setup_test_provider(all_coins, vec![], None, None).await?;
+
+ [&mut wallet, &mut wallet2, &mut wallet3, &mut receiver]
+ .iter_mut()
+ .for_each(|wallet| {
+ wallet.set_provider(provider.clone());
+ });
+
+Now we can use the predicate abigen to create a predicate encoder instance for us. To spend the funds now locked in the predicate, we must 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.
+ abigen!(Predicate(
+ name = "MyPredicate",
+ abi = "e2e/sway/predicates/signatures/out/release/signatures-abi.json"
+ ));
+
+ let predicate_data = MyPredicateEncoder::default().encode_data(signatures)?;
+ let code_path = "../../e2e/sway/predicates/signatures/out/release/signatures.bin";
+
+ let predicate: Predicate = Predicate::load_from(code_path)?
+ .with_provider(provider)
+ .with_data(predicate_data);
+
+Next, we transfer some assets from a wallet to the created predicate. We also confirm that the funds are indeed transferred.
+ let amount_to_predicate = 500;
+
+ wallet
+ .transfer(
+ predicate.address(),
+ amount_to_predicate,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+ let predicate_balance = predicate.get_asset_balance(&asset_id).await?;
+ assert_eq!(predicate_balance, amount_to_predicate);
+
+We can use the transfer
method from the Account trait to transfer the assets. If the predicate data is correct, the receiver
wallet will get the funds, and we will verify that the amount is correct.
let amount_to_receiver = 300;
+ predicate
+ .transfer(
+ receiver.address(),
+ amount_to_receiver,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+ let receiver_balance_after = receiver.get_asset_balance(&asset_id).await?;
+ assert_eq!(initial_balance + amount_to_receiver, receiver_balance_after);
+
+
+ If you have a script or predicate that is larger than normal or which you plan +on calling often, you can pre-upload its code as a blob to the network and run a +loader script/predicate instead. The loader can be configured with the +script/predicate configurables, so you can change how the script/predicate is +configured on each run without having large transactions due to the code +duplication.
+A high level pre-upload:
+ my_script.convert_into_loader().await?.main().call().await?;
+
+The upload of the blob is handled inside of the convert_into_loader
method. If you
+want more fine-grained control over it, you can create the script transaction
+manually:
let regular = Executable::load_from(binary_path)?;
+
+ let configurables = MyScriptConfigurables::default().with_SECRET_NUMBER(10001)?;
+ let loader = regular
+ .convert_to_loader()?
+ .with_configurables(configurables);
+
+ // The Blob must be uploaded manually, otherwise the script code will revert.
+ loader.upload_blob(wallet.clone()).await?;
+
+ let encoder = fuels::core::codec::ABIEncoder::default();
+ let token = MyStruct {
+ field_a: MyEnum::B(99),
+ field_b: Bits256([17; 32]),
+ }
+ .into_token();
+ let data = encoder.encode(&[token])?;
+
+ let mut tb = ScriptTransactionBuilder::default()
+ .with_script(loader.code())
+ .with_script_data(data);
+
+ wallet.adjust_for_fee(&mut tb, 0).await?;
+
+ wallet.add_witnesses(&mut tb)?;
+
+ let tx = tb.build(&provider).await?;
+
+ let response = provider.send_transaction_and_await_commit(tx).await?;
+
+ response.check(None)?;
+
+You can prepare a predicate for pre-uploading without doing network requests:
+ let configurables = MyPredicateConfigurables::default().with_SECRET_NUMBER(10001)?;
+
+ let predicate_data = MyPredicateEncoder::default().encode_data(1, 19)?;
+
+ let executable =
+ Executable::load_from("sway/predicates/predicate_blobs/out/release/predicate_blobs.bin")?;
+
+ let loader = executable
+ .convert_to_loader()?
+ .with_configurables(configurables);
+
+ let mut predicate: Predicate = Predicate::from_code(loader.code()).with_data(predicate_data);
+
+Once you want to execute the predicate, you must beforehand upload the blob +containing its code:
+ loader.upload_blob(extra_wallet).await?;
+
+ predicate.set_provider(provider.clone());
+
+ let expected_fee = 1;
+ predicate
+ .transfer(
+ receiver.address(),
+ predicate_balance - expected_fee,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+By pre-uploading the predicate code, you allow for cheaper calls to the predicate +from subsequent callers.
+ +The Fuel Rust SDK can be used for a variety of things, including:
+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!
+Please visit the Fuel installation guide to install the Fuel toolchain binaries and prerequisites.
+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:
+forc
and running the testsfuels-rs
crateYou can create a new Sway project with
+forc new <Project name>
+
+Or you can initialize a project within an existing folder with
+forc init
+
+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 install 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
+
+
+Add these dependencies on your Cargo.toml
:
fuels = "0.66.0"
+
+++Note We're using version
+0.66.0
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::*;
+
+Another way to experience the SDK is to look at the source code. The e2e/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 file
+packages/fuels/Forc.toml
contains a `[workspace], which members are the paths to all integration tests. +To build these tests, run the following command:
forc build --release --path e2e
+
++++
forc
can also be used to clean and format the test projects. Check thehelp
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
+
+Read The Sway Book for more in-depth knowledge about Sway, the official smart contract language for the Fuel Virtual Machine.
+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:
fuel-core
) and instantiate a provider that points to that node's IP and port.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.
+ +We can interact with the Testnet
node by using the following example.
use std::str::FromStr;
+
+ use fuels::{crypto::SecretKey, prelude::*};
+
+ // Create a provider pointing to the testnet.
+ let provider = Provider::connect("testnet.fuel.network").await.unwrap();
+
+ // Setup a private key
+ let secret = SecretKey::from_str(
+ "a1447cd75accc6b71a976fd3401a1f6ce318d27ba660b0315ee6ac347bf39568",
+ )?;
+
+ // 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());
+
+++For detailed information about various testnet networks and their optimal toolchain configurations for your project, please visit the following link:
+ +
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
+ +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 Testnet at
+ +
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(format!("127.0.0.1:{port}")).await?;
+
+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::prelude::{FuelService, Provider};
+
+ // Run the fuel node.
+ let server = FuelService::start(
+ NodeConfig::default(),
+ ChainConfig::default(),
+ StateConfig::default(),
+ )
+ .await?;
+
+ // Create a client that will talk to the node created above.
+ let client = Provider::from(server.bound_address()).await?;
+ assert!(client.healthy().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 = 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?;
+
+The fuel-core-lib
feature allows 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.66.10", features = ["fuel-core-lib"] }
+
+The rocksdb
is an additional feature that, when combined with fuel-core-lib
, provides persistent storage capabilities while using fuel-core
as a library.
fuels = { version = "0.66.10", features = ["rocksdb"] }
+
+RocksDB enables the preservation of the blockchain's state locally, facilitating its future utilization.
+To create or use a local database, follow these instructions:
+ let provider_config = NodeConfig {
+ database_type: DbType::RocksDb(Some(PathBuf::from("/tmp/.spider/db"))),
+ ..NodeConfig::default()
+ };
+
+++Note: If the specified database does not exist, a new database will be created at that path. To utilize the code snippets above, either the
+fuel-core
binary must be present, or both thefuel-core-lib
androcksdb
features need to be enabled.
Once you set up a provider, you can 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.
+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(),
+ AssetId::zeroed(),
+ number_of_coins,
+ amount_per_coin,
+ );
+
+ let retry_config = RetryConfig::new(3, Backoff::Fixed(Duration::from_secs(2)))?;
+ let provider = setup_test_provider(coins.clone(), vec![], None, None)
+ .await?
+ .with_retry_config(retry_config);
+
+This method returns all unspent coins (of a given asset ID) from a wallet.
+ let coins = provider
+ .get_coins(wallet.address(), *provider.base_asset_id())
+ .await?;
+ assert_eq!(coins.len(), 1);
+
+The following example shows how to fetch resources owned by an address. First, you create a ResourceFilter
which specifies the target address, asset ID, and amount. You can also define UTXO IDs and message IDs that should be excluded when retrieving the resources:
pub struct ResourceFilter {
+ pub from: Bech32Address,
+ pub asset_id: Option<AssetId>,
+ pub amount: u64,
+ pub excluded_utxos: Vec<UtxoId>,
+ pub excluded_message_nonces: Vec<Nonce>,
+}
+
+The example uses default values for the asset ID and the exclusion lists. This resolves to the base asset ID and empty vectors for the ID lists respectively:
+ let filter = ResourceFilter {
+ from: wallet.address().clone(),
+ amount: 1,
+ ..Default::default()
+ };
+ let spendable_resources = provider.get_spendable_resources(filter).await?;
+ assert_eq!(spendable_resources.len(), 1);
+
+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?;
+
+The Provider
can be configured to retry a request upon receiving a io::Error
.
++Note: Currently all node errors are received as
+io::Error
s. So, if configured, a retry will happen even if, for example, a transaction failed to verify.
We can configure the number of retry attempts and the retry strategy as detailed below.
+RetryConfig
The retry behavior can be altered by giving a custom RetryConfig
. It allows for configuring the maximum number of attempts and the interval strategy used.
#[derive(Clone, Debug)]
+pub struct RetryConfig {
+ max_attempts: NonZeroU32,
+ interval: Backoff,
+}
+
+ let retry_config = RetryConfig::new(3, Backoff::Fixed(Duration::from_secs(2)))?;
+ let provider = setup_test_provider(coins.clone(), vec![], None, None)
+ .await?
+ .with_retry_config(retry_config);
+
+Backoff
Backoff
defines different strategies for managing intervals between retry attempts.
+Each strategy allows you to customize the waiting time before a new attempt based on the number of attempts made.
Linear(Duration)
: Default
Increases the waiting time linearly with each attempt.Exponential(Duration)
: Doubles the waiting time with each attempt.Fixed(Duration)
: Uses a constant waiting time between attempts.#[derive(Debug, Clone)]
+pub enum Backoff {
+ Linear(Duration),
+ Exponential(Duration),
+ Fixed(Duration),
+}
+
+The ViewOnlyAccount
trait provides a common interface to query balances.
The Account
trait, in addition to the above, also provides a common interface to retrieve spendable resources or transfer assets. When performing actions in the SDK that lead to a transaction, you will typically need to provide an account that will be used to allocate resources required by the transaction, including transaction fees.
The traits are implemented by the following types:
+ +An account implements the following methods for transferring assets:
+transfer
force_transfer_to_contract
withdraw_to_base_layer
The following examples are provided for a Wallet
account. A Predicate
account would work similarly, but you might need to set its predicate data before attempting to spend resources owned by it.
With wallet.transfer
you can initiate a transaction to transfer an asset from your account to a target address.
use fuels::prelude::*;
+
+ // Setup 2 test wallets with 1 coin each
+ let num_wallets = 2;
+ let coins_per_wallet = 1;
+ let coin_amount = 2;
+
+ let wallets = launch_custom_provider_and_get_wallets(
+ WalletsConfig::new(Some(num_wallets), Some(coins_per_wallet), Some(coin_amount)),
+ None,
+ None,
+ )
+ .await?;
+
+ // Transfer the base asset with amount 1 from wallet 1 to wallet 2
+ let transfer_amount = 1;
+ let asset_id = Default::default();
+ let (_tx_id, _receipts) = wallets[0]
+ .transfer(
+ wallets[1].address(),
+ transfer_amount,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+ let wallet_2_final_coins = wallets[1].get_coins(AssetId::zeroed()).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
+ .try_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, TxPolicies::default())
+ .await?;
+
+ // Check that the contract now has 1 coin
+ let contract_balances = wallet
+ .try_provider()?
+ .get_contract_balances(&contract_id)
+ .await?;
+ assert_eq!(contract_balances.len(), 1);
+
+ let random_asset_balance = contract_balances.get(&random_asset_id).unwrap();
+ assert_eq!(*random_asset_balance, 300);
+
+For transferring assets to the base layer chain, you can use wallet.withdraw_to_base_layer
.
use std::str::FromStr;
+
+ use fuels::prelude::*;
+
+ let wallets = launch_custom_provider_and_get_wallets(
+ WalletsConfig::new(Some(1), None, None),
+ None,
+ None,
+ )
+ .await?;
+ let wallet = wallets.first().unwrap();
+
+ let amount = 1000;
+ let base_layer_address = Address::from_str(
+ "0x4710162c2e3a95a6faff05139150017c9e38e5e280432d546fae345d6ce6d8fe",
+ )?;
+ 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, TxPolicies::default())
+ .await?;
+
+ let _block_height = wallet.try_provider()?.produce_blocks(1, None).await?;
+
+ // Retrieve a message proof from the provider
+ let proof = wallet
+ .try_provider()?
+ .get_message_proof(&tx_id, &msg_id, None, Some(2))
+ .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 policies. Lastly, to verify that the transfer succeeded, the relevant message proof is retrieved with provider.get_message_proof,
and the amount and the recipient are verified.
To facilitate account impersonation, the Rust SDK provides the ImpersonatedAccount
struct. Since it implements Account
, we can use it to simulate ownership of assets held by an account with a given address. This also implies that we can impersonate contract calls from that address. ImpersonatedAccount
will only succeed in unlocking assets if the network is set up with utxo_validation = false
.
// create impersonator for an address
+ let address =
+ Address::from_str("0x17f46f562778f4bb5fe368eeae4985197db51d80c83494ea7f84c530172dedd1")
+ .unwrap();
+ let address = Bech32Address::from(address);
+ let impersonator = ImpersonatedAccount::new(address, Some(provider.clone()));
+
+ let contract_instance = MyContract::new(contract_id, impersonator.clone());
+
+ let response = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .call()
+ .await?;
+
+ assert_eq!(42, response.value);
+
+You can use wallets for many important things, for instance:
+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.
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 = 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 std::str::FromStr;
+
+ use fuels::{crypto::SecretKey, prelude::*};
+
+ // Use the test helper to setup a test provider.
+ let provider = 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 withset_provider()
. The optional parameter +enables defining owners (wallet addresses) of genesis coins before a provider is launched.
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 = 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);
+
+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.
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.
You can learn more about signing here.
+ + +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.
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 with the lock
method 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.
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.
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 = 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))?;
+
+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 = 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)?;
+
+In the Fuel network, 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::zeroed();
+ 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), you can use the get_balances
method:
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_balance = balances.get(&asset_id.to_string()).unwrap();
+
+You'll often want to create one or more test wallets when testing your contracts. Here's how to do it.
+If you need multiple test wallets, they can be set up using the launch_custom_provider_and_get_wallets
method.
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
orlaunch_custom_provider_and_get_wallets
+will have deterministic addresses.
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 = setup_test_provider(coins.clone(), vec![], None, None).await?;
+ wallet.set_provider(provider);
+
+Vec<(UtxoId, Coin)>
has num_assets
* coins_per_assets
coins (UTXOs)Vec<AssetId>
contains the num_assets
randomly generated AssetId
s (always includes the base asset)You can also create assets with specific AssetId
s, 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: AssetId::zeroed(),
+ 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 = 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
+
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(),
+ AssetId::zeroed(),
+ number_of_coins,
+ amount_per_coin,
+ );
+
+++Note Choosing a large number of coins and assets for
+setup_multiple_assets_coins
orsetup_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.
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
. 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 = secret_seed.as_slice().try_into()?;
+
+ // Create a wallet using the private key created above.
+ let wallet = WalletUnlocked::new_from_private_key(secret, None);
+
+ let message = Message::new("my message".as_bytes());
+ let signature = wallet.sign(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 recovered_address = signature.recover(&message)?;
+
+ assert_eq!(wallet.address().hash(), recovered_address.hash());
+
+ // Verify signature
+ signature.verify(&recovered_address, &message)?;
+
+Signers
to a transaction builderEvery signed resource in the inputs needs to have a witness index that points to a valid witness. Changing the witness index inside an input will change the transaction ID. This means that we need to set all witness indexes before finally signing the transaction. Previously, the user had to make sure that the witness indexes and the order of the witnesses are correct. To automate this process, the SDK will keep track of the signers in the transaction builder and resolve the final transaction automatically. This is done by storing signers until the final transaction is built.
+Below is a full example of how to create a transaction builder and add signers to it.
+++Note: When you add a
+Signer
to a transaction builder, the signer is stored inside it and the transaction will not be resolved until you callbuild()
!
let secret = SecretKey::from_str(
+ "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
+ )?;
+ let wallet = WalletUnlocked::new_from_private_key(secret, None);
+
+ // Set up a transaction
+ let mut tb = {
+ let input_coin = Input::ResourceSigned {
+ resource: CoinType::Coin(Coin {
+ amount: 10000000,
+ owner: wallet.address().clone(),
+ ..Default::default()
+ }),
+ };
+
+ let output_coin = Output::coin(
+ Address::from_str(
+ "0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077",
+ )?,
+ 1,
+ Default::default(),
+ );
+
+ ScriptTransactionBuilder::prepare_transfer(
+ vec![input_coin],
+ vec![output_coin],
+ Default::default(),
+ )
+ };
+
+ // Add `Signer` to the transaction builder
+ tb.add_signer(wallet.clone())?;
+
+If you have a built transaction and want to add a signature, you can use the sign_with
method.
tx.sign_with(&wallet, provider.chain_id()).await?;
+
+You might have noticed this snippet in the previous sections:
+ abigen!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/contract_test/out/release/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. +In order to call your contracts, scripts or predicates, you first need to generate the Rust bindings for them.
+ +The following subsections contain more details about the abigen!
syntax and the code generated from it.
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/release/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.
+abigen!
is a procedural macro -- it generates code. It accepts inputs in the format of:
ProgramType(name="MyProgramType", abi="my_program-abi.json")...
+
+where:
+ProgramType
is one of: Contract
, Script
or Predicate
,
name
is the name that will be given to the generated bindings,
abi
is either a path to the JSON ABI file or its actual contents.
So, an abigen!
which generates bindings for two contracts and one script looks like this:
abigen!(
+ Contract(
+ name = "ContractA",
+ abi = "e2e/sway/bindings/sharing_types/contract_a/out/release/contract_a-abi.json"
+ ),
+ Contract(
+ name = "ContractB",
+ abi = "e2e/sway/bindings/sharing_types/contract_b/out/release/contract_b-abi.json"
+ ),
+ Script(
+ name = "MyScript",
+ abi = "e2e/sway/scripts/arguments/out/release/arguments-abi.json"
+ ),
+ Predicate(
+ name = "MyPredicateEncoder",
+ abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
+ ),
+ );
+
+A rough overview:
+pub mod abigen_bindings {
+ pub mod contract_a_mod {
+ struct SomeCustomStruct{/*...*/};
+ // other custom types used in the contract
+
+ struct ContractA {/*...*/};
+ impl ContractA {/*...*/};
+ // ...
+ }
+ pub mod contract_b_mod {
+ // ...
+ }
+ pub mod my_script_mod {
+ // ...
+ }
+ pub mod my_predicate_mod{
+ // ...
+ }
+ pub mod shared_types{
+ // ...
+ }
+}
+
+pub use contract_a_mod::{/*..*/};
+pub use contract_b_mod::{/*..*/};
+pub use my_predicate_mod::{/*..*/};
+pub use shared_types::{/*..*/};
+
+Each ProgramType
gets its own mod
based on the name
given in the abigen!
. Inside the respective mods, the custom types used by that program are generated, and the bindings through which the actual calls can be made.
One extra mod
called shared_types
is generated if abigen!
detects that the given programs share types. Instead of each mod
regenerating the type for itself, the type is lifted out into the shared_types
module, generated only once, and then shared between all program bindings that use it. Reexports are added to each mod so that even if a type is deemed shared, you can still access it as though each mod
had generated the type for itself (i.e. my_contract_mod::SharedType
).
A type is deemed shared if its name and definition match up. This can happen either because you've used the same library (a custom one or a type from the stdlib
) or because you've happened to define the exact same type.
Finally, pub use
statements are inserted, so you don't have to fully qualify the generated types. To avoid conflict, only types that have unique names will get a pub use
statement. If you find rustc
can't find your type, it might just be that there is another generated type with the same name. To fix the issue just qualify the path by doing abigen_bindings::whatever_contract_mod::TheType
.
++Note: +It is highly encouraged that you generate all your bindings in one
+abigen!
call. Doing it in this manner will allow type sharing and avoid name collisions you'd normally get when callingabigen!
multiple times inside the same namespace. If you choose to proceed otherwise, keep in mind the generated code overview presented above and appropriately separate theabigen!
calls into different modules to resolve the collision.
Let's look at a contract with two methods: initialize_counter(arg: u64) -> u64
and increment_counter(arg: u64) -> u64
, with the following JSON ABI:
{
+ "programType": "contract",
+ "specVersion": "1",
+ "encodingVersion": "1",
+ "concreteTypes": [
+ {
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0",
+ "type": "u64"
+ }
+ ],
+ "functions": [
+ {
+ "inputs": [
+ {
+ "name": "value",
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "name": "initialize_counter",
+ "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ },
+ {
+ "inputs": [
+ {
+ "name": "value",
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "name": "increment_counter",
+ "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "metadataTypes": []
+}
+
+By doing this:
+ use fuels::prelude::*;
+ // Replace with your own JSON abi path (relative to the root of your crate)
+ abigen!(Contract(
+ name = "MyContractName",
+ abi = "examples/rust_bindings/src/abi.json"
+ ));
+
+or this:
+ use fuels::prelude::*;
+ abigen!(Contract(
+ name = "MyContract",
+ abi = r#"
+ {
+ "programType": "contract",
+ "specVersion": "1",
+ "encodingVersion": "1",
+ "concreteTypes": [
+ {
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0",
+ "type": "u64"
+ }
+ ],
+ "functions": [
+ {
+ "inputs": [
+ {
+ "name": "value",
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "name": "initialize_counter",
+ "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ },
+ {
+ "inputs": [
+ {
+ "name": "value",
+ "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "name": "increment_counter",
+ "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
+ }
+ ],
+ "metadataTypes": []
+ }
+ "#
+ ));
+
+you'll generate this (shortened for brevity's sake):
+pub mod abigen_bindings {
+ pub mod my_contract_mod {
+ #[derive(Debug, Clone)]
+ pub struct MyContract<A: ::fuels::accounts::Account> {
+ contract_id: ::fuels::types::bech32::Bech32ContractId,
+ account: A,
+ log_decoder: ::fuels::core::codec::LogDecoder,
+ encoder_config: ::fuels::core::codec::EncoderConfig,
+ }
+ impl<A: ::fuels::accounts::Account> MyContract<A> {
+ pub fn new(
+ contract_id: impl ::core::convert::Into<::fuels::types::bech32::Bech32ContractId>,
+ account: A,
+ ) -> Self {
+ let contract_id: ::fuels::types::bech32::Bech32ContractId = contract_id.into();
+ let log_decoder = ::fuels::core::codec::LogDecoder::new(
+ ::fuels::core::codec::log_formatters_lookup(vec![], contract_id.clone().into()),
+ );
+ let encoder_config = ::fuels::core::codec::EncoderConfig::default();
+ Self {
+ contract_id,
+ account,
+ log_decoder,
+ encoder_config,
+ }
+ }
+ pub fn contract_id(&self) -> &::fuels::types::bech32::Bech32ContractId {
+ &self.contract_id
+ }
+ pub fn account(&self) -> A {
+ self.account.clone()
+ }
+ pub fn with_account<U: ::fuels::accounts::Account>(self, account: U) -> MyContract<U> {
+ MyContract {
+ contract_id: self.contract_id,
+ account,
+ log_decoder: self.log_decoder,
+ encoder_config: self.encoder_config,
+ }
+ }
+ pub fn with_encoder_config(
+ mut self,
+ encoder_config: ::fuels::core::codec::EncoderConfig,
+ ) -> MyContract<A> {
+ self.encoder_config = encoder_config;
+ self
+ }
+ pub async fn get_balances(
+ &self,
+ ) -> ::fuels::types::errors::Result<
+ ::std::collections::HashMap<::fuels::types::AssetId, u64>,
+ > {
+ ::fuels::accounts::ViewOnlyAccount::try_provider(&self.account)?
+ .get_contract_balances(&self.contract_id)
+ .await
+ .map_err(::std::convert::Into::into)
+ }
+ pub fn methods(&self) -> MyContractMethods<A> {
+ MyContractMethods {
+ contract_id: self.contract_id.clone(),
+ account: self.account.clone(),
+ log_decoder: self.log_decoder.clone(),
+ encoder_config: self.encoder_config.clone(),
+ }
+ }
+ }
+ pub struct MyContractMethods<A: ::fuels::accounts::Account> {
+ contract_id: ::fuels::types::bech32::Bech32ContractId,
+ account: A,
+ log_decoder: ::fuels::core::codec::LogDecoder,
+ encoder_config: ::fuels::core::codec::EncoderConfig,
+ }
+ impl<A: ::fuels::accounts::Account> MyContractMethods<A> {
+ #[doc = " This method will read the counter from storage, increment it"]
+ #[doc = " and write the incremented value to storage"]
+ pub fn increment_counter(
+ &self,
+ value: ::core::primitive::u64,
+ ) -> ::fuels::programs::calls::CallHandler<
+ A,
+ ::fuels::programs::calls::ContractCall,
+ ::core::primitive::u64,
+ > {
+ ::fuels::programs::calls::CallHandler::new_contract_call(
+ self.contract_id.clone(),
+ self.account.clone(),
+ ::fuels::core::codec::encode_fn_selector("increment_counter"),
+ &[::fuels::core::traits::Tokenizable::into_token(value)],
+ self.log_decoder.clone(),
+ false,
+ self.encoder_config.clone(),
+ )
+ }
+ pub fn initialize_counter(
+ &self,
+ value: ::core::primitive::u64,
+ ) -> ::fuels::programs::calls::CallHandler<
+ A,
+ ::fuels::programs::calls::ContractCall,
+ ::core::primitive::u64,
+ > {
+ ::fuels::programs::calls::CallHandler::new_contract_call(
+ self.contract_id.clone(),
+ self.account.clone(),
+ ::fuels::core::codec::encode_fn_selector("initialize_counter"),
+ &[::fuels::core::traits::Tokenizable::into_token(value)],
+ self.log_decoder.clone(),
+ false,
+ self.encoder_config.clone(),
+ )
+ }
+ }
+ impl<A: ::fuels::accounts::Account> ::fuels::programs::calls::ContractDependency for MyContract<A> {
+ fn id(&self) -> ::fuels::types::bech32::Bech32ContractId {
+ self.contract_id.clone()
+ }
+ fn log_decoder(&self) -> ::fuels::core::codec::LogDecoder {
+ self.log_decoder.clone()
+ }
+ }
+ #[derive(Clone, Debug, Default)]
+ pub struct MyContractConfigurables {
+ offsets_with_data: ::std::vec::Vec<(u64, ::std::vec::Vec<u8>)>,
+ encoder: ::fuels::core::codec::ABIEncoder,
+ }
+ impl MyContractConfigurables {
+ pub fn new(encoder_config: ::fuels::core::codec::EncoderConfig) -> Self {
+ Self {
+ encoder: ::fuels::core::codec::ABIEncoder::new(encoder_config),
+ ..::std::default::Default::default()
+ }
+ }
+ }
+ impl From<MyContractConfigurables> for ::fuels::core::Configurables {
+ fn from(config: MyContractConfigurables) -> Self {
+ ::fuels::core::Configurables::new(config.offsets_with_data)
+ }
+ }
+ }
+}
+pub use abigen_bindings::my_contract_mod::MyContract;
+pub use abigen_bindings::my_contract_mod::MyContractConfigurables;
+pub use abigen_bindings::my_contract_mod::MyContractMethods;
+
+++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 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!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
+ ));
+
+ // This is an instance of your contract which you can use to make calls to your functions
+ let contract_instance = MyContract::new(contract_id_2, 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);
+
+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.
+Once you've written a contract in Sway and compiled it with forc build
, you'll have in your hands two important artifacts: the compiled binary file and the JSON ABI file.
++Note: Read here for more on how to work with Sway.
+
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.
+ + +First, the Contract::load_from
function is used to load a contract binary with a LoadConfiguration
. If you are only interested in a single instance of your contract, use the default configuration: LoadConfiguration::default()
. After the contract binary is loaded, you can use the deploy()
method to deploy the contract to the blockchain.
// This helper will launch a local node and provide a test wallet linked to it
+ let wallet = launch_provider_and_get_wallet().await?;
+
+ // This will load and deploy your contract binary to the chain so that its ID can
+ // be used to initialize the instance
+ let contract_id = Contract::load_from(
+ "../../e2e/sway/contracts/contract_test/out/release/contract_test.bin",
+ LoadConfiguration::default(),
+ )?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+ println!("Contract deployed @ {contract_id}");
+
+Alternatively, you can use LoadConfiguration
to configure how the contract is loaded. LoadConfiguration
let's you:
Salt
to get a new contract_id
++Note: The next section will give more information on how
+configurables
can be used.
Additionally, you can set custom TxParameters
when deploying the loaded contract.
// Optional: Add `Salt`
+ let rng = &mut StdRng::seed_from_u64(2322u64);
+ let salt: [u8; 32] = rng.gen();
+
+ // Optional: Configure storage
+ let key = Bytes32::from([1u8; 32]);
+ let value = Bytes32::from([2u8; 32]);
+ let storage_slot = StorageSlot::new(key, value);
+ let storage_configuration =
+ StorageConfiguration::default().add_slot_overrides([storage_slot]);
+ let configuration = LoadConfiguration::default()
+ .with_storage_configuration(storage_configuration)
+ .with_salt(salt);
+
+ // Optional: Configure deployment parameters
+ let tx_policies = TxPolicies::default()
+ .with_tip(1)
+ .with_script_gas_limit(1_000_000)
+ .with_maturity(0);
+
+ let contract_id_2 = Contract::load_from(
+ "../../e2e/sway/contracts/contract_test/out/release/contract_test.bin",
+ configuration,
+ )?
+ .deploy(&wallet, tx_policies)
+ .await?;
+
+ println!("Contract deployed @ {contract_id_2}");
+
+After the contract is deployed, you can use the contract's methods like this:
+ // 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!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
+ ));
+
+ // This is an instance of your contract which you can use to make calls to your functions
+ let contract_instance = MyContract::new(contract_id_2, 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);
+
+++Note: When redeploying an existing
+Contract
, ensure that you initialize it with a unique salt to prevent deployment failures caused by a contract ID collision. To accomplish this, utilize thewith_salt
method to clone the existingContract
with a new salt.
In Sway, you can define configurable
constants which can be changed during the contract deployment in the SDK. Here is an example how the constants are defined.
contract;
+
+#[allow(dead_code)]
+enum EnumWithGeneric<D> {
+ VariantOne: D,
+ VariantTwo: (),
+}
+
+struct StructWithGeneric<D> {
+ field_1: D,
+ field_2: u64,
+}
+
+configurable {
+ BOOL: bool = true,
+ U8: u8 = 8,
+ U16: u16 = 16,
+ U32: u32 = 32,
+ U64: u64 = 63,
+ U256: u256 = 0x0000000000000000000000000000000000000000000000000000000000000008u256,
+ B256: b256 = 0x0101010101010101010101010101010101010101010101010101010101010101,
+ STR_4: str[4] = __to_str_array("fuel"),
+ TUPLE: (u8, bool) = (8, true),
+ ARRAY: [u32; 3] = [253, 254, 255],
+ STRUCT: StructWithGeneric<u8> = StructWithGeneric {
+ field_1: 8,
+ field_2: 16,
+ },
+ ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true),
+}
+//U128: u128 = 128, //TODO: add once https://github.com/FuelLabs/sway/issues/5356 is done
+
+abi TestContract {
+ fn return_configurables() -> (bool, u8, u16, u32, u64, u256, b256, str[4], (u8, bool), [u32; 3], StructWithGeneric<u8>, EnumWithGeneric<bool>);
+}
+
+impl TestContract for Contract {
+ fn return_configurables() -> (bool, u8, u16, u32, u64, u256, b256, str[4], (u8, bool), [u32; 3], StructWithGeneric<u8>, EnumWithGeneric<bool>) {
+ (BOOL, U8, U16, U32, U64, U256, B256, STR_4, TUPLE, ARRAY, STRUCT, ENUM)
+ }
+}
+
+Each of the configurable constants will get a dedicated with
method in the SDK. For example, the constant STR_4
will get the with_STR_4
method which accepts the same type as defined in the contract code. Below is an example where we chain several with
methods and deploy the contract with the new constants.
abigen!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/configurables/out/release/configurables-abi.json"
+ ));
+
+ let wallet = launch_provider_and_get_wallet().await?;
+
+ let str_4: SizedAsciiString<4> = "FUEL".try_into()?;
+ let new_struct = StructWithGeneric {
+ field_1: 16u8,
+ field_2: 32,
+ };
+ let new_enum = EnumWithGeneric::VariantTwo;
+
+ let configurables = MyContractConfigurables::default()
+ .with_BOOL(false)?
+ .with_U8(7)?
+ .with_U16(15)?
+ .with_U32(31)?
+ .with_U64(63)?
+ .with_U256(U256::from(8))?
+ .with_B256(Bits256([2; 32]))?
+ .with_STR_4(str_4.clone())?
+ .with_TUPLE((7, false))?
+ .with_ARRAY([252, 253, 254])?
+ .with_STRUCT(new_struct.clone())?
+ .with_ENUM(new_enum.clone())?;
+
+ let contract_id = Contract::load_from(
+ "sway/contracts/configurables/out/release/configurables.bin",
+ LoadConfiguration::default().with_configurables(configurables),
+ )?
+ .deploy_if_not_exists(&wallet, TxPolicies::default())
+ .await?;
+
+ let contract_instance = MyContract::new(contract_id, wallet.clone());
+
+If you use storage in your contract, the default storage values will be generated in a JSON file (e.g. my_contract-storage_slots.json
) by the Sway compiler. These are loaded automatically for you when you load a contract binary. If you wish to override some of the defaults, you need to provide the corresponding storage slots manually:
use fuels::{programs::contract::Contract, tx::StorageSlot};
+ let slot_override = StorageSlot::new([1; 32].into(), [2; 32].into());
+ let storage_config =
+ StorageConfiguration::default().add_slot_overrides([slot_override]);
+
+ let load_config =
+ LoadConfiguration::default().with_storage_configuration(storage_config);
+ let _: Result<_> = Contract::load_from("...", load_config);
+
+If you don't have the slot storage file (my_contract-storage_slots.json
example from above) for some reason, or you don't wish to load any of the default values, you can disable the auto-loading of storage slots:
use fuels::programs::contract::Contract;
+ let storage_config = StorageConfiguration::default().with_autoload(false);
+
+ let load_config =
+ LoadConfiguration::default().with_storage_configuration(storage_config);
+ let _: Result<_> = Contract::load_from("...", load_config);
+
+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:
abigen!(Contract(
+ name = "MyContract",
+ // Replace with your contract ABI.json path
+ abi = "e2e/sway/contracts/contract_test/out/release/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()?;
+
+ 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 bech32
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()?;
+
+ let connected_contract_instance = MyContract::new(contract_id, wallet);
+
+You can learn more about the Fuel SDK bech32
types here.
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/release/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/release/my-test.bin
+half-word byte op raw notes
+ 0 0 JI(4) 90 00 00 04 jump to byte 16
+ 1 4 NOOP 47 00 00 00
+ 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.
+If your contract exceeds the size limit for a single deployment:
+ let contract = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?;
+ let max_allowed = provider
+ .consensus_parameters()
+ .contract_params()
+ .contract_max_size();
+
+ assert!(contract.code().len() as u64 > max_allowed);
+
+you can deploy it in segments using a partitioned approach:
+ let max_words_per_blob = 10_000;
+ let contract_id = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?
+ .convert_to_loader(max_words_per_blob)?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+When you convert a standard contract into a loader contract, the following changes occur:
+After deploying the loader contract, you can interact with it just as you would with a standard contract:
+ let contract_instance = MyContract::new(contract_id, wallet);
+ let response = contract_instance.methods().something().call().await?.value;
+ assert_eq!(response, 1001);
+
+A helper function is available to deploy your contract normally if it is within the size limit, or as a loader contract if it exceeds the limit:
+ let max_words_per_blob = 10_000;
+ let contract_id = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?
+ .smart_deploy(&wallet, TxPolicies::default(), max_words_per_blob)
+ .await?;
+
+You also have the option to separate the blob upload from the contract deployment for more granular control:
+ let contract_id = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?
+ .convert_to_loader(max_words_per_blob)?
+ .upload_blobs(&wallet, TxPolicies::default())
+ .await?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+Alternatively, you can manually split your contract code into blobs and then create and deploy a loader:
+ let chunk_size = 100_000;
+ assert!(
+ chunk_size % 8 == 0,
+ "all chunks, except the last, must be word-aligned"
+ );
+ let blobs = contract
+ .code()
+ .chunks(chunk_size)
+ .map(|chunk| Blob::new(chunk.to_vec()))
+ .collect();
+
+ let contract_id = Contract::loader_from_blobs(blobs, random_salt(), vec![])?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+Or you can upload the blobs yourself and proceed with just the loader deployment:
+ let max_words_per_blob = 10_000;
+ let blobs = Contract::load_from(
+ contract_binary,
+ LoadConfiguration::default().with_salt(random_salt()),
+ )?
+ .convert_to_loader(max_words_per_blob)?
+ .blobs()
+ .to_vec();
+
+ let mut all_blob_ids = vec![];
+ let mut already_uploaded_blobs = HashSet::new();
+ for blob in blobs {
+ let blob_id = blob.id();
+ all_blob_ids.push(blob_id);
+
+ // uploading the same blob twice is not allowed
+ if already_uploaded_blobs.contains(&blob_id) {
+ continue;
+ }
+
+ let mut tb = BlobTransactionBuilder::default().with_blob(blob);
+ wallet.adjust_for_fee(&mut tb, 0).await?;
+ wallet.add_witnesses(&mut tb)?;
+
+ let tx = tb.build(&provider).await?;
+ provider
+ .send_transaction_and_await_commit(tx)
+ .await?
+ .check(None)?;
+
+ already_uploaded_blobs.insert(blob_id);
+ }
+
+ let contract_id = Contract::loader_from_blob_ids(all_blob_ids, random_salt(), vec![])?
+ .deploy(&wallet, TxPolicies::default())
+ .await?;
+
+The size of a Blob transaction is constrained by three factors:
+ + + provider.consensus_parameters().tx_params().max_size();
+
+ provider.consensus_parameters().tx_params().max_gas_per_tx();
+
+To estimate an appropriate size for your blobs, you can run:
+ let max_blob_size = BlobTransactionBuilder::default()
+ .estimate_max_blob_size(&provider)
+ .await?;
+
+
+However, keep in mind the following limitations:
+Therefore, it is advisable to make your blobs a few percent smaller than the estimated maximum size.
+Once you've deployed your contract, as seen in the previous sections, you'll likely want to:
+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 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!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
+ ));
+
+ // This is an instance of your contract which you can use to make calls to your functions
+ let contract_instance = MyContract::new(contract_id_2, 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.
+Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows:
+ let response = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .submit()
+ .await?;
+
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let value = response.response().await?.value;
+
+
+Next, we'll see how we can further configure the many different parameters in a contract call.
+You can use the with_account()
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_account(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 policies are defined as follows:
+pub struct TxPolicies {
+ tip: Option<u64>,
+ witness_limit: Option<u64>,
+ maturity: Option<u64>,
+ max_fee: Option<u64>,
+ script_gas_limit: Option<u64>,
+}
+
+Where:
+When the Script Gas Limit is not set, the Rust SDK will estimate the consumed gas in the background and set it as the limit.
+If the Witness Limit is not set, the SDK will set it to the size of all witnesses and signatures defined in the transaction builder.
+You can configure these parameters by creating an instance of TxPolicies
and passing it to a chain method called with_tx_policies
:
let contract_methods = MyContract::new(contract_id.clone(), wallet.clone()).methods();
+
+ let tx_policies = TxPolicies::default()
+ .with_tip(1)
+ .with_script_gas_limit(1_000_000)
+ .with_maturity(0);
+
+ let response = contract_methods
+ .initialize_counter(42) // Our contract method
+ .with_tx_policies(tx_policies) // Chain the tx policies
+ .call() // Perform the contract call
+ .await?; // This is an async call, `.await` it.
+
+
+
+You can also use TxPolicies::default()
to use the default values.
This way:
+ let response = contract_methods
+ .initialize_counter(42)
+ .with_tx_policies(TxPolicies::default())
+ .call()
+ .await?;
+
+As you might have noticed, TxPolicies
can also be specified when deploying contracts or transferring assets by passing it to the respective methods.
The parameters for a contract call are:
+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.
#[payable]
+ 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_policies = TxPolicies::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::default().with_amount(1_000_000);
+
+ let response = contract_methods
+ .get_msg_amount() // Our contract method.
+ .with_tx_policies(tx_policies) // Chain the tx policies.
+ .call_params(call_params)? // Chain the call parameters.
+ .call() // Perform the contract call.
+ .await?;
+
+
+
+call_params
returns a result to ensure you don't forward assets to a contract method that isn't payable.
In the following example, we try to forward an amount of 100
of the base asset to non_payable
. As its name suggests, non_payable
isn't annotated with #[payable]
in the contract code. Passing CallParameters
with an amount other than 0
leads to an error:
let err = contract_methods
+ .non_payable()
+ .call_params(CallParameters::default().with_amount(100))
+ .expect_err("should return error");
+
+ assert!(matches!(err, Error::Other(s) if s.contains("assets forwarded to non-payable method")));
+
+++Note: forwarding gas to a contract call is always possible, regardless of the contract method being non-payable.
+
You can also use CallParameters::default()
to use the default values:
pub const DEFAULT_CALL_PARAMS_AMOUNT: u64 = 0;
+
+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 1_000_000 and `gas_forwarded` to 4300 to specify that
+ // the contract call transaction may consume up to 1_000_000 gas, while the actual call may
+ // only use 4300 gas
+ let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000);
+ let call_params = CallParameters::default().with_gas_forwarded(4300);
+
+ let response = contract_methods
+ .get_msg_amount() // Our contract method.
+ .with_tx_policies(tx_policies) // Chain the tx policies.
+ .call_params(call_params)? // Chain the call parameters.
+ .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.
The SDK provides the option to transfer assets within the same transaction, when making a contract call. By using add_custom_asset()
you specify the asset ID, the amount, and the destination address:
let amount = 1000;
+ let _ = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .add_custom_asset(
+ AssetId::zeroed(),
+ amount,
+ Some(other_wallet.address().clone()),
+ )
+ .call()
+ .await?;
+
+You've probably noticed that you're often chaining .call().await.unwrap()
. That's because:
.call()
and .simulate()
(more on this in the next section)..await
it or perform concurrent tasks, making full use of Rust's async..unwrap()
the Result<CallResponse, Error>
returned by the contract call.Once you unwrap the CallResponse
, you have access to this struct:
pub struct CallResponse<D> {
+ pub value: D,
+ pub receipts: Vec<Receipt>,
+ pub gas_used: u64,
+ pub log_decoder: LogDecoder,
+ pub tx_id: Option<Bytes32>,
+}
+
+
+
+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).
receipts
will hold all receipts generated by that specific contract call.gas_used
is the amount of gas consumed by the contract call.tx_id
will hold the ID of the corresponding submitted transaction.You can use the is_ok
and is_err
methods to check if a contract call Result
is Ok
or contains an error. These methods will return either true
or false
.
let is_ok = response.is_ok();
+let is_error = response.is_err();
+
+
+
+
+If is_err
returns true
, you can use the unwrap_err
method to unwrap the error message.
if response.is_err() {
+ let err = response.unwrap_err();
+ println!("ERROR: {:?}", err);
+};
+
+
+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] = __to_str_array("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 decode_logs_with_type::<T>
from a CallResponse
, 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.decode_logs_with_type::<u64>()?;
+ let log_bits256 = response.decode_logs_with_type::<Bits256>()?;
+ let log_string = response.decode_logs_with_type::<SizedAsciiString<4>>()?;
+ let log_array = response.decode_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 use the decode_logs()
function to retrieve a LogResult
struct containing a results
field that is a vector of Result<String>
values representing the success or failure of decoding each log.
let contract_methods = contract_instance.methods();
+ let response = contract_methods.produce_multiple_logs().call().await?;
+ let logs = response.decode_logs();
+
+Due to possible performance hits, it is not recommended to use decode_logs()
outside of a debugging scenario.
++Note: String slices cannot be logged directly. Use the
+__to_str_array()
function to convert it to astr[N]
first.
Sometimes, the contract you call might transfer funds to a specific address, depending on its execution. The underlying transaction for such a contract call has to have the appropriate number of variable outputs to succeed.
+ +Let's say you deployed a contract with the following method:
+ fn transfer(coins: u64, asset_id: AssetId, recipient: Identity) {
+ transfer(recipient, asset_id, coins);
+ }
+
+When calling transfer_coins_to_output
with the SDK, you can specify the number of variable outputs:
let address = wallet.address();
+ let asset_id = contract_id.asset_id(&Bits256::zeroed());
+
+ // withdraw some tokens to wallet
+ let response = contract_methods
+ .transfer(1_000_000, asset_id, address.into())
+ .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
+ .call()
+ .await?;
+
+
+
+with_variable_output_policy
sets the policy regarding variable outputs. You can either set the number of variable outputs yourself by providing VariableOutputPolicy::Exactly(n)
or let the SDK estimate it for you with VariableOutputPolicy::EstimateMinimum
. A variable output indicates that the amount and the owner may vary based on transaction execution.
++Note: that the Sway
+lib-std
functionmint_to_address
callstransfer_to_address
under the hood, so you need to callwith_variable_output_policy
in the Rust SDK tests like you would fortransfer_to_address
.
Sometimes you want to simulate a call to a contract without changing the state of the blockchain. This can be achieved by calling .simulate
instead of .call
and passing in the desired execution context:
.simulate(Execution::Realistic)
simulates the transaction in a manner that closely resembles a real call. You need a wallet with base assets to cover the transaction cost, even though no funds will be consumed. This is useful for validating that a real call would succeed if made at that moment. It allows you to debug issues with your contract without spending gas. // you would mint 100 coins if the transaction wasn't simulated
+ let counter = contract_methods
+ .mint_coins(100)
+ .simulate(Execution::Realistic)
+ .await?;
+
+.simulate(Execution::StateReadOnly)
disables many validations, adds fake gas, extra variable outputs, blank witnesses, etc., enabling you to read state even with an account that has no funds. // you don't need any funds to read state
+ let balance = contract_methods
+ .get_balance(contract_id, AssetId::zeroed())
+ .simulate(Execution::StateReadOnly)
+ .await?
+ .value;
+
+If your contract method is calling other contracts you will have to add the appropriate Inputs
and Outputs
to your transaction. For your convenience, the CallHandler
provides methods that prepare those inputs and outputs for you. You have two methods that you can use: with_contracts(&[&contract_instance, ...])
and with_contract_ids(&[&contract_id, ...])
.
with_contracts(&[&contract_instance, ...])
requires contract instances that were created using the abigen
macro. When setting the external contracts with this method, logs and require revert errors originating from the external contract can be propagated and decoded by the calling contract.
let response = contract_caller_instance
+ .methods()
+ .increment_from_contract(lib_contract_id, 42)
+ .with_contracts(&[&lib_contract_instance])
+ .call()
+ .await?;
+
+If however, you do not need to decode logs or you do not have a contract instance that was generated using the abigen
macro you can use with_contract_ids(&[&contract_id, ...])
and provide the required contract ids.
let response = contract_caller_instance
+ .methods()
+ .increment_from_contract(lib_contract_id, 42)
+ .with_contract_ids(&[lib_contract_id.clone()])
+ .call()
+ .await?;
+
+With CallHandler
, 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 CallHandler
and optionally configure transaction policies:
let multi_call_handler = CallHandler::new_multi_call(wallet.clone())
+ .add_call(call_handler_1)
+ .add_call(call_handler_2);
+
+++Note: any transaction policies configured on separate contract calls are disregarded in favor of the parameters provided to the multi-call
+CallHandler
.
Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows:
+ let submitted_tx = multi_call_handler.submit().await?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let (counter, array): (u64, [u64; 2]) = submitted_tx.response().await?.value;
+
+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 CallResponse
by moving the type annotation to the invoked method:
let response = multi_call_handler.call::<(u64, [u64; 2])>().await?;
+
+Previously, we mentioned that a contract call might require you to manually specify external contracts, variable outputs, or output messages. The SDK can also attempt to estimate and set these dependencies for you at the cost of running multiple simulated calls in the background.
+The following example uses a contract call that calls an external contract and later mints assets to a specified address. Calling it without including the dependencies will result in a revert:
+ let address = wallet.address();
+ let amount = 100;
+
+ let response = contract_methods
+ .mint_then_increment_from_contract(called_contract_id, amount, address.into())
+ .call()
+ .await;
+
+ assert!(matches!(
+ response,
+ Err(Error::Transaction(Reason::Reverted { .. }))
+ ));
+
+As mentioned in previous chapters, you can specify the external contract and add an output variable to resolve this:
+ let response = contract_methods
+ .mint_then_increment_from_contract(called_contract_id, amount, address.into())
+ .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
+ .with_contract_ids(&[called_contract_id.into()])
+ .call()
+ .await?;
+
+But this requires you to know the contract ID of the external contract and the needed number of output variables. Alternatively, by chaining .estimate_tx_dependencies()
instead, the dependencies will be estimated by the SDK and set automatically. The optional parameter is the maximum number of simulation attempts:
let response = contract_methods
+ .mint_then_increment_from_contract(called_contract_id, amount, address.into())
+ .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
+ .determine_missing_contracts(Some(2))
+ .await?
+ .call()
+ .await?;
+
+The minimal number of attempts corresponds to the number of external contracts and output variables needed and defaults to 10.
+++Note:
+estimate_tx_dependencies()
can also be used when working with script calls or multi calls.estimate_tx_dependencies()
does not currently resolve the dependencies needed for logging from an external contract. For more information, see here. If no resolution was found after exhausting all simulation attempts, the last received error will be propagated. The same will happen if an error is unrelated to transaction dependencies.
With the function estimate_transaction_cost(tolerance: Option<f64>, block_horizon: Option<u32>)
provided by CallHandler
, you can get a cost estimation for a specific call. The return type, TransactionCost
, is a struct that contains relevant information for the estimation:
pub struct TransactionCost {
+ pub gas_price: u64,
+ pub gas_used: u64,
+ pub metered_bytes_size: u64,
+ pub total_fee: u64,
+}
+
+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 = Some(0.0);
+ let block_horizon = Some(1);
+ let transaction_cost = contract_instance
+ .methods()
+ .initialize_counter(42) // Build the ABI call
+ .estimate_transaction_cost(tolerance, block_horizon) // Get estimated transaction cost
+ .await?;
+
+ let call_handler_1 = contract_methods.initialize_counter(42);
+ let call_handler_2 = contract_methods.get_array([42; 2]);
+
+ let multi_call_handler = CallHandler::new_multi_call(wallet.clone())
+ .add_call(call_handler_1)
+ .add_call(call_handler_2);
+
+ let tolerance = Some(0.0);
+ let block_horizon = Some(1);
+ let transaction_cost = multi_call_handler
+ .estimate_transaction_cost(tolerance, block_horizon) // 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.
+++Note The same estimation interface is available for scripts.
+
With low-level calls, you can specify the parameters of your calls at runtime and make indirect calls through other contracts.
+Your caller contract should call std::low_level_call::call_with_function_selector
, providing:
Bytes
Bytes
u64
)std::low_level_call::CallParams
fn call_low_level_call(
+ target: ContractId,
+ function_selector: Bytes,
+ calldata: Bytes,
+ ) {
+ let call_params = CallParams {
+ coins: 0,
+ asset_id: AssetId::from(ZERO_B256),
+ gas: 10_000,
+ };
+
+ call_with_function_selector(target, function_selector, calldata, call_params);
+ }
+
+On the SDK side, you can construct an encoded function selector using fuels::core::encode_fn_selector
, and encoded calldata using the fuels::core::calldata!
macro.
E.g. to call the following function on the target contract:
+ #[storage(write)]
+ fn set_value_multiple_complex(a: MyStruct, b: str[4]);
+
+you would construct the function selector and the calldata as such, and provide them to the caller contract (like the one above):
+ let function_selector = encode_fn_selector("set_value_multiple_complex");
+ let call_data = calldata!(
+ MyStruct {
+ a: true,
+ b: [1, 2, 3],
+ },
+ SizedAsciiString::<4>::try_from("fuel")?
+ )?;
+
+ caller_contract_instance
+ .methods()
+ .call_low_level_call(
+ target_contract_instance.id(),
+ Bytes(function_selector),
+ Bytes(call_data),
+ )
+ .determine_missing_contracts(None)
+ .await?
+ .call()
+ .await?;
+
+++Note: the
+calldata!
macro uses the defaultEncoderConfig
configuration under the hood.
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 abigen!
macro seen previously.
// The abigen is used for the same purpose as with contracts (Rust bindings)
+ abigen!(Script(
+ name = "MyScript",
+ abi = "e2e/sway/scripts/arguments/out/release/arguments-abi.json"
+ ));
+ let wallet = launch_provider_and_get_wallet().await?;
+ let bin_path = "sway/scripts/arguments/out/release/arguments.bin";
+ let script_instance = MyScript::new(wallet, bin_path);
+
+ let bim = Bimbam { val: 90 };
+ let bam = SugarySnack {
+ twix: 100,
+ mars: 1000,
+ };
+
+ let result = script_instance.main(bim, bam).call().await?;
+
+ let expected = Bimbam { val: 2190 };
+ assert_eq!(result.value, expected);
+
+Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows:
+ let submitted_tx = script_instance.main(my_struct).submit().await?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let value = submitted_tx.response().await?.value;
+
+The method for passing transaction policies is the same as with contracts. As a reminder, the workflow would look like this:
+ let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000);
+ let result = script_instance
+ .main(a, b)
+ .with_tx_policies(tx_policies)
+ .call()
+ .await?;
+
+Script calls provide the same logging functions, decode_logs()
and decode_logs_with_type<T>()
, as contract calls. As a reminder, the workflow looks like this:
abigen!(Script(
+ name = "log_script",
+ abi = "e2e/sway/logs/script_logs/out/release/script_logs-abi.json"
+ ));
+
+ let wallet = launch_provider_and_get_wallet().await?;
+ let bin_path = "sway/logs/script_logs/out/release/script_logs.bin";
+ let instance = log_script::new(wallet.clone(), bin_path);
+
+ let response = instance.main().call().await?;
+
+ let logs = response.decode_logs();
+ let log_u64 = response.decode_logs_with_type::<u64>()?;
+
+Scripts use the same interfaces for setting external contracts as contract methods.
+Below is an example that uses with_contracts(&[&contract_instance, ...])
.
let response = script_instance
+ .main(contract_id)
+ .with_contracts(&[&contract_instance])
+ .call()
+ .await?;
+
+And this is an example that uses with_contract_ids(&[&contract_id, ...])
.
let response = script_instance
+ .main(contract_id)
+ .with_contract_ids(&[contract_id.into()])
+ .call()
+ .await?;
+
+Same as contracts, you can define configurable
constants in scripts
which can be changed during the script execution. Here is an example how the constants are defined.
script;
+
+#[allow(dead_code)]
+enum EnumWithGeneric<D> {
+ VariantOne: D,
+ VariantTwo: (),
+}
+
+struct StructWithGeneric<D> {
+ field_1: D,
+ field_2: u64,
+}
+
+configurable {
+ BOOL: bool = true,
+ U8: u8 = 8,
+ U16: u16 = 16,
+ U32: u32 = 32,
+ U64: u64 = 63,
+ U256: u256 = 0x0000000000000000000000000000000000000000000000000000000000000008u256,
+ B256: b256 = 0x0101010101010101010101010101010101010101010101010101010101010101,
+ STR_4: str[4] = __to_str_array("fuel"),
+ TUPLE: (u8, bool) = (8, true),
+ ARRAY: [u32; 3] = [253, 254, 255],
+ STRUCT: StructWithGeneric<u8> = StructWithGeneric {
+ field_1: 8,
+ field_2: 16,
+ },
+ ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true),
+}
+//U128: u128 = 128, //TODO: add once https://github.com/FuelLabs/sway/issues/5356 is done
+
+fn main() -> (bool, u8, u16, u32, u64, u256, b256, str[4], (u8, bool), [u32; 3], StructWithGeneric<u8>, EnumWithGeneric<bool>) {
+ (BOOL, U8, U16, U32, U64, U256, B256, STR_4, TUPLE, ARRAY, STRUCT, ENUM)
+}
+
+Each configurable constant will get a dedicated with
method in the SDK. For example, the constant STR_4
will get the with_STR_4
method which accepts the same type defined in sway. Below is an example where we chain several with
methods and execute the script with the new constants.
abigen!(Script(
+ name = "MyScript",
+ abi = "e2e/sway/scripts/script_configurables/out/release/script_configurables-abi.json"
+ ));
+
+ let wallet = launch_provider_and_get_wallet().await?;
+ let bin_path = "sway/scripts/script_configurables/out/release/script_configurables.bin";
+ let instance = MyScript::new(wallet, bin_path);
+
+ let str_4: SizedAsciiString<4> = "FUEL".try_into()?;
+ let new_struct = StructWithGeneric {
+ field_1: 16u8,
+ field_2: 32,
+ };
+ let new_enum = EnumWithGeneric::VariantTwo;
+
+ let configurables = MyScriptConfigurables::new(EncoderConfig {
+ max_tokens: 5,
+ ..Default::default()
+ })
+ .with_BOOL(false)?
+ .with_U8(7)?
+ .with_U16(15)?
+ .with_U32(31)?
+ .with_U64(63)?
+ .with_U256(U256::from(8))?
+ .with_B256(Bits256([2; 32]))?
+ .with_STR_4(str_4.clone())?
+ .with_TUPLE((7, false))?
+ .with_ARRAY([252, 253, 254])?
+ .with_STRUCT(new_struct.clone())?
+ .with_ENUM(new_enum.clone())?;
+
+ let response = instance
+ .with_configurables(configurables)
+ .main()
+ .call()
+ .await?;
+
+Predicates, in Sway, are programs that return a Boolean value and do not have any side effects (they are pure). A predicate address can own assets. The predicate address is generated from the compiled 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. 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 the funds can be transferred if the predicate is validated successfully.
Let's consider the following predicate example:
+predicate;
+
+fn main(a: u32, b: u64) -> bool {
+ b == a.as_u64()
+}
+
+We will look at a complete example of using the SDK to send and receive funds from a predicate.
+First, we set up the wallets and a node instance. The call to the abigen!
macro will generate all the types specified in the predicate plus two custom structs:
encode_data
function that will conveniently encode all the arguments of the main function for us.++Note: The
+abigen!
macro will appendEncoder
andConfigurables
to the predicate'sname
field. Fox example,name="MyPredicate"
will result in two structs calledMyPredicateEncoder
andMyPredicateConfigurables
.
let asset_id = AssetId::zeroed();
+ let wallets_config = WalletsConfig::new_multiple_assets(
+ 2,
+ vec![AssetConfig {
+ id: asset_id,
+ num_coins: 1,
+ coin_amount: 1_000,
+ }],
+ );
+
+ let wallets = &launch_custom_provider_and_get_wallets(wallets_config, None, None).await?;
+
+ let first_wallet = &wallets[0];
+ let second_wallet = &wallets[1];
+
+ abigen!(Predicate(
+ name = "MyPredicate",
+ abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
+ ));
+
+Once we've compiled our predicate with forc build
, we can create a Predicate
instance via Predicate::load_from
. The resulting data from encode_data
can then be set on the loaded predicate.
let predicate_data = MyPredicateEncoder::default().encode_data(4096, 4096)?;
+ let code_path = "../../e2e/sway/predicates/basic_predicate/out/release/basic_predicate.bin";
+
+ let predicate: Predicate = Predicate::load_from(code_path)?
+ .with_provider(first_wallet.try_provider()?.clone())
+ .with_data(predicate_data);
+
+Next, we lock some assets in this predicate using the first wallet:
+ // First wallet transfers amount to predicate.
+ first_wallet
+ .transfer(predicate.address(), 500, asset_id, TxPolicies::default())
+ .await?;
+
+ // Check predicate balance.
+ let balance = predicate.get_asset_balance(&AssetId::zeroed()).await?;
+
+ assert_eq!(balance, 500);
+
+Then we can transfer assets owned by the predicate via the Account trait:
+ let amount_to_unlock = 300;
+
+ predicate
+ .transfer(
+ second_wallet.address(),
+ amount_to_unlock,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+ // Second wallet balance is updated.
+ let balance = second_wallet.get_asset_balance(&AssetId::zeroed()).await?;
+ assert_eq!(balance, 1300);
+
+Same as contracts and scripts, you can define configurable constants in predicates
, which can be changed during the predicate execution. Here is an example of how the constants are defined.
#[allow(dead_code)]
+enum EnumWithGeneric<D> {
+ VariantOne: D,
+ VariantTwo: (),
+}
+
+struct StructWithGeneric<D> {
+ field_1: D,
+ field_2: u64,
+}
+
+configurable {
+ BOOL: bool = true,
+ U8: u8 = 8,
+ TUPLE: (u8, bool) = (8, true),
+ ARRAY: [u32; 3] = [253, 254, 255],
+ STRUCT: StructWithGeneric<u8> = StructWithGeneric {
+ field_1: 8,
+ field_2: 16,
+ },
+ ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true),
+}
+
+fn main(
+ switch: bool,
+ u_8: u8,
+ some_tuple: (u8, bool),
+ some_array: [u32; 3],
+ some_struct: StructWithGeneric<u8>,
+ some_enum: EnumWithGeneric<bool>,
+) -> bool {
+ switch == BOOL && u_8 == U8 && some_tuple.0 == TUPLE.0 && some_tuple.1 == TUPLE.1 && some_array[0] == ARRAY[0] && some_array[1] == ARRAY[1] && some_array[2] == ARRAY[2] && some_struct == STRUCT && some_enum == ENUM
+}
+
+Each configurable constant will get a dedicated with
method in the SDK. For example, the constant U8
will get the with_U8
method which accepts the same type defined in sway. Below is an example where we chain several with
methods and update the predicate with the new constants.
abigen!(Predicate(
+ name = "MyPredicate",
+ abi = "e2e/sway/predicates/predicate_configurables/out/release/predicate_configurables-abi.json"
+ ));
+
+ let new_tuple = (16, false);
+ let new_array = [123, 124, 125];
+ let new_struct = StructWithGeneric {
+ field_1: 32u8,
+ field_2: 64,
+ };
+ let new_enum = EnumWithGeneric::VariantTwo;
+
+ let configurables = MyPredicateConfigurables::default()
+ .with_U8(8)?
+ .with_TUPLE(new_tuple)?
+ .with_ARRAY(new_array)?
+ .with_STRUCT(new_struct.clone())?
+ .with_ENUM(new_enum.clone())?;
+
+ let predicate_data = MyPredicateEncoder::default()
+ .encode_data(true, 8u8, new_tuple, new_array, new_struct, new_enum)?;
+
+ let mut predicate: Predicate = Predicate::load_from(
+ "sway/predicates/predicate_configurables/out/release/predicate_configurables.bin",
+ )?
+ .with_data(predicate_data)
+ .with_configurables(configurables);
+
+This is a more involved example where the 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 the 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.
predicate;
+
+use std::{b512::B512, constants::ZERO_B256, ecr::ec_recover_address, inputs::input_predicate_data};
+
+fn extract_public_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 == Address::from(expected_public_key) {
+ return 1;
+ }
+ }
+
+ 0
+}
+
+fn main(signatures: [B512; 3]) -> bool {
+ let public_keys = [
+ 0xd58573593432a30a800f97ad32f877425c223a9e427ab557aab5d5bb89156db0,
+ 0x14df7c7e4e662db31fe2763b1734a3d680e7b743516319a49baaa22b2032a857,
+ 0x3ff494fb136978c3125844625dad6baf6e87cdb1328c8a51f35bda5afe72425c,
+ ];
+
+ let mut matched_keys = 0;
+
+ matched_keys = extract_public_key_and_match(signatures[0], public_keys[0]);
+ matched_keys = matched_keys + extract_public_key_and_match(signatures[1], public_keys[1]);
+ matched_keys = matched_keys + extract_public_key_and_match(signatures[2], public_keys[2]);
+
+ matched_keys > 1
+}
+
+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. Then we create the receiver wallet, which we will use to spend the predicate funds.
+ let secret_key1: SecretKey =
+ "0x862512a2363db2b3a375c0d4bbbd27172180d89f23f2e259bac850ab02619301".parse()?;
+
+ let secret_key2: SecretKey =
+ "0x37fa81c84ccd547c30c176b118d5cb892bdb113e8e80141f266519422ef9eefd".parse()?;
+
+ let secret_key3: SecretKey =
+ "0x976e5c3fa620092c718d852ca703b6da9e3075b9f2ecb8ed42d9f746bf26aafb".parse()?;
+
+ 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 mut receiver = WalletUnlocked::new_random(None);
+
+Next, let's add some coins, start a provider and connect it with the wallets.
+ let asset_id = AssetId::zeroed();
+ let num_coins = 32;
+ let amount = 64;
+ let initial_balance = amount * num_coins;
+ let all_coins = [&wallet, &wallet2, &wallet3, &receiver]
+ .iter()
+ .flat_map(|wallet| {
+ setup_single_asset_coins(wallet.address(), asset_id, num_coins, amount)
+ })
+ .collect::<Vec<_>>();
+
+ let provider = setup_test_provider(all_coins, vec![], None, None).await?;
+
+ [&mut wallet, &mut wallet2, &mut wallet3, &mut receiver]
+ .iter_mut()
+ .for_each(|wallet| {
+ wallet.set_provider(provider.clone());
+ });
+
+Now we can use the predicate abigen to create a predicate encoder instance for us. To spend the funds now locked in the predicate, we must 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.
+ abigen!(Predicate(
+ name = "MyPredicate",
+ abi = "e2e/sway/predicates/signatures/out/release/signatures-abi.json"
+ ));
+
+ let predicate_data = MyPredicateEncoder::default().encode_data(signatures)?;
+ let code_path = "../../e2e/sway/predicates/signatures/out/release/signatures.bin";
+
+ let predicate: Predicate = Predicate::load_from(code_path)?
+ .with_provider(provider)
+ .with_data(predicate_data);
+
+Next, we transfer some assets from a wallet to the created predicate. We also confirm that the funds are indeed transferred.
+ let amount_to_predicate = 500;
+
+ wallet
+ .transfer(
+ predicate.address(),
+ amount_to_predicate,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+ let predicate_balance = predicate.get_asset_balance(&asset_id).await?;
+ assert_eq!(predicate_balance, amount_to_predicate);
+
+We can use the transfer
method from the Account trait to transfer the assets. If the predicate data is correct, the receiver
wallet will get the funds, and we will verify that the amount is correct.
let amount_to_receiver = 300;
+ predicate
+ .transfer(
+ receiver.address(),
+ amount_to_receiver,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+ let receiver_balance_after = receiver.get_asset_balance(&asset_id).await?;
+ assert_eq!(initial_balance + amount_to_receiver, receiver_balance_after);
+
+If you have a script or predicate that is larger than normal or which you plan +on calling often, you can pre-upload its code as a blob to the network and run a +loader script/predicate instead. The loader can be configured with the +script/predicate configurables, so you can change how the script/predicate is +configured on each run without having large transactions due to the code +duplication.
+A high level pre-upload:
+ my_script.convert_into_loader().await?.main().call().await?;
+
+The upload of the blob is handled inside of the convert_into_loader
method. If you
+want more fine-grained control over it, you can create the script transaction
+manually:
let regular = Executable::load_from(binary_path)?;
+
+ let configurables = MyScriptConfigurables::default().with_SECRET_NUMBER(10001)?;
+ let loader = regular
+ .convert_to_loader()?
+ .with_configurables(configurables);
+
+ // The Blob must be uploaded manually, otherwise the script code will revert.
+ loader.upload_blob(wallet.clone()).await?;
+
+ let encoder = fuels::core::codec::ABIEncoder::default();
+ let token = MyStruct {
+ field_a: MyEnum::B(99),
+ field_b: Bits256([17; 32]),
+ }
+ .into_token();
+ let data = encoder.encode(&[token])?;
+
+ let mut tb = ScriptTransactionBuilder::default()
+ .with_script(loader.code())
+ .with_script_data(data);
+
+ wallet.adjust_for_fee(&mut tb, 0).await?;
+
+ wallet.add_witnesses(&mut tb)?;
+
+ let tx = tb.build(&provider).await?;
+
+ let response = provider.send_transaction_and_await_commit(tx).await?;
+
+ response.check(None)?;
+
+You can prepare a predicate for pre-uploading without doing network requests:
+ let configurables = MyPredicateConfigurables::default().with_SECRET_NUMBER(10001)?;
+
+ let predicate_data = MyPredicateEncoder::default().encode_data(1, 19)?;
+
+ let executable =
+ Executable::load_from("sway/predicates/predicate_blobs/out/release/predicate_blobs.bin")?;
+
+ let loader = executable
+ .convert_to_loader()?
+ .with_configurables(configurables);
+
+ let mut predicate: Predicate = Predicate::from_code(loader.code()).with_data(predicate_data);
+
+Once you want to execute the predicate, you must beforehand upload the blob +containing its code:
+ loader.upload_blob(extra_wallet).await?;
+
+ predicate.set_provider(provider.clone());
+
+ let expected_fee = 1;
+ predicate
+ .transfer(
+ receiver.address(),
+ predicate_balance - expected_fee,
+ asset_id,
+ TxPolicies::default(),
+ )
+ .await?;
+
+By pre-uploading the predicate code, you allow for cheaper calls to the predicate +from subsequent callers.
+Until now, we have used helpers to create transactions, send them with a provider, and parse the results. However, sometimes we must make custom transactions with specific inputs, outputs, witnesses, etc. In the next chapter, we will show how to use the Rust SDKs transaction builders to accomplish this.
+The Rust SDK simplifies the creation of Create and Script transactions through two handy builder structs CreateTransactionBuilder
, ScriptTransactionBuilder
, and the TransactionBuilder
trait.
Calling build(&provider)
on a builder will result in the corresponding CreateTransaction
or ScriptTransaction
that can be submitted to the network.
++Note This section contains additional information about the inner workings of the builders. If you are just interested in how to use them, you can skip to the next section.
+
The builders take on the heavy lifting behind the scenes, offering two standout advantages: handling predicate data offsets and managing witness indexing.
+When your transaction involves predicates with dynamic data as inputs, like vectors, the dynamic data contains a pointer pointing to the beginning of the raw data. This pointer's validity hinges on the order of transaction inputs, and any shifting could render it invalid. However, the transaction builders conveniently postpone the resolution of these pointers until you finalize the build process.
+Similarly, adding signatures for signed coins requires the signed coin input to hold an index corresponding to the signature in the witnesses array. These indexes can also become invalid if the witness order changes. The Rust SDK again defers the resolution of these indexes until the transaction is finalized. It handles the assignment of correct index witnesses behind the scenes, sparing you the hassle of dealing with indexing intricacies during input definition.
+Another added benefit of the builder pattern is that it guards against changes once the transaction is finalized. The transactions resulting from a builder don't permit any changes to the struct that could cause the transaction ID to be modified. This eliminates the headache of calculating and storing a transaction ID for future use, only to accidentally modify the transaction later, resulting in a different transaction ID.
+Here is an example outlining some of the features of the transaction builders.
+In this scenario, we have a predicate that holds some bridged asset with ID bridged_asset_id. It releases it's locked assets if the transaction sends ask_amount of the base asset to the receiver address:
+ let ask_amount = 100;
+ let locked_amount = 500;
+ let bridged_asset_id = AssetId::from([1u8; 32]);
+ let receiver = Bech32Address::from_str(
+ "fuel1p8qt95dysmzrn2rmewntg6n6rg3l8ztueqafg5s6jmd9cgautrdslwdqdw",
+ )?;
+
+Our goal is to create a transaction that will use our hot wallet to transfer the ask_amount to the receiver and then send the unlocked predicate assets to a second wallet that acts as our cold storage.
+Let's start by instantiating a builder. Since we don't plan to deploy a contract, the ScriptTransactionBuilder
is the appropriate choice:
let tb = ScriptTransactionBuilder::default();
+
+Next, we need to define transaction inputs of the base asset that sum up to ask_amount. We also need transaction outputs that will assign those assets to the predicate address and thereby unlock it. The methods get_asset_inputs_for_amount
and get_asset_outputs_for_amount
can help with that. We need to specify the asset ID, the target amount, and the target address:
let base_inputs = hot_wallet
+ .get_asset_inputs_for_amount(*provider.base_asset_id(), ask_amount, None)
+ .await?;
+ let base_outputs = hot_wallet.get_asset_outputs_for_amount(
+ &receiver,
+ *provider.base_asset_id(),
+ ask_amount,
+ );
+
+Let's repeat the same process but this time for transferring the assets held by the predicate to our cold storage:
+ let other_asset_inputs = predicate
+ .get_asset_inputs_for_amount(bridged_asset_id, locked_amount, None)
+ .await?;
+ let other_asset_outputs =
+ predicate.get_asset_outputs_for_amount(cold_wallet.address(), bridged_asset_id, 500);
+
+We combine all of the inputs and outputs and set them on the builder:
+ let inputs = base_inputs
+ .into_iter()
+ .chain(other_asset_inputs.into_iter())
+ .collect();
+ let outputs = base_outputs
+ .into_iter()
+ .chain(other_asset_outputs.into_iter())
+ .collect();
+
+ let mut tb = tb.with_inputs(inputs).with_outputs(outputs);
+
+As we have used coins that require a signature, we have to add the signer to the transaction builder with:
+ tb.add_signer(hot_wallet.clone())?;
+
+++Note The signature is not created until the transaction is finalized with
+build(&provider)
We need to do one more thing before we stop thinking about transaction inputs. Executing the transaction also incurs a fee that is paid with the base asset. Our base asset inputs need to be large enough so that the total amount covers the transaction fee and any other operations we are doing. The Account
trait lets us use adjust_for_fee()
for adjusting the transaction inputs if needed to cover the fee. The second argument to adjust_for_fee()
is the total amount of the base asset that we expect our transaction to spend regardless of fees. In our case, this is the ask_amount we are transferring to the predicate.
hot_wallet.adjust_for_fee(&mut tb, 100).await?;
+
+++Note It is recommended to add signers before calling
+adjust_for_fee()
as the estimation will include the size of the witnesses.
We can also define transaction policies. For example, we can limit the gas price by doing the following:
+ let tx_policies = TxPolicies::default().with_tip(1);
+ let tb = tb.with_tx_policies(tx_policies);
+
+Our builder needs a signature from the hot wallet to unlock its coins before we call build()
and submit the resulting transaction through the provider:
let tx = tb.build(&provider).await?;
+ let tx_id = provider.send_transaction(tx).await?;
+
+Finally, we verify the transaction succeeded and that the cold storage indeed holds the bridged asset now:
+ let status = provider.tx_status(&tx_id).await?;
+ assert!(matches!(status, TxStatus::Success { .. }));
+
+ let balance = cold_wallet.get_asset_balance(&bridged_asset_id).await?;
+ assert_eq!(balance, locked_amount);
+
+If you need to build the transaction without signatures, which is useful when estimating transaction costs or simulations, you can change the build strategy used:
+ let mut tx = tb
+ .with_build_strategy(ScriptBuildStrategy::NoSignatures)
+ .build(provider)
+ .await?;
+ tx.sign_with(&wallet, provider.chain_id()).await?;
+
+++Note In contrast to adding signers to a transaction builder, when signing a built transaction, you must ensure that the order of signatures matches the order of signed inputs. Multiple signed inputs with the same owner will have the same witness index.
+
When preparing a contract call via CallHandler
, the Rust SDK uses a transaction builder in the background. You can fetch this builder and customize it before submitting it to the network. After the transaction is executed successfully, you can use the corresponding CallHandler
to generate a call response. The call response can be used to decode return values and logs. Below are examples for both contract and script calls.
let call_handler = contract_instance.methods().initialize_counter(counter);
+
+ let mut tb = call_handler.transaction_builder().await?;
+
+ // customize the builder...
+
+ wallet.adjust_for_fee(&mut tb, 0).await?;
+ tb.add_signer(wallet.clone())?;
+
+ let tx = tb.build(provider).await?;
+
+ let tx_id = provider.send_transaction(tx).await?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
+
+ let tx_status = provider.tx_status(&tx_id).await?;
+
+ let response = call_handler.get_response_from(tx_status)?;
+
+ assert_eq!(counter, response.value);
+
+ let script_call_handler = script_instance.main(1, 2);
+
+ let mut tb = script_call_handler.transaction_builder().await?;
+
+ // customize the builder...
+
+ wallet.adjust_for_fee(&mut tb, 0).await?;
+ tb.add_signer(wallet.clone())?;
+
+ let tx = tb.build(provider).await?;
+
+ let tx_id = provider.send_transaction(tx).await?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let tx_status = provider.tx_status(&tx_id).await?;
+
+ let response = script_call_handler.get_response_from(tx_status)?;
+
+ assert_eq!(response.value, "hello");
+
+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 std::str::FromStr;
+
+ use fuels::types::Bytes32;
+
+ // 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)?;
+ assert_eq!([0u8; 32], *b256);
+
+Bytes32
also implements the fmt
module'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!("{b256:#x}");
+
+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 toBytes32
; also used to represent hashes, and it holds a 256-bit value. In Rust, through the SDK, this is represented asBits256(value)
wherevalue
is a[u8; 32]
. If your contract method takes ab256
as input, all you need to do is pass aBits256([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 std::str::FromStr;
+
+ use fuels::types::Address;
+
+ // 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)?;
+ 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 std::str::FromStr;
+
+ use fuels::types::ContractId;
+
+ // 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)?;
+ 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 std::str::FromStr;
+
+ use fuels::types::AssetId;
+
+ // 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)?;
+ assert_eq!([0u8; 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::types::{bech32::Bech32Address, 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)?;
+ 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
fromAddress
orBech32ContractId
fromContractId
theHRP
(Human-Readable Part) is set to "fuel" per default.
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!(Contract(name="MyContract",
+ abi="e2e/sway/types/contracts/complex_types_contract/out/release/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.
+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);
+
+Sway supports unused generic type parameters when declaring structs/enums:
+struct SomeStruct<T, K> {
+ field: u64
+}
+
+enum SomeEnum<T, K> {
+ One: u64
+}
+
+
+If you tried the same in Rust you'd get complaints that T
and K
must be used or removed. When generating Rust bindings for such types we make use of the PhantomData
type. The generated bindings for the above example would look something like this:
struct SomeStruct<T, K> {
+ pub field: u64,
+ pub _unused_generic_0: PhantomData<T>
+ pub _unused_generic_1: PhantomData<K>
+}
+
+enum SomeEnum<T, K> {
+ One(u64),
+ IgnoreMe(PhantomData<T>, PhantomData<K>)
+}
+
+To lessen the impact to developer experience you may use the new
method to initialize a structure without bothering with the PhantomData
s.:
assert_eq!(
+ <StructUnusedGeneric<u16, u32>>::new(15),
+ StructUnusedGeneric {
+ field: 15,
+ _unused_generic_0: std::marker::PhantomData,
+ _unused_generic_1: std::marker::PhantomData
+ }
+ );
+
+If your struct doesn't have any fields we'll also derive Default
. As for enums all PhantomData
s are placed inside a new variant called IgnoreMe
which you'll need to ignore in your matches:
match my_enum {
+ EnumUnusedGeneric::One(_value) => {}
+ EnumUnusedGeneric::IgnoreMe(..) => panic!("Will never receive this variant"),
+ }
+
+String
The Rust SDK represents Fuel's String
s 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 SizedAsciiString
s 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>
.
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 b256 = Bits256([1; 32]);
+
+ let call_handler = contract_instance.methods().b256_as_input(b256);
+
+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]);
+
+Bytes
In Fuel, a type called Bytes
represents a collection of tightly-packed bytes. The Rust SDK represents Bytes
as Bytes(Vec<u8>)
. Here's an example of using Bytes
in a contract call:
let bytes = Bytes(vec![40, 41, 42]);
+
+ contract_methods.accept_bytes(bytes).call().await?;
+
+If you have a hexadecimal value as a string and wish to convert it to Bytes
, you may do so with from_hex_str
:
let hex_str = "0101010101010101010101010101010101010101010101010101010101010101";
+
+ let bytes = Bytes::from_hex_str(hex_str)?;
+
+ assert_eq!(bytes.0, vec![1u8; 32]);
+
+ // With the `0x0` prefix
+ let hex_str = "0x0101010101010101010101010101010101010101010101010101010101010101";
+
+ let bytes = Bytes::from_hex_str(hex_str)?;
+
+ assert_eq!(bytes.0, vec![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
+ value: Bits256,
+}
+
+Here's an example:
+ let b256 = Bits256::from_hex_str(
+ "0x1616060606060606060606060606060606060606060606060606060606060606",
+ )?;
+ let evm_address = EvmAddress::from(b256);
+
+ let call_handler = contract_instance
+ .methods()
+ .evm_address_as_input(evm_address);
+
+++Note: when creating an
+EvmAddress
fromBits256
, the first 12 bytes will be cleared because an EVM address is only 20 bytes long.
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 from contract methods is supported transparently, with the caveat that you cannot have them nested inside another type. This limitation is temporary.
+ let response = contract_methods.u8_in_vec(10).call().await?;
+ assert_eq!(response.value, (0..10).collect::<Vec<_>>());
+
+++Note: you can still interact with contracts containing methods that return vectors nested inside another type, just not interact with the methods themselves
+
Below you can find examples for common type conversions:
+Bytes32
Address
ContractId
Identity
AssetId
Bech32
str
Bits256
Bytes
B512
EvmAddress
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::types::{AssetId, ContractId};
+
+ let contract_id = ContractId::new([1u8; 32]);
+
+ let asset_id: AssetId = AssetId::new(*contract_id);
+
+ assert_eq!([1u8; 32], *asset_id);
+
+Bytes32
Convert a [u8; 32]
array to Bytes32
:
let my_slice = [1u8; 32];
+ let b256 = Bytes32::new(my_slice);
+
+Convert a hex string to Bytes32
:
let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
+ let b256 = Bytes32::from_str(hex_str)?;
+
+Address
Convert a [u8; 32]
array to an Address
:
let my_slice = [1u8; 32];
+ let address = Address::new(my_slice);
+
+Convert a Bech32
address to an Address
:
let _plain_address: Address = bech32_address.into();
+
+Convert a wallet to an Address
:
let wallet_unlocked = WalletUnlocked::new_random(None);
+ let address: Address = wallet_unlocked.address().into();
+
+Convert a hex string to an Address
:
let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
+ let address = Address::from_str(hex_str)?;
+
+ContractId
Convert a [u8; 32]
array to ContractId
:
let my_slice = [1u8; 32];
+ let contract_id = ContractId::new(my_slice);
+
+Convert a hex string to a ContractId
:
let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
+ let contract_id = ContractId::from_str(hex_str)?;
+
+Convert a contract instance to a ContractId
:
let contract_id: ContractId = contract_instance.id().into();
+
+Identity
Convert an Address
to an Identity
:
let _identity_from_address = Identity::Address(address);
+
+Convert a ContractId
to an Identity
:
let _identity_from_contract_id = Identity::ContractId(contract_id);
+
+AssetId
Convert a [u8; 32]
array to an AssetId
:
let my_slice = [1u8; 32];
+ let asset_id = AssetId::new(my_slice);
+
+Convert a hex string to an AssetId
:
let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
+ let asset_id = AssetId::from_str(hex_str)?;
+
+Bech32
Convert a [u8; 32]
array to a Bech32
address:
let hrp = "fuel";
+ let my_slice = [1u8; 32];
+ let _bech32_address = Bech32Address::new(hrp, my_slice);
+
+Convert Bytes32
to a Bech32
address:
let my_hash = Bytes32::new([1u8; 32]);
+ let _bech32_address = Bech32Address::new(hrp, my_hash);
+
+Convert a string to a Bech32
address:
let address = "fuel1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsx2mt2";
+ let bech32_address = Bech32Address::from_str(address)?;
+
+Convert an Address
to a Bech32
address:
let plain_address = Address::new([0u8; 32]);
+ let bech32_address = Bech32Address::from(plain_address);
+
+str
Convert a ContractId
to a str
:
let _str_from_contract_id: &str = contract_id.to_string().as_str();
+
+Convert an Address
to a str
:
let _str_from_address: &str = address.to_string().as_str();
+
+Convert an AssetId
to a str
:
let _str_from_asset_id: &str = asset_id.to_string().as_str();
+
+Convert Bytes32
to a str
:
let _str_from_bytes32: &str = b256.to_string().as_str();
+
+Bits256
Convert a hex string to Bits256
:
let hex_str = "0x0101010101010101010101010101010101010101010101010101010101010101";
+
+ let bits256 = Bits256::from_hex_str(hex_str)?;
+
+Convert a ContractId
to Bits256
:
let _contract_id_to_bits_256 = Bits256(contract_id.into());
+
+Convert an Address
to Bits256
:
let bits_256 = Bits256(address.into());
+
+Convert an AssetId
to Bits256
:
let _asset_id_to_bits_256 = Bits256(asset_id.into());
+
+Bytes
Convert a string to Bytes
:
let hex_str = "0x0101010101010101010101010101010101010101010101010101010101010101";
+
+ let bytes = Bytes::from_hex_str(hex_str)?;
+
+B512
Convert two hex strings to B512
:
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
Convert a Bits256
address to an EvmAddress
:
let _evm_address = EvmAddress::from(bits_256);
+
+Encoding and decoding are done as per the fuel spec. To this end, fuels
makes use of the ABIEncoder
and the ABIDecoder
.
To encode a type, you must first convert it into a Token
. This is commonly done by implementing the Tokenizable
trait.
To decode, you also need to provide a ParamType
describing the schema of the type in question. This is commonly done by implementing the Parameterize trait.
All types generated by the abigen!
macro implement both the Tokenizable
and Parameterize
traits.
fuels
also contains implementations for:
Tokenizable
for the fuels
-owned types listed here as well as for some foreign types (such as u8
, u16
, std::vec::Vec<T: Tokenizable>
, etc.).Parameterize
for the fuels
-owned types listed here as well as for some foreign types (such as u8
, u16
, std::vec::Vec<T: Parameterize>
, etc.).Both Tokenizable
and Parameterize
can be derived for struct
s and enum
s if all inner types implement the derived traits:
use fuels::macros::{Parameterize, Tokenizable};
+
+ #[derive(Parameterize, Tokenizable)]
+ struct MyStruct {
+ field_a: u8,
+ }
+
+ #[derive(Parameterize, Tokenizable)]
+ enum SomeEnum {
+ A(MyStruct),
+ B(Vec<u64>),
+ }
+
+++Note: +Deriving
+Tokenizable
onenum
s requires that all variants also implementParameterize
.
The derived code expects that the fuels
package is accessible through ::fuels
. If this is not the case then the derivation macro needs to be given the locations of fuels::types
and fuels::core
.
#[derive(Parameterize, Tokenizable)]
+ #[FuelsCorePath = "fuels_core_elsewhere"]
+ #[FuelsTypesPath = "fuels_types_elsewhere"]
+ pub struct SomeStruct {
+ field_a: u64,
+ }
+
+If you want no-std
generated code:
use fuels::macros::{Parameterize, Tokenizable};
+ #[derive(Parameterize, Tokenizable)]
+ #[NoStd]
+ pub struct SomeStruct {
+ field_a: u64,
+ }
+
+Be sure to read the prerequisites to encoding.
+Encoding is done via the ABIEncoder
:
use fuels::{
+ core::{codec::ABIEncoder, traits::Tokenizable},
+ macros::Tokenizable,
+ };
+
+ #[derive(Tokenizable)]
+ struct MyStruct {
+ field: u64,
+ }
+
+ let instance = MyStruct { field: 101 };
+ let _encoded: Vec<u8> = ABIEncoder::default().encode(&[instance.into_token()])?;
+
+There is also a shortcut-macro that can encode multiple types which implement Tokenizable
:
use fuels::{core::codec::calldata, macros::Tokenizable};
+
+ #[derive(Tokenizable)]
+ struct MyStruct {
+ field: u64,
+ }
+ let _: Vec<u8> = calldata!(MyStruct { field: 101 }, MyStruct { field: 102 })?;
+
+The encoder can be configured to limit its resource expenditure:
+ use fuels::core::codec::ABIEncoder;
+
+ ABIEncoder::new(EncoderConfig {
+ max_depth: 5,
+ max_tokens: 100,
+ });
+
+The default values for the EncoderConfig
are:
impl Default for EncoderConfig {
+ fn default() -> Self {
+ Self {
+ max_depth: 45,
+ max_tokens: 10_000,
+ }
+ }
+}
+
+You can also configure the encoder used to encode the arguments of the contract method:
+ let _ = contract_instance
+ .with_encoder_config(EncoderConfig {
+ max_depth: 10,
+ max_tokens: 2_000,
+ })
+ .methods()
+ .initialize_counter(42)
+ .call()
+ .await?;
+
+The same method is available for script calls.
+Be sure to read the prerequisites to decoding.
+Decoding is done via the ABIDecoder
:
use fuels::{
+ core::{
+ codec::ABIDecoder,
+ traits::{Parameterize, Tokenizable},
+ },
+ macros::{Parameterize, Tokenizable},
+ types::Token,
+ };
+
+ #[derive(Parameterize, Tokenizable)]
+ struct MyStruct {
+ field: u64,
+ }
+
+ let bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 101];
+
+ let token: Token = ABIDecoder::default().decode(&MyStruct::param_type(), bytes)?;
+
+ let _: MyStruct = MyStruct::from_token(token)?;
+
+First into a Token
, then via the Tokenizable
trait, into the desired type.
If the type came from abigen!
(or uses the ::fuels::macros::TryFrom
derivation) then you can also use try_into
to convert bytes into a type that implements both Parameterize
and Tokenizable
:
use fuels::macros::{Parameterize, Tokenizable, TryFrom};
+
+ #[derive(Parameterize, Tokenizable, TryFrom)]
+ struct MyStruct {
+ field: u64,
+ }
+
+ let bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 101];
+
+ let _: MyStruct = bytes.try_into()?;
+
+Under the hood, try_from_bytes
is being called, which does what the preceding example did.
The decoder can be configured to limit its resource expenditure:
+
+ use fuels::core::codec::ABIDecoder;
+
+ ABIDecoder::new(DecoderConfig {
+ max_depth: 5,
+ max_tokens: 100,
+ });
+
+
+
+For an explanation of each configuration value visit the DecoderConfig
.
The default values for the DecoderConfig
are:
impl Default for DecoderConfig {
+ fn default() -> Self {
+ Self {
+ max_depth: 45,
+ max_tokens: 10_000,
+ }
+ }
+}
+
+You can also configure the decoder used to decode the return value of the contract method:
+ let _ = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .with_decoder_config(DecoderConfig {
+ max_depth: 10,
+ max_tokens: 2_000,
+ })
+ .call()
+ .await?;
+
+The same method is available for script calls.
+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.
+fuels-rs
Testing++ +note This section is still a work in progress.
+
If you're new to Rust, you'll want to review these important tools to help you build tests.
+assert!
macroYou can use the assert!
macro to assert certain conditions in your test. This macro invokes panic!()
and fails the test if the expression inside evaluates to false
.
assert!(value == 5);
+
+
+assert_eq!
macroThe assert_eq!
macro works a lot like the assert
macro, however instead it accepts two values, and panics if those values are not equal.
assert_eq!(balance, 100);
+
+
+assert_ne!
macroThe assert_ne!
macro works just like the assert_eq!
macro, but it will panic if the two values are equal.
assert_ne!(address, 0);
+
+
+println!
macroYou can use the println!
macro to print values to the console.
println!("WALLET 1 ADDRESS {}", wallet_1.address());
+println!("WALLET 1 ADDRESS {:?}", wallet_1.address());
+
+
+
+
+Using {}
will print the value, and using {:?}
will print the value plus its type.
Using {:?}
will also allow you to print values that do not have the Display
trait implemented but do have the Debug
trait. Alternatively you can use the dbg!
macro to print these types of variables.
println!("WALLET 1 PROVIDER {:?}", wallet_1.provider().unwrap());
+dbg!("WALLET 1 PROVIDER {}", wallet_1.provider().unwrap());
+
+
+
+
+To print more complex types that don't have it already, you can implement your own formatted display method with the fmt
module from the Rust standard library.
use std::fmt;
+
+struct Point {
+ x: u64,
+ y: u64,
+}
+
+// add print functionality with the fmt module
+impl fmt::Display for Point {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "value of x: {}, value of y: {}", self.x, self.y)
+ }
+}
+
+let p = Point {x: 1, y: 2};
+println!("POINT: {}", p);
+
+
+You can run your tests to see if they pass or fail with
+cargo test
+
+
+
+Outputs will be hidden if the test passes. If you want to see outputs printed from your tests regardless of whether they pass or fail, use the nocapture
flag.
cargo test -- --nocapture
+
+When deploying contracts with the abigen!
macro, as shown in the previous sections, the user can:
However, it is often the case that we want to quickly set up a test with default values and work directly with contract or script instances. The setup_program_test!
can do exactly that.
Used to reduce boilerplate in integration tests. Accepts input in the form
+of COMMAND(ARG...)...
COMMAND
is either Wallets
, Abigen
, LoadScript
or Deploy
.
ARG
is either a:
name="MyContract"
), or,"some_str_literal"
, true
, 5
, ...)Abigen(Contract(name="MyContract", project="some_project"))
)Available COMMAND
s:
Example: Options(profile="debug")
Description: Sets options from ARG
s to be used by other COMMAND
s.
Available options:
+profile
: sets the cargo
build profile. Variants: "release"
(default), "debug"
Cardinality: 0 or 1.
+Example: Wallets("a_wallet", "another_wallet"...)
Description: Launches a local provider and generates wallets with names taken from the provided ARG
s.
Cardinality: 0 or 1.
+Example:
+Abigen(
+ Contract(
+ name = "MyContract",
+ project = "some_folder"
+ ),
+ Script(
+ name = "MyScript",
+ project = "some_folder"
+ ),
+ Predicate(
+ name = "MyPredicate",
+ project = "some_folder"
+ ),
+)
+
+Description: Generates the program bindings under the name name
. project
should point to root of the forc
project. The project must be compiled in release
mode (--release
flag) for Abigen
command to work.
Cardinality: 0 or N.
+Example: Deploy(name="instance_name", contract="MyContract", wallet="a_wallet")
Description: Deploys the contract
(with salt) using wallet
. Will create a contract instance accessible via name
. Due to salt usage, the same contract can be deployed multiple times. Requires that an Abigen
command be present with name
equal to contract
. wallet
can either be one of the wallets in the Wallets
COMMAND
or the name of a wallet you've previously generated yourself.
Cardinality: 0 or N.
+LoadScript
Example: LoadScript(name = "script_instance", script = "MyScript", wallet = "wallet")
Description: Creates a script instance of script
under name
using wallet
.
Cardinality: 0 or N.
+The setup code that you have seen in previous sections gets reduced to:
+ setup_program_test!(
+ Wallets("wallet"),
+ Abigen(Contract(
+ name = "TestContract",
+ project = "e2e/sway/contracts/contract_test"
+ )),
+ Deploy(
+ name = "contract_instance",
+ contract = "TestContract",
+ wallet = "wallet"
+ ),
+ );
+
+ let response = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .call()
+ .await?;
+
+ assert_eq!(42, response.value);
+
+++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 by referencing the same wallet in the
+Deploy
command.
setup_program_test!(
+ Wallets("wallet"),
+ Abigen(
+ Contract(
+ name = "LibContract",
+ project = "e2e/sway/contracts/lib_contract"
+ ),
+ Contract(
+ name = "LibContractCaller",
+ project = "e2e/sway/contracts/lib_contract_caller"
+ ),
+ ),
+ Deploy(
+ name = "lib_contract_instance",
+ contract = "LibContract",
+ wallet = "wallet",
+ random_salt = false,
+ ),
+ Deploy(
+ name = "contract_caller_instance",
+ contract = "LibContractCaller",
+ wallet = "wallet",
+ ),
+ Deploy(
+ name = "contract_caller_instance2",
+ contract = "LibContractCaller",
+ wallet = "wallet",
+ ),
+ );
+ let lib_contract_id = lib_contract_instance.contract_id();
+
+ let contract_caller_id = contract_caller_instance.contract_id();
+
+ let contract_caller_id2 = contract_caller_instance2.contract_id();
+
+ // Because we deploy with salt, we can deploy the same contract multiple times
+ assert_ne!(contract_caller_id, contract_caller_id2);
+
+ // The first contract can be called because they were deployed on the same provider
+ let response = contract_caller_instance
+ .methods()
+ .increment_from_contract(lib_contract_id, 42)
+ .with_contracts(&[&lib_contract_instance])
+ .call()
+ .await?;
+
+ assert_eq!(43, response.value);
+
+ let response = contract_caller_instance2
+ .methods()
+ .increment_from_contract(lib_contract_id, 42)
+ .with_contracts(&[&lib_contract_instance])
+ .call()
+ .await?;
+
+ assert_eq!(43, response.value);
+
+In this example, three contracts are deployed on the same provider using the wallet
generated by the Wallets
command. The second and third macros use the same contract but have different IDs because of the deployment with salt. Both of them can call the first contract by using their 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_program_test!(
+ Abigen(Contract(
+ name = "TestContract",
+ project = "e2e/sway/contracts/contract_test"
+ )),
+ Deploy(
+ name = "contract_instance",
+ contract = "TestContract",
+ wallet = "wallet",
+ random_salt = false,
+ ),
+ );
+
+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 havemanual_blocks_enabled = true
in the config for the running node. See example below.
let wallets =
+ launch_custom_provider_and_get_wallets(WalletsConfig::default(), None, None).await?;
+ let wallet = &wallets[0];
+ let provider = wallet.try_provider()?;
+
+ assert_eq!(provider.latest_block_height().await?, 0u32);
+
+ provider.produce_blocks(3, None).await?;
+
+ assert_eq!(provider.latest_block_height().await?, 3u32);
+
+You can also set a custom block time as the second, optional argument. Here is an example:
+ let block_time = 20u32; // seconds
+ let config = NodeConfig {
+ // This is how you specify the time between blocks
+ block_production: Trigger::Interval {
+ block_time: std::time::Duration::from_secs(block_time.into()),
+ },
+ ..NodeConfig::default()
+ };
+ let wallets =
+ launch_custom_provider_and_get_wallets(WalletsConfig::default(), Some(config), None)
+ .await?;
+ let wallet = &wallets[0];
+ let provider = wallet.try_provider()?;
+
+ assert_eq!(provider.latest_block_height().await?, 0u32);
+ let origin_block_time = provider.latest_block_time().await?.unwrap();
+ let blocks_to_produce = 3;
+
+ provider.produce_blocks(blocks_to_produce, None).await?;
+ assert_eq!(provider.latest_block_height().await?, blocks_to_produce);
+ let expected_latest_block_time = origin_block_time
+ .checked_add_signed(Duration::try_seconds((blocks_to_produce * block_time) as i64).unwrap())
+ .unwrap();
+ assert_eq!(
+ provider.latest_block_time().await?.unwrap(),
+ expected_latest_block_time
+ );
+
+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.
+
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
and ChainConfig
:
use fuels::{
+ prelude::*,
+ tx::{ConsensusParameters, FeeParameters, TxParameters},
+ };
+
+Next, we can define some values for the consensus parameters:
+ let tx_params = TxParameters::default()
+ .with_max_gas_per_tx(1_000)
+ .with_max_inputs(2);
+ let fee_params = FeeParameters::default().with_gas_price_factor(10);
+
+ let mut consensus_parameters = ConsensusParameters::default();
+ consensus_parameters.set_tx_params(tx_params);
+ consensus_parameters.set_fee_params(fee_params);
+
+ let chain_config = ChainConfig {
+ consensus_parameters,
+ ..ChainConfig::default()
+ };
+
+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_provider()
, which starts a node with the given configurations and returns a
+provider attached to that node:
let node_config = NodeConfig::default();
+ let _provider =
+ setup_test_provider(coins, vec![], Some(node_config), Some(chain_config)).await?;
+
+Consider the following contract:
+contract;
+
+use std::{
+ asset::{
+ mint_to,
+ transfer,
+ },
+ call_frames::{
+ msg_asset_id,
+ },
+ constants::ZERO_B256,
+ context::msg_amount,
+};
+
+abi LiquidityPool {
+ #[payable]
+ fn deposit(recipient: Identity);
+ #[payable]
+ fn withdraw(recipient: Identity);
+}
+
+const BASE_TOKEN: AssetId = AssetId::from(0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c);
+
+impl LiquidityPool for Contract {
+ #[payable]
+ fn deposit(recipient: Identity) {
+ assert(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(recipient, ZERO_B256, amount_to_mint);
+ }
+
+ #[payable]
+ fn withdraw(recipient: Identity) {
+ assert(0 < msg_amount());
+
+ // Amount to withdraw.
+ let amount_to_transfer = msg_amount() / 2;
+
+ // Transfer base token to recipient.
+ transfer(recipient, BASE_TOKEN, amount_to_transfer);
+ }
+}
+
+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!(Contract(
+ name = "MyContract",
+ abi = "e2e/sway/contracts/liquidity_pool/out/release/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()?;
+
+ let asset_ids = [AssetId::zeroed(), 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::load_from(
+ "../../e2e/sway/contracts/liquidity_pool/out/release/liquidity_pool.bin",
+ LoadConfiguration::default(),
+ )?
+ .deploy(wallet, TxPolicies::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::default()
+ .with_amount(deposit_amount)
+ .with_asset_id(base_asset_id);
+
+ contract_methods
+ .deposit(wallet.address().into())
+ .call_params(call_params)?
+ .with_variable_output_policy(VariableOutputPolicy::Exactly(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 = contract_id.asset_id(&Bits256::zeroed());
+ let lp_token_balance = wallet.get_asset_balance(&lp_asset_id).await?;
+
+ let call_params = CallParameters::default()
+ .with_amount(lp_token_balance)
+ .with_asset_id(lp_asset_id);
+
+ contract_methods
+ .withdraw(wallet.address().into())
+ .call_params(call_params)?
+ .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
+ .call()
+ .await?;
+
+ let base_balance = wallet.get_asset_balance(&base_asset_id).await?;
+ assert_eq!(base_balance, deposit_amount);
+
+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.
We transfer only a part of the base asset balance so that the rest can cover 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)?;
+
+ let input = wallet_1
+ .get_asset_inputs_for_amount(id, amount, None)
+ .await?;
+ inputs.extend(input);
+
+ // we don't transfer the full base asset so we can cover fees
+ let output = if id == *provider.base_asset_id() {
+ wallet_1.get_asset_outputs_for_amount(wallet_2.address(), id, amount / 2)
+ } else {
+ wallet_1.get_asset_outputs_for_amount(wallet_2.address(), id, amount)
+ };
+
+ outputs.extend(output);
+ }
+
+All that is left is to build the transaction via ScriptTransactionBuilder
, 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 tb =
+ ScriptTransactionBuilder::prepare_transfer(inputs, outputs, TxPolicies::default());
+ tb.add_signer(wallet_1.clone())?;
+
+ let tx = tb.build(&provider).await?;
+
+ provider.send_transaction_and_await_commit(tx).await?;
+
+ let balances = wallet_2.get_balances().await?;
+
+ assert_eq!(balances.len(), NUM_ASSETS as usize);
+ for (id, balance) in balances {
+ if id == provider.base_asset_id().to_string() {
+ assert_eq!(balance, AMOUNT / 2);
+ } else {
+ assert_eq!(balance, AMOUNT);
+ }
+ }
+
+++ +note This section is still a work in progress.
+
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 selector = encode_fn_selector(fn_name);
+
+ assert_eq!(
+ selector,
+ [0, 0, 0, 0, 0, 0, 0, 12, 115, 111, 109, 101, 95, 102, 110, 95, 110, 97, 109, 101]
+ );
+
+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.
+ +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.
+ +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.
Thanks for your interest in contributing to the Fuel Rust SDK!
+This document outlines the process for installing dependencies, setting up for development, and conventions for contributing.`
+If you run into any difficulties getting started, you can always ask questions on our Discourse.
+You may contribute to the project in many ways, some of which involve coding knowledge and some which do not. A few examples include:
+Check out our Help Wanted or Good First Issues to find a suitable task.
+If you are planning something big, for example, changes related to multiple components or changes to current behaviors, make sure to open an issue to discuss with us before starting on the implementation.
+This is a rough outline of what a contributor's workflow looks like:
+Thanks for your contributions!
+Pull requests should be linked to at least one issue in the same repo.
+If the pull request resolves the relevant issues, and you want GitHub to close these issues automatically after it merged into the default branch, you can use the syntax (KEYWORD #ISSUE-NUMBER
) like this:
close #123
+
+If the pull request links an issue but does not close it, you can use the keyword ref
like this:
ref #456
+
+Multiple issues should use full syntax for each issue and be separated by a comma, like:
+close #123, ref #456
+
+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:
+Fuels Rust SDK
book - e.g. Types
storage.rs
Otherwise, we recommend putting the integration test inside the existing categories above.
+fuels-rs
Rust WorkspacesThis 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.
+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)
+
+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.
+$ 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 parameter 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>
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
+
+Similar to encoding parameters only:
+$ cargo run -- decode params -t bool -t u32 -t u32 0000000000000001000000000000002a0000000000000064
+Bool(true)
+U32(42)
+U32(100)
+
+$ cargo run -- decode function examples/simple.json takes_u32_returns_bool 0000000000000001
+Bool(true)
+
+
+ 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.
+ +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 abigen!
macro seen previously.
// The abigen is used for the same purpose as with contracts (Rust bindings)
+ abigen!(Script(
+ name = "MyScript",
+ abi = "e2e/sway/scripts/arguments/out/release/arguments-abi.json"
+ ));
+ let wallet = launch_provider_and_get_wallet().await?;
+ let bin_path = "sway/scripts/arguments/out/release/arguments.bin";
+ let script_instance = MyScript::new(wallet, bin_path);
+
+ let bim = Bimbam { val: 90 };
+ let bam = SugarySnack {
+ twix: 100,
+ mars: 1000,
+ };
+
+ let result = script_instance.main(bim, bam).call().await?;
+
+ let expected = Bimbam { val: 2190 };
+ assert_eq!(result.value, expected);
+
+Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows:
+ let submitted_tx = script_instance.main(my_struct).submit().await?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let value = submitted_tx.response().await?.value;
+
+The method for passing transaction policies is the same as with contracts. As a reminder, the workflow would look like this:
+ let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000);
+ let result = script_instance
+ .main(a, b)
+ .with_tx_policies(tx_policies)
+ .call()
+ .await?;
+
+Script calls provide the same logging functions, decode_logs()
and decode_logs_with_type<T>()
, as contract calls. As a reminder, the workflow looks like this:
abigen!(Script(
+ name = "log_script",
+ abi = "e2e/sway/logs/script_logs/out/release/script_logs-abi.json"
+ ));
+
+ let wallet = launch_provider_and_get_wallet().await?;
+ let bin_path = "sway/logs/script_logs/out/release/script_logs.bin";
+ let instance = log_script::new(wallet.clone(), bin_path);
+
+ let response = instance.main().call().await?;
+
+ let logs = response.decode_logs();
+ let log_u64 = response.decode_logs_with_type::<u64>()?;
+
+Scripts use the same interfaces for setting external contracts as contract methods.
+Below is an example that uses with_contracts(&[&contract_instance, ...])
.
let response = script_instance
+ .main(contract_id)
+ .with_contracts(&[&contract_instance])
+ .call()
+ .await?;
+
+And this is an example that uses with_contract_ids(&[&contract_id, ...])
.
let response = script_instance
+ .main(contract_id)
+ .with_contract_ids(&[contract_id.into()])
+ .call()
+ .await?;
+
+Same as contracts, you can define configurable
constants in scripts
which can be changed during the script execution. Here is an example how the constants are defined.
script;
+
+#[allow(dead_code)]
+enum EnumWithGeneric<D> {
+ VariantOne: D,
+ VariantTwo: (),
+}
+
+struct StructWithGeneric<D> {
+ field_1: D,
+ field_2: u64,
+}
+
+configurable {
+ BOOL: bool = true,
+ U8: u8 = 8,
+ U16: u16 = 16,
+ U32: u32 = 32,
+ U64: u64 = 63,
+ U256: u256 = 0x0000000000000000000000000000000000000000000000000000000000000008u256,
+ B256: b256 = 0x0101010101010101010101010101010101010101010101010101010101010101,
+ STR_4: str[4] = __to_str_array("fuel"),
+ TUPLE: (u8, bool) = (8, true),
+ ARRAY: [u32; 3] = [253, 254, 255],
+ STRUCT: StructWithGeneric<u8> = StructWithGeneric {
+ field_1: 8,
+ field_2: 16,
+ },
+ ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true),
+}
+//U128: u128 = 128, //TODO: add once https://github.com/FuelLabs/sway/issues/5356 is done
+
+fn main() -> (bool, u8, u16, u32, u64, u256, b256, str[4], (u8, bool), [u32; 3], StructWithGeneric<u8>, EnumWithGeneric<bool>) {
+ (BOOL, U8, U16, U32, U64, U256, B256, STR_4, TUPLE, ARRAY, STRUCT, ENUM)
+}
+
+Each configurable constant will get a dedicated with
method in the SDK. For example, the constant STR_4
will get the with_STR_4
method which accepts the same type defined in sway. Below is an example where we chain several with
methods and execute the script with the new constants.
abigen!(Script(
+ name = "MyScript",
+ abi = "e2e/sway/scripts/script_configurables/out/release/script_configurables-abi.json"
+ ));
+
+ let wallet = launch_provider_and_get_wallet().await?;
+ let bin_path = "sway/scripts/script_configurables/out/release/script_configurables.bin";
+ let instance = MyScript::new(wallet, bin_path);
+
+ let str_4: SizedAsciiString<4> = "FUEL".try_into()?;
+ let new_struct = StructWithGeneric {
+ field_1: 16u8,
+ field_2: 32,
+ };
+ let new_enum = EnumWithGeneric::VariantTwo;
+
+ let configurables = MyScriptConfigurables::new(EncoderConfig {
+ max_tokens: 5,
+ ..Default::default()
+ })
+ .with_BOOL(false)?
+ .with_U8(7)?
+ .with_U16(15)?
+ .with_U32(31)?
+ .with_U64(63)?
+ .with_U256(U256::from(8))?
+ .with_B256(Bits256([2; 32]))?
+ .with_STR_4(str_4.clone())?
+ .with_TUPLE((7, false))?
+ .with_ARRAY([252, 253, 254])?
+ .with_STRUCT(new_struct.clone())?
+ .with_ENUM(new_enum.clone())?;
+
+ let response = instance
+ .with_configurables(configurables)
+ .main()
+ .call()
+ .await?;
+
+
+ If you're new to Rust, you'll want to review these important tools to help you build tests.
+assert!
macroYou can use the assert!
macro to assert certain conditions in your test. This macro invokes panic!()
and fails the test if the expression inside evaluates to false
.
assert!(value == 5);
+
+
+assert_eq!
macroThe assert_eq!
macro works a lot like the assert
macro, however instead it accepts two values, and panics if those values are not equal.
assert_eq!(balance, 100);
+
+
+assert_ne!
macroThe assert_ne!
macro works just like the assert_eq!
macro, but it will panic if the two values are equal.
assert_ne!(address, 0);
+
+
+println!
macroYou can use the println!
macro to print values to the console.
println!("WALLET 1 ADDRESS {}", wallet_1.address());
+println!("WALLET 1 ADDRESS {:?}", wallet_1.address());
+
+
+
+
+Using {}
will print the value, and using {:?}
will print the value plus its type.
Using {:?}
will also allow you to print values that do not have the Display
trait implemented but do have the Debug
trait. Alternatively you can use the dbg!
macro to print these types of variables.
println!("WALLET 1 PROVIDER {:?}", wallet_1.provider().unwrap());
+dbg!("WALLET 1 PROVIDER {}", wallet_1.provider().unwrap());
+
+
+
+
+To print more complex types that don't have it already, you can implement your own formatted display method with the fmt
module from the Rust standard library.
use std::fmt;
+
+struct Point {
+ x: u64,
+ y: u64,
+}
+
+// add print functionality with the fmt module
+impl fmt::Display for Point {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "value of x: {}, value of y: {}", self.x, self.y)
+ }
+}
+
+let p = Point {x: 1, y: 2};
+println!("POINT: {}", p);
+
+
+You can run your tests to see if they pass or fail with
+cargo test
+
+
+
+Outputs will be hidden if the test passes. If you want to see outputs printed from your tests regardless of whether they pass or fail, use the nocapture
flag.
cargo test -- --nocapture
+
+
+ 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 havemanual_blocks_enabled = true
in the config for the running node. See example below.
let wallets =
+ launch_custom_provider_and_get_wallets(WalletsConfig::default(), None, None).await?;
+ let wallet = &wallets[0];
+ let provider = wallet.try_provider()?;
+
+ assert_eq!(provider.latest_block_height().await?, 0u32);
+
+ provider.produce_blocks(3, None).await?;
+
+ assert_eq!(provider.latest_block_height().await?, 3u32);
+
+You can also set a custom block time as the second, optional argument. Here is an example:
+ let block_time = 20u32; // seconds
+ let config = NodeConfig {
+ // This is how you specify the time between blocks
+ block_production: Trigger::Interval {
+ block_time: std::time::Duration::from_secs(block_time.into()),
+ },
+ ..NodeConfig::default()
+ };
+ let wallets =
+ launch_custom_provider_and_get_wallets(WalletsConfig::default(), Some(config), None)
+ .await?;
+ let wallet = &wallets[0];
+ let provider = wallet.try_provider()?;
+
+ assert_eq!(provider.latest_block_height().await?, 0u32);
+ let origin_block_time = provider.latest_block_time().await?.unwrap();
+ let blocks_to_produce = 3;
+
+ provider.produce_blocks(blocks_to_produce, None).await?;
+ assert_eq!(provider.latest_block_height().await?, blocks_to_produce);
+ let expected_latest_block_time = origin_block_time
+ .checked_add_signed(Duration::try_seconds((blocks_to_produce * block_time) as i64).unwrap())
+ .unwrap();
+ assert_eq!(
+ provider.latest_block_time().await?.unwrap(),
+ expected_latest_block_time
+ );
+
+
+ fuels-rs
Testing++ + +note This section is still a work in progress.
+
When deploying contracts with the abigen!
macro, as shown in the previous sections, the user can:
However, it is often the case that we want to quickly set up a test with default values and work directly with contract or script instances. The setup_program_test!
can do exactly that.
Used to reduce boilerplate in integration tests. Accepts input in the form
+of COMMAND(ARG...)...
COMMAND
is either Wallets
, Abigen
, LoadScript
or Deploy
.
ARG
is either a:
name="MyContract"
), or,"some_str_literal"
, true
, 5
, ...)Abigen(Contract(name="MyContract", project="some_project"))
)Available COMMAND
s:
Example: Options(profile="debug")
Description: Sets options from ARG
s to be used by other COMMAND
s.
Available options:
+profile
: sets the cargo
build profile. Variants: "release"
(default), "debug"
Cardinality: 0 or 1.
+Example: Wallets("a_wallet", "another_wallet"...)
Description: Launches a local provider and generates wallets with names taken from the provided ARG
s.
Cardinality: 0 or 1.
+Example:
+Abigen(
+ Contract(
+ name = "MyContract",
+ project = "some_folder"
+ ),
+ Script(
+ name = "MyScript",
+ project = "some_folder"
+ ),
+ Predicate(
+ name = "MyPredicate",
+ project = "some_folder"
+ ),
+)
+
+Description: Generates the program bindings under the name name
. project
should point to root of the forc
project. The project must be compiled in release
mode (--release
flag) for Abigen
command to work.
Cardinality: 0 or N.
+Example: Deploy(name="instance_name", contract="MyContract", wallet="a_wallet")
Description: Deploys the contract
(with salt) using wallet
. Will create a contract instance accessible via name
. Due to salt usage, the same contract can be deployed multiple times. Requires that an Abigen
command be present with name
equal to contract
. wallet
can either be one of the wallets in the Wallets
COMMAND
or the name of a wallet you've previously generated yourself.
Cardinality: 0 or N.
+LoadScript
Example: LoadScript(name = "script_instance", script = "MyScript", wallet = "wallet")
Description: Creates a script instance of script
under name
using wallet
.
Cardinality: 0 or N.
+The setup code that you have seen in previous sections gets reduced to:
+ setup_program_test!(
+ Wallets("wallet"),
+ Abigen(Contract(
+ name = "TestContract",
+ project = "e2e/sway/contracts/contract_test"
+ )),
+ Deploy(
+ name = "contract_instance",
+ contract = "TestContract",
+ wallet = "wallet"
+ ),
+ );
+
+ let response = contract_instance
+ .methods()
+ .initialize_counter(42)
+ .call()
+ .await?;
+
+ assert_eq!(42, response.value);
+
+++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 by referencing the same wallet in the
+Deploy
command.
setup_program_test!(
+ Wallets("wallet"),
+ Abigen(
+ Contract(
+ name = "LibContract",
+ project = "e2e/sway/contracts/lib_contract"
+ ),
+ Contract(
+ name = "LibContractCaller",
+ project = "e2e/sway/contracts/lib_contract_caller"
+ ),
+ ),
+ Deploy(
+ name = "lib_contract_instance",
+ contract = "LibContract",
+ wallet = "wallet",
+ random_salt = false,
+ ),
+ Deploy(
+ name = "contract_caller_instance",
+ contract = "LibContractCaller",
+ wallet = "wallet",
+ ),
+ Deploy(
+ name = "contract_caller_instance2",
+ contract = "LibContractCaller",
+ wallet = "wallet",
+ ),
+ );
+ let lib_contract_id = lib_contract_instance.contract_id();
+
+ let contract_caller_id = contract_caller_instance.contract_id();
+
+ let contract_caller_id2 = contract_caller_instance2.contract_id();
+
+ // Because we deploy with salt, we can deploy the same contract multiple times
+ assert_ne!(contract_caller_id, contract_caller_id2);
+
+ // The first contract can be called because they were deployed on the same provider
+ let response = contract_caller_instance
+ .methods()
+ .increment_from_contract(lib_contract_id, 42)
+ .with_contracts(&[&lib_contract_instance])
+ .call()
+ .await?;
+
+ assert_eq!(43, response.value);
+
+ let response = contract_caller_instance2
+ .methods()
+ .increment_from_contract(lib_contract_id, 42)
+ .with_contracts(&[&lib_contract_instance])
+ .call()
+ .await?;
+
+ assert_eq!(43, response.value);
+
+In this example, three contracts are deployed on the same provider using the wallet
generated by the Wallets
command. The second and third macros use the same contract but have different IDs because of the deployment with salt. Both of them can call the first contract by using their 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_program_test!(
+ Abigen(Contract(
+ name = "TestContract",
+ project = "e2e/sway/contracts/contract_test"
+ )),
+ Deploy(
+ name = "contract_instance",
+ contract = "TestContract",
+ wallet = "wallet",
+ random_salt = false,
+ ),
+ );
+
+
+ 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));
+
+
+ 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 std::str::FromStr;
+
+ use fuels::types::Address;
+
+ // 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)?;
+ assert_eq!([0u8; 32], *address);
+
+
+ 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 std::str::FromStr;
+
+ use fuels::types::AssetId;
+
+ // 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)?;
+ assert_eq!([0u8; 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::types::{bech32::Bech32Address, 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)?;
+ 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
fromAddress
orBech32ContractId
fromContractId
theHRP
(Human-Readable Part) is set to "fuel" per default.
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 b256 = Bits256([1; 32]);
+
+ let call_handler = contract_instance.methods().b256_as_input(b256);
+
+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]);
+
+
+ Bytes
In Fuel, a type called Bytes
represents a collection of tightly-packed bytes. The Rust SDK represents Bytes
as Bytes(Vec<u8>)
. Here's an example of using Bytes
in a contract call:
let bytes = Bytes(vec![40, 41, 42]);
+
+ contract_methods.accept_bytes(bytes).call().await?;
+
+If you have a hexadecimal value as a string and wish to convert it to Bytes
, you may do so with from_hex_str
:
let hex_str = "0101010101010101010101010101010101010101010101010101010101010101";
+
+ let bytes = Bytes::from_hex_str(hex_str)?;
+
+ assert_eq!(bytes.0, vec![1u8; 32]);
+
+ // With the `0x0` prefix
+ let hex_str = "0x0101010101010101010101010101010101010101010101010101010101010101";
+
+ let bytes = Bytes::from_hex_str(hex_str)?;
+
+ assert_eq!(bytes.0, vec![1u8; 32]);
+
+
+ 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 std::str::FromStr;
+
+ use fuels::types::Bytes32;
+
+ // 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)?;
+ assert_eq!([0u8; 32], *b256);
+
+Bytes32
also implements the fmt
module'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!("{b256:#x}");
+
+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 toBytes32
; also used to represent hashes, and it holds a 256-bit value. In Rust, through the SDK, this is represented asBits256(value)
wherevalue
is a[u8; 32]
. If your contract method takes ab256
as input, all you need to do is pass aBits256([u8; 32])
when calling it from the SDK.
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 std::str::FromStr;
+
+ use fuels::types::ContractId;
+
+ // 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)?;
+ assert_eq!([0u8; 32], *contract_id);
+
+
+ Below you can find examples for common type conversions:
+Bytes32
Address
ContractId
Identity
AssetId
Bech32
str
Bits256
Bytes
B512
EvmAddress
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::types::{AssetId, ContractId};
+
+ let contract_id = ContractId::new([1u8; 32]);
+
+ let asset_id: AssetId = AssetId::new(*contract_id);
+
+ assert_eq!([1u8; 32], *asset_id);
+
+Bytes32
Convert a [u8; 32]
array to Bytes32
:
let my_slice = [1u8; 32];
+ let b256 = Bytes32::new(my_slice);
+
+Convert a hex string to Bytes32
:
let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
+ let b256 = Bytes32::from_str(hex_str)?;
+
+Address
Convert a [u8; 32]
array to an Address
:
let my_slice = [1u8; 32];
+ let address = Address::new(my_slice);
+
+Convert a Bech32
address to an Address
:
let _plain_address: Address = bech32_address.into();
+
+Convert a wallet to an Address
:
let wallet_unlocked = WalletUnlocked::new_random(None);
+ let address: Address = wallet_unlocked.address().into();
+
+Convert a hex string to an Address
:
let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
+ let address = Address::from_str(hex_str)?;
+
+ContractId
Convert a [u8; 32]
array to ContractId
:
let my_slice = [1u8; 32];
+ let contract_id = ContractId::new(my_slice);
+
+Convert a hex string to a ContractId
:
let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
+ let contract_id = ContractId::from_str(hex_str)?;
+
+Convert a contract instance to a ContractId
:
let contract_id: ContractId = contract_instance.id().into();
+
+Identity
Convert an Address
to an Identity
:
let _identity_from_address = Identity::Address(address);
+
+Convert a ContractId
to an Identity
:
let _identity_from_contract_id = Identity::ContractId(contract_id);
+
+AssetId
Convert a [u8; 32]
array to an AssetId
:
let my_slice = [1u8; 32];
+ let asset_id = AssetId::new(my_slice);
+
+Convert a hex string to an AssetId
:
let hex_str = "0x0000000000000000000000000000000000000000000000000000000000000000";
+ let asset_id = AssetId::from_str(hex_str)?;
+
+Bech32
Convert a [u8; 32]
array to a Bech32
address:
let hrp = "fuel";
+ let my_slice = [1u8; 32];
+ let _bech32_address = Bech32Address::new(hrp, my_slice);
+
+Convert Bytes32
to a Bech32
address:
let my_hash = Bytes32::new([1u8; 32]);
+ let _bech32_address = Bech32Address::new(hrp, my_hash);
+
+Convert a string to a Bech32
address:
let address = "fuel1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsx2mt2";
+ let bech32_address = Bech32Address::from_str(address)?;
+
+Convert an Address
to a Bech32
address:
let plain_address = Address::new([0u8; 32]);
+ let bech32_address = Bech32Address::from(plain_address);
+
+str
Convert a ContractId
to a str
:
let _str_from_contract_id: &str = contract_id.to_string().as_str();
+
+Convert an Address
to a str
:
let _str_from_address: &str = address.to_string().as_str();
+
+Convert an AssetId
to a str
:
let _str_from_asset_id: &str = asset_id.to_string().as_str();
+
+Convert Bytes32
to a str
:
let _str_from_bytes32: &str = b256.to_string().as_str();
+
+Bits256
Convert a hex string to Bits256
:
let hex_str = "0x0101010101010101010101010101010101010101010101010101010101010101";
+
+ let bits256 = Bits256::from_hex_str(hex_str)?;
+
+Convert a ContractId
to Bits256
:
let _contract_id_to_bits_256 = Bits256(contract_id.into());
+
+Convert an Address
to Bits256
:
let bits_256 = Bits256(address.into());
+
+Convert an AssetId
to Bits256
:
let _asset_id_to_bits_256 = Bits256(asset_id.into());
+
+Bytes
Convert a string to Bytes
:
let hex_str = "0x0101010101010101010101010101010101010101010101010101010101010101";
+
+ let bytes = Bytes::from_hex_str(hex_str)?;
+
+B512
Convert two hex strings to B512
:
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
Convert a Bits256
address to an EvmAddress
:
let _evm_address = EvmAddress::from(bits_256);
+
+
+ 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!(Contract(name="MyContract",
+ abi="e2e/sway/types/contracts/complex_types_contract/out/release/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.
+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);
+
+Sway supports unused generic type parameters when declaring structs/enums:
+struct SomeStruct<T, K> {
+ field: u64
+}
+
+enum SomeEnum<T, K> {
+ One: u64
+}
+
+
+If you tried the same in Rust you'd get complaints that T
and K
must be used or removed. When generating Rust bindings for such types we make use of the PhantomData
type. The generated bindings for the above example would look something like this:
struct SomeStruct<T, K> {
+ pub field: u64,
+ pub _unused_generic_0: PhantomData<T>
+ pub _unused_generic_1: PhantomData<K>
+}
+
+enum SomeEnum<T, K> {
+ One(u64),
+ IgnoreMe(PhantomData<T>, PhantomData<K>)
+}
+
+To lessen the impact to developer experience you may use the new
method to initialize a structure without bothering with the PhantomData
s.:
assert_eq!(
+ <StructUnusedGeneric<u16, u32>>::new(15),
+ StructUnusedGeneric {
+ field: 15,
+ _unused_generic_0: std::marker::PhantomData,
+ _unused_generic_1: std::marker::PhantomData
+ }
+ );
+
+If your struct doesn't have any fields we'll also derive Default
. As for enums all PhantomData
s are placed inside a new variant called IgnoreMe
which you'll need to ignore in your matches:
match my_enum {
+ EnumUnusedGeneric::One(_value) => {}
+ EnumUnusedGeneric::IgnoreMe(..) => panic!("Will never receive this variant"),
+ }
+
+
+ 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
+ value: Bits256,
+}
+
+Here's an example:
+ let b256 = Bits256::from_hex_str(
+ "0x1616060606060606060606060606060606060606060606060606060606060606",
+ )?;
+ let evm_address = EvmAddress::from(b256);
+
+ let call_handler = contract_instance
+ .methods()
+ .evm_address_as_input(evm_address);
+
+++ +Note: when creating an
+EvmAddress
fromBits256
, the first 12 bytes will be cleared because an EVM address is only 20 bytes long.
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.
+ +String
The Rust SDK represents Fuel's String
s 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 SizedAsciiString
s 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>
.
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 from contract methods is supported transparently, with the caveat that you cannot have them nested inside another type. This limitation is temporary.
+ let response = contract_methods.u8_in_vec(10).call().await?;
+ assert_eq!(response.value, (0..10).collect::<Vec<_>>());
+
+++ +Note: you can still interact with contracts containing methods that return vectors nested inside another type, just not interact with the methods themselves
+
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.
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.
You can learn more about signing here.
+ + +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.
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 with the lock
method 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.
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.
In the Fuel network, 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::zeroed();
+ 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), you can use the get_balances
method:
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_balance = balances.get(&asset_id.to_string()).unwrap();
+
+
+ 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 = 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))?;
+
+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 = 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)?;
+
+
+ You can use wallets for many important things, for instance:
+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.
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 = 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);
+
+
+ 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 = 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 std::str::FromStr;
+
+ use fuels::{crypto::SecretKey, prelude::*};
+
+ // Use the test helper to setup a test provider.
+ let provider = 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 withset_provider()
. The optional parameter +enables defining owners (wallet addresses) of genesis coins before a provider is launched.
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
. 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 = secret_seed.as_slice().try_into()?;
+
+ // Create a wallet using the private key created above.
+ let wallet = WalletUnlocked::new_from_private_key(secret, None);
+
+ let message = Message::new("my message".as_bytes());
+ let signature = wallet.sign(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 recovered_address = signature.recover(&message)?;
+
+ assert_eq!(wallet.address().hash(), recovered_address.hash());
+
+ // Verify signature
+ signature.verify(&recovered_address, &message)?;
+
+Signers
to a transaction builderEvery signed resource in the inputs needs to have a witness index that points to a valid witness. Changing the witness index inside an input will change the transaction ID. This means that we need to set all witness indexes before finally signing the transaction. Previously, the user had to make sure that the witness indexes and the order of the witnesses are correct. To automate this process, the SDK will keep track of the signers in the transaction builder and resolve the final transaction automatically. This is done by storing signers until the final transaction is built.
+Below is a full example of how to create a transaction builder and add signers to it.
+++Note: When you add a
+Signer
to a transaction builder, the signer is stored inside it and the transaction will not be resolved until you callbuild()
!
let secret = SecretKey::from_str(
+ "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
+ )?;
+ let wallet = WalletUnlocked::new_from_private_key(secret, None);
+
+ // Set up a transaction
+ let mut tb = {
+ let input_coin = Input::ResourceSigned {
+ resource: CoinType::Coin(Coin {
+ amount: 10000000,
+ owner: wallet.address().clone(),
+ ..Default::default()
+ }),
+ };
+
+ let output_coin = Output::coin(
+ Address::from_str(
+ "0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077",
+ )?,
+ 1,
+ Default::default(),
+ );
+
+ ScriptTransactionBuilder::prepare_transfer(
+ vec![input_coin],
+ vec![output_coin],
+ Default::default(),
+ )
+ };
+
+ // Add `Signer` to the transaction builder
+ tb.add_signer(wallet.clone())?;
+
+If you have a built transaction and want to add a signature, you can use the sign_with
method.
tx.sign_with(&wallet, provider.chain_id()).await?;
+
+
+ You'll often want to create one or more test wallets when testing your contracts. Here's how to do it.
+If you need multiple test wallets, they can be set up using the launch_custom_provider_and_get_wallets
method.
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
orlaunch_custom_provider_and_get_wallets
+will have deterministic addresses.
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 = setup_test_provider(coins.clone(), vec![], None, None).await?;
+ wallet.set_provider(provider);
+
+Vec<(UtxoId, Coin)>
has num_assets
* coins_per_assets
coins (UTXOs)Vec<AssetId>
contains the num_assets
randomly generated AssetId
s (always includes the base asset)You can also create assets with specific AssetId
s, 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: AssetId::zeroed(),
+ 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 = 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
+
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(),
+ AssetId::zeroed(),
+ number_of_coins,
+ amount_per_coin,
+ );
+
+++ +Note Choosing a large number of coins and assets for
+setup_multiple_assets_coins
orsetup_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.