The Sway Programming Language
Sway is a domain-specific language (DSL) for the Fuel Virtual Machine (FuelVM), a blockchain-optimized VM designed for the Fuel blockchain. Sway is based on Rust, and includes syntax to leverage a blockchain VM without needlessly verbose boilerplate.
This book documents how to write smart contracts in Sway, along with how to install and use the Sway toolchain.
Before starting developing smart contracts in Sway, please keep in mind the known issues and workarounds of the language and toolchain.
Introduction
To get started with Forc and Sway smart contract development, install the Fuel toolchain and Fuel full node and set up your first project.
Installation
The Sway toolchain is sufficient to compile Sway smart contracts. Otherwise, note that if you want to run Sway smart contracts (e.g. for testing), a Fuel Core full node is required, which is packaged together with the Sway toolchain together as the Fuel toolchain.
Installing from Pre-compiled Binaries
Pre-compiled release binaries for Linux and macOS are available for the Sway toolchain. Native Windows is currently unsupported (tracking issue for Windows support). Windows Subsystem for Linux should work but is not officially supported.
fuelup
is the equivalent of Rust's rustup
for the Fuel toolchain. It enables easily downloading binary releases of the Fuel toolchain.
Start by installing fuelup
with:
curl --proto '=https' --tlsv1.2 -sSf \
https://fuellabs.github.io/fuelup/fuelup-init.sh | sh
fuelup-init
will ask for permission to add ~/.fuelup/bin
to your PATH. Otherwise, you can also pass --no-modify-path
so that fuelup-init
does not modify your PATH:
curl --proto '=https' --tlsv1.2 -sSf \
https://fuellabs.github.io/fuelup/fuelup-init.sh | sh -s -- --no-modify-path
Once fuelup
is installed, fuelup-init
automatically runs the command below
fuelup toolchain install latest
to install the latest Fuel toolchain.
You can run the same command at a later time to update the toolchain.
Installing from Source
Dependencies
A prerequisite for installing and using Sway is the Rust toolchain. Platform-specific instructions for installing rustup
can be found here. Then, install the Rust toolchain with:
# Install the latest stable Rust toolchain.
rustup install stable
Installing fuel-core
may require installing additional system dependencies. See here for instructions.
The Sway toolchain is built and tested against the stable
Rust toolchain version (https://github.com/rust-lang/rust/releases/latest). There is no guarantee it will work with the nightly
Rust toolchain, or with earlier stable
versions, so ensure you are using stable
with:
# Update installed Rust toolchain; can be used independently.
rustup update
# Set the stable Rust toolchain as default; can be used independently.
rustup default stable
Installing from Cargo
The Sway toolchain and Fuel Core full node can be installed from source with Cargo with:
cargo install forc fuel-core
Updating forc
from Cargo
You can update the toolchain from source with Cargo with:
cargo install forc fuel-core
Installing forc
Plugins from Cargo
The Fuel ecosystem has a few plugins which can be easily installed via Cargo.
Note:
forc
detects anything in your$PATH
prefixed withforc-
as a plugin. Useforc plugins
to see what you currently have installed.
# Sway Formatter
cargo install forc-fmt
# Block Explorer
cargo install forc-explore
# Sway Language Server
cargo install forc-lsp
Building from Source
Rather than installing from cargo
, the Sway toolchain can be built from a local source checkout by following instructions at https://github.com/FuelLabs/sway. The Fuel Core full node implementation can be built from source by following instructions at https://github.com/FuelLabs/fuel-core.
Enable tab completion for Bash, Fish, Zsh, or PowerShell
forc
supports generating completion scripts for Bash, Fish, Zsh, and PowerShell. See forc completions --help
for full details, but the gist is as simple as using one of the following:
# Bash
forc completions --shell=bash > ~/.local/share/bash-completion/completions/forc
# Bash (macOS/Homebrew)
forc completions --shell=bash > $(brew --prefix)/etc/bash_completion.d/forc.bash-completion
# Fish
mkdir -p ~/.config/fish/completions
forc completions --shell=fish > ~/.config/fish/completions/forc.fish
# Zsh
forc completions --shell=zsh > ~/.zfunc/_forc
# PowerShell v5.0+
forc completions --shell=powershell >> $PROFILE.CurrentUserCurrentHost
# or
forc completions --shell=powershell | Out-String | Invoke-Expression
Once the completions have been generated and properly installed, close and reopen your terminal for the new completions to take effect.
Sway Quickstart
Follow this guide to write, test, and deploy a simple smart contract in Sway.
Check out the Developer Quickstart Guide for a step-by-step guide on building a fullstack dapp on Fuel. The guide will walk you through writing a smart contract, setting up a wallet, and building a frontend to interact with your contract.
Glossary
Before we begin, it may be helpful to understand terminology that will used throughout the docs and how they relate to each other:
- Fuel: the Fuel blockchain.
- FuelVM: the virtual machine powering Fuel.
- Sway: the domain-specific language crafted for the FuelVM; it is inspired by Rust.
- Forc: the build system and package manager for Sway, similar to Cargo for Rust.
Understand Sway Program Types
There are four types of Sway programs:
contract
predicate
script
library
Contracts, predicates, and scripts can produce artifacts usable on the blockchain, while a library is simply a project designed for code reuse and is not directly deployable.
See the chapter on program types for more information.
Your First Sway Project
We'll build a simple counter contract with two functions: one to increment the counter, and one to return the value of the counter.
A few pieces of info that will be helpful before moving on:
- The main features of a smart contract that differentiate it from scripts or predicates are that it is callable and stateful.
- A script is runnable bytecode on the chain which can call contracts to perform some task. It does not represent ownership of any resources and it cannot be called by a contract.
Writing the Contract
First, let's install the Sway toolchain. Then with forc
installed, create a contract project:
forc new counter_contract
Here is the project that Forc has initialized:
$ cd counter_contract
$ tree .
├── Cargo.toml
├── Forc.toml
├── src
│ └── main.sw
└── tests
└── harness.rs
Forc.toml
is the manifest file (similar to Cargo.toml
for Cargo or package.json
for Node), and defines project metadata such as the project name and dependencies.
We'll be writing our code in the src/main.sw
.
cd
(change directories) into your contract project and delete the boilerplate code in src/main.sw
. Every Sway file must start with a declaration of what type of program the file contains; here, we've declared that this file is a contract.
contract;
Next, we'll define a storage value. In our case, we have a single counter that we'll call counter
of type 64-bit unsigned integer and initialize it to 0.
storage {
counter: u64 = 0,
}
ABI
An ABI defines an interface, and there is no function body in the ABI. A contract must either define or import an ABI declaration and implement it. It is considered best practice to define your ABI in a separate library and import it into your contract because this allows callers of the contract to import and use the ABI in scripts to call your contract.
For simplicity, we will define the ABI natively in the contract.
abi Counter {
#[storage(read, write)]
fn increment();
#[storage(read)]
fn counter() -> u64;
}
Going line by line
#[storage(read, write)]
is an annotation which denotes that this function has permission to read and write a value in storage.
fn increment()
- We're introducing the functionality to increment and denoting it shouldn't return any value.
#[storage(read)]
is an annotation which denotes that this function has permission to read values in storage.
fn counter() -> u64;
- We're introducing the functionality to increment the counter and denoting the function's return value.
Implement ABI
Below your ABI definition, you will write the implementation of the functions defined in your ABI.
impl Counter for Contract {
#[storage(read)]
fn counter() -> u64 {
return storage.counter;
}
#[storage(read, write)]
fn increment() {
storage.counter = storage.counter + 1;
}
}
Note
return storage.counter;
is equivalent tostorage.counter
.
What we just did
Read and return the counter property value from the contract storage.
fn counter() -> u64 {
return storage.counter;
}
The function body accesses the value counter in storage, and increments the value by one. Then, we return the newly updated value of counter.
fn increment() {
storage.counter = storage.counter + 1;
}
Build the Contract
Build counter_contract
by running the following command in your terminal from inside the counter_contract
directory:
forc build
You should see something like this output:
Compiled library "core".
Compiled library "std".
Compiled contract "counter_contract".
Bytecode size is 224 bytes.
Deploy the Contract
It's now time to deploy the contract and call it on a Fuel node. We will show how to do this using forc
from the command line, but you can also do it using the Rust SDK or the TypeScript SDK
Spin Up a Fuel node
In a separate tab in your terminal, spin up a local Fuel node:
fuel-core run --db-type in-memory
This starts a Fuel node with a volatile database that will be cleared when shut down (good for testing purposes).
Deploy counter_contract
To Your Local Fuel Node
Note If you want to deploy your contract to the testnet instead of to a local Fuel node, check out the Developer Quickstart Guide.
To deploy counter_contract
on your local Fuel node, open a new terminal tab and run the following command from the root of the wallet_contract
directory:
forc deploy --unsigned
Note You can't use the same terminal session that is running fuel-core to run any other commands as this will end your fuel-core process.
This should produce some output in stdout
that looks like this:
$ forc deploy --unsigned
Compiled library "core".
Compiled library "std".
Compiled contract "counter_contract".
Bytecode size is 224 bytes.
Contract id: 0xaf94c0a707756caae667ee43ca18bace441b25998c668010192444a19674dc4f
Logs:
TransactionId(HexFormatted(7cef24ea33513733ab78c5daa5328d622d4b38187d0f0d1857b272090d99f96a))
Note the contract ID—you will need it if you want to build out a frontend to interact with this contract.
Testing your Contract
In the directory tests
, navigate to harness.rs.
Here you'll see there is some boilerplate code to help you start interacting with and testing your contract.
At the bottom of the file, define the body of can_get_contract_instance
. Here is what your code should look like to verify that the value of the counter did get incremented:
#[tokio::test]
async fn can_get_contract_instance() {
// Increment the counter
let _result = instance.increment().call().await.unwrap();
// Get the current value of the counter
let result = instance.counter().call().await.unwrap();
assert!(result.value > 0);
}
Run the following command in the terminal: forc test
.
You'll see something like this as your output:
Compiled library "core".
Compiled library "std".
Compiled contract "counter_contract".
Bytecode size is 224 bytes.
Compiling counter_contract v0.1.0 (<path/to/counter_contract>)
Finished test [unoptimized + debuginfo] target(s) in 4.55s
Running tests/harness.rs (target/debug/deps/integration_tests-7a2922c770587b45)
running 1 test
test can_get_contract_id ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s
Congratulations, you've just created and tested your first Sway smart contract 🎉.
Next Steps
Now that you've written a smart contract with Sway and deployed it to a local Fuel node, try out building a fullstack dapp deployed to the testnet. A step-by-step guide to write your smart contract, deploy to testnet, set up a wallet, and build a frontend can be found in the Developer Quickstart Guide.
The Fuel Toolchain
The Fuel toolchain consists of several components.
Forc (forc
)
The "Fuel Orchestrator" Forc is our equivalent of Rust's Cargo. It is the primary entry point for creating, building, testing, and deploying Sway projects.
Sway Language Server (forc-lsp
)
The Sway Language Server forc-lsp
is provided to expose features to IDEs. Installation instructions.
Currently, only Visual Studio Code is supported through a plugin. Vim support is forthcoming, though syntax highlighting is provided.
Note: There is no need to manually run
forc-lsp
(the plugin will automatically start it), however bothforc
andforc-lsp
must be in your$PATH
. To check ifforc
is in your$PATH
, typeforc --help
in your terminal.
Sway Formatter (forc-fmt
)
A canonical formatter is provided with forc-fmt
. Installation instructions. It can be run manually with
forc fmt
The Visual Studio Code plugin will automatically format Sway files with forc-fmt
on save.
Fuel Core (fuel-core
)
An implementation of the Fuel protocol, Fuel Core, is provided together with the Sway toolchain to form the Fuel toolchain. The Rust SDK will automatically start and stop an instance of the node during tests, so there is no need to manually run a node unless using Forc directly without the SDK.
A Forc Project
To initialize a new project with Forc, use forc new
:
forc new my-fuel-project
Here is the project that Forc has initialized:
$ cd my-fuel-project
$ tree .
├── Forc.toml
└── src
└── main.sw
Forc.toml
is the manifest file (similar to Cargo.toml
for Cargo or package.json
for Node), and defines project metadata such as the project name and dependencies.
For additional information on dependency management, see: here.
[project]
authors = ["User"]
entry = "main.sw"
license = "Apache-2.0"
name = "my-fuel-project"
[dependencies]
Here are the contents of the only Sway file in the project, and the main entry point, src/main.sw
:
contract;
abi MyContract {
fn test_function() -> bool;
}
impl MyContract for Contract {
fn test_function() -> bool {
true
}
}
The project is a contract, one of four different project types. For additional information on different project types, see here.
We now compile our project with forc build
, passing the flag --print-finalized-asm
to view the generated assembly:
$ forc build --print-finalized-asm
...
.program:
ji i4
noop
DATA_SECTION_OFFSET[0..32]
DATA_SECTION_OFFSET[32..64]
lw $ds $is 1
add $$ds $$ds $is
lw $r0 $fp i73 ; load input function selector
lw $r1 data_0 ; load fn selector for comparison
eq $r2 $r0 $r1 ; function selector comparison
jnzi $r2 i12 ; jump to selected function
movi $$tmp i123 ; special code for mismatched selector
rvrt $$tmp ; revert if no selectors matched
ret $one
.data:
data_0 .word 559005003
Compiled contract "my-fuel-project".
Bytecode size is 60 bytes.
Standard Library
Similar to Rust, Sway comes with its own standard library.
The Sway Standard Library is the foundation of portable Sway software, a set of minimal shared abstractions for the broader Sway ecosystem. It offers core types, like Result<T, E>
and Option<T>
, library-defined operations on language primitives, native asset management, blockchain contextual operations, access control, storage management, and support for types from other VMs, among many other things.
The entire Sway standard library is a Forc project called std
, and is available directly here: https://github.com/FuelLabs/sway/tree/master/sway-lib-std (navigate to the appropriate tagged release if the latest master
is not compatible).
Using the Standard Library
The standard library is made implicitly available to all Forc projects created using forc new
. In other words, it is not required to manually specify std
as an explicit dependency. Forc will automatically use the version of std
that matches its version.
Importing items from the standard library can be done using the use
keyword, just as importing items from any Sway project. For example:
use std::storage::StorageVec;
This imports the StorageVec
type into the current namespace.
Standard Library Prelude
Sway comes with a variety of things in its standard library. However, if you had to manually import every single thing that you used, it would be very verbose. But importing a lot of things that a program never uses isn't good either. A balance needs to be struck.
The prelude is the list of things that Sway automatically imports into every Sway program. It's kept as small as possible, and is focused on things which are used in almost every single Sway program.
The current version of the prelude lives in std::prelude
, and re-exports the following:
std::address::Address
, a wrapper around theb256
type representing a wallet address.std::contract_id::ContractId
, a wrapper around theb256
type representing the ID of a contract.std::identity::Identity
, an enum with two possible variants:Address: Address
andContractId: ContractId
.std::vec::Vec
, a growable, heap-allocated vector.std::storage::StorageMap
, a key-value mapping in contract storage.std::option::Option
, an enum which expresses the presence or absence of a value.std::result::Result
, an enum for functions that may succeed or fail.std::assert::assert
, a function that reverts the VM if the condition provided to it isfalse
.std::revert::require
, a function that reverts the VM and logs a given value if the condition provided to it isfalse
.std::revert::revert
, a function that reverts the VM.
Example
Some basic example contracts to see how Sway and Forc work.
Counter
The following is a simple example of a contract which implements a counter. Both the initialize_counter()
and increment_counter()
ABI methods return the currently set value.
forc template --template-name counter my_counter_project
contract;
abi TestContract {
#[storage(write)]
fn initialize_counter(value: u64) -> u64;
#[storage(read, write)]
fn increment_counter(amount: u64) -> u64;
}
storage {
counter: u64 = 0,
}
impl TestContract for Contract {
#[storage(write)]
fn initialize_counter(value: u64) -> u64 {
storage.counter = value;
value
}
#[storage(read, write)]
fn increment_counter(amount: u64) -> u64 {
let incremented = storage.counter + amount;
storage.counter = incremented;
incremented
}
}
Subcurrency
The following is a simple example of a subcurrency which implements functionality to mint and send a token. It is a ledger-based token, i.e. the contract maintains a ledger of user account balances.
Being a ledger-based token, this example does not use Fuel's native asset system. It is not recommended to actually use ledger-based tokens in production; this example is here purely for illustrative purposes.
contract;
use std::{auth::{AuthError, msg_sender}, hash::sha256, logging::log};
////////////////////////////////////////
// Event declarations
////////////////////////////////////////
//
// Events allow clients to react to changes in the contract.
// Unlike Solidity, events are simply structs.
//
/// Emitted when a token is sent.
struct Sent {
from: Address,
to: Address,
amount: u64,
}
////////////////////////////////////////
// ABI method declarations
////////////////////////////////////////
/// ABI for a subcurrency.
abi Token {
// Mint new tokens and send to an address.
// Can only be called by the contract creator.
#[storage(read, write)]
fn mint(receiver: Address, amount: u64);
// Sends an amount of an existing token.
// Can be called from any address.
#[storage(read, write)]
fn send(receiver: Address, amount: u64);
}
////////////////////////////////////////
// Constants
////////////////////////////////////////
/// Address of contract creator.
const MINTER = Address::from(0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b);
////////////////////////////////////////
// Contract storage
////////////////////////////////////////
// Contract storage persists across transactions.
storage {
balances: StorageMap<Address, u64> = StorageMap {},
}
////////////////////////////////////////
// ABI definitions
////////////////////////////////////////
/// Contract implements the `Token` ABI.
impl Token for Contract {
#[storage(read, write)]
fn mint(receiver: Address, amount: u64) {
// Note: The return type of `msg_sender()` can be inferred by the
// compiler. It is shown here for explicitness.
let sender: Result<Identity, AuthError> = msg_sender();
let sender: Address = match sender.unwrap() {
Identity::Address(addr) => {
assert(addr == MINTER);
addr
},
_ => revert(0),
};
// Increase the balance of receiver
storage.balances.insert(receiver, storage.balances.get(receiver) + amount)
}
#[storage(read, write)]
fn send(receiver: Address, amount: u64) {
// Note: The return type of `msg_sender()` can be inferred by the
// compiler. It is shown here for explicitness.
let sender: Result<Identity, AuthError> = msg_sender();
let sender = match sender.unwrap() {
Identity::Address(addr) => addr,
_ => revert(0),
};
// Reduce the balance of sender
let sender_amount = storage.balances.get(sender);
assert(sender_amount > amount);
storage.balances.insert(sender, sender_amount - amount);
// Increase the balance of receiver
storage.balances.insert(receiver, storage.balances.get(receiver) + amount);
log(Sent {
from: sender,
to: receiver,
amount: amount,
});
}
}
FizzBuzz
This example is not the traditional FizzBuzz; instead it is the smart contract version! A script can call the fizzbuzz
ABI method of this contract with some u64
value and receive back its fizzbuzzability as an enum
.
The format for custom structs and enums such as FizzBuzzResult
will be automatically included in the ABI JSON so that off-chain code can handle the encoded form of the returned data.
contract;
enum FizzBuzzResult {
Fizz: (),
Buzz: (),
FizzBuzz: (),
Other: u64,
}
abi FizzBuzz {
fn fizzbuzz(input: u64) -> FizzBuzzResult;
}
impl FizzBuzz for Contract {
fn fizzbuzz(input: u64) -> FizzBuzzResult {
if input % 15 == 0 {
FizzBuzzResult::FizzBuzz
} else if input % 3 == 0 {
FizzBuzzResult::Fizz
} else if input % 5 == 0 {
FizzBuzzResult::Buzz
} else {
FizzBuzzResult::Other(input)
}
}
}
Wallet Smart Contract
ABI Declaration
library wallet_abi;
abi Wallet {
#[storage(read, write)]
fn receive_funds();
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address);
}
ABI Implementation
contract;
use std::{
auth::{
AuthError,
msg_sender,
},
call_frames::msg_asset_id,
constants::BASE_ASSET_ID,
context::msg_amount,
token::transfer_to_address,
};
use wallet_abi::Wallet;
const OWNER_ADDRESS = Address::from(0x8900c5bec4ca97d4febf9ceb4754a60d782abbf3cd815836c1872116f203f861);
storage {
balance: u64 = 0,
}
impl Wallet for Contract {
#[storage(read, write)]
fn receive_funds() {
if msg_asset_id() == BASE_ASSET_ID {
// If we received `BASE_ASSET_ID` then keep track of the balance.
// Otherwise, we're receiving other native assets and don't care
// about our balance of tokens.
storage.balance += msg_amount();
}
}
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address) {
// Note: The return type of `msg_sender()` can be inferred by the
// compiler. It is shown here for explicitness.
let sender: Result<Identity, AuthError> = msg_sender();
match sender.unwrap() {
Identity::Address(addr) => assert(addr == OWNER_ADDRESS),
_ => revert(0),
};
let current_balance = storage.balance;
assert(current_balance >= amount_to_send);
storage.balance = current_balance - amount_to_send;
// Note: `transfer_to_address()` is not a call and thus not an
// interaction. Regardless, this code conforms to
// checks-effects-interactions to avoid re-entrancy.
transfer_to_address(amount_to_send, BASE_ASSET_ID, recipient_address);
}
}
Sway Program Types
A Sway program itself has a type: it is either a contract, a predicate, a script, or a library. The first three of these things are all deployable to the blockchain. A library is simply a project designed for code reuse and is never directly deployed to the chain.
Every Sway file must begin with a declaration of what type of program it is. A project can have many libraries within it, but only one contract, script, or predicate. Scripts and predicates require main
functions to serve as entry points, while contracts instead publish an ABI. This chapter will go into detail about all of these various types of programs and what purposes they serve.
Contracts are used primarily for protocols or systems that operate within a fixed set of rules. A good example would be a staking contract or a decentralized exchange.
Scripts are used for complex on-chain interactions that won't persist. An example of this may be using a DEX and Lender to create a leveraged position (borrow, swap, re-collateralize, borrow) which is a complex transaction that would usually take multiple steps.
Libraries are for code that is reusable and useful for handling common situations. A good example of this would be a library to handle fixed-point math or big number math.
What is a Smart Contract?
A smart contract is no different than a script or predicate in that it is a piece of bytecode that is deployed to the blockchain via a transaction. The main features of a smart contract that differentiate it from scripts or predicates are that it is callable and stateful. Put another way, a smart contract is analogous to a deployed API with some database state. The interface of a smart contract, also just called a contract, must be defined strictly with an ABI declaration. See this contract for an example.
Syntax of a Smart Contract
As with any Sway program, the program starts with a declaration of what program type it is. A contract must also either define or import an ABI declaration and implement it. It is considered good practice to define your ABI in a separate library and import it into your contract. This allows callers of your contract to simply import the ABI directly and use it in their scripts to call your contract. Let's take a look at an ABI declaration in a library:
library wallet_abi;
abi Wallet {
#[storage(read, write)]
fn receive_funds();
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address);
}
Let's focus on the ABI declaration and inspect it line-by-line.
The ABI Declaration
abi Wallet {
#[storage(read, write)]
fn receive_funds();
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address);
}
In the first line, abi Wallet {
, we declare the name of this Application Binary Interface, or ABI. We are naming this ABI Wallet
. To import this ABI into either a script for calling or a contract for implementing, you would use
use wallet_abi::Wallet;
In the second line,
#[storage(read, write)]
fn receive_funds();
we are declaring an ABI method called receive_funds
which, when called, should receive funds into this wallet. Note that we are simply defining an interface here, so there is no function body or implementation of the function. We only need to define the interface itself. In this way, ABI declarations are similar to trait declarations. This particular ABI method does not take any parameters.
In the third line,
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address);
we are declaring another ABI method, this time called send_funds
. It takes two parameters: the amount to send, and the address to send the funds to.
Note: The ABI methods
receive_funds
andsend_funds
also require the annotation#[storage(read, write)]
because their implementations require reading and writing a storage variable that keeps track of the wallet balance, as we will see shortly. Refer to Purity for more information on storage annotations.
Implementing an ABI for a Smart Contract
Now that we've discussed how to define the interface, let's discuss how to use it. We will start by implementing the above ABI for a specific contract.
Implementing an ABI for a contract is accomplished with impl <ABI name> for Contract
syntax. The for Contract
syntax can only be used to implement an ABI for a contract; implementing methods for a struct should use impl Foo
syntax.
impl Wallet for Contract {
#[storage(read, write)]
fn receive_funds() {
if msg_asset_id() == BASE_ASSET_ID {
// If we received `BASE_ASSET_ID` then keep track of the balance.
// Otherwise, we're receiving other native assets and don't care
// about our balance of tokens.
storage.balance += msg_amount();
}
}
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address) {
// Note: The return type of `msg_sender()` can be inferred by the
// compiler. It is shown here for explicitness.
let sender: Result<Identity, AuthError> = msg_sender();
match sender.unwrap() {
Identity::Address(addr) => assert(addr == OWNER_ADDRESS),
_ => revert(0),
};
let current_balance = storage.balance;
assert(current_balance >= amount_to_send);
storage.balance = current_balance - amount_to_send;
// Note: `transfer_to_address()` is not a call and thus not an
// interaction. Regardless, this code conforms to
// checks-effects-interactions to avoid re-entrancy.
transfer_to_address(amount_to_send, BASE_ASSET_ID, recipient_address);
}
}
You may notice once again the similarities between traits and ABIs. And, indeed, as a bonus, you can specify methods in addition to the interface surface of an ABI, just like a trait. By implementing the methods in the interface surface, you get the extra method implementations For Free™.
Note that the above implementation of the ABI follows the Checks, Effects, Interactions pattern.
Calling a Smart Contract from a Script
Note: In most cases, calling a contract should be done from the Rust SDK or the TypeScript SDK which provide a more ergonomic UI for interacting with a contract. However, there are situations where manually writing a script to call a contract is required.
Now that we have defined our interface and implemented it for our contract, we need to know how to actually call our contract. Let's take a look at a contract call:
script;
use std::constants::ZERO_B256;
use wallet_abi::Wallet;
fn main() {
let contract_address = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b;
let caller = abi(Wallet, contract_address);
let amount_to_send = 200;
let recipient_address = Address::from(0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b);
caller.send_funds {
gas: 10000,
coins: 0,
asset_id: ZERO_B256,
}(amount_to_send, recipient_address);
}
The main new concept is the abi cast: abi(AbiName, contract_address)
. This returns a ContractCaller
type which can be used to call contracts. The methods of the ABI become the methods available on this contract caller: send_funds
and receive_funds
. We then directly call the contract ABI method as if it was just a regular method. You also have the option of specifying the following special parameters inside curly braces right before the main list of parameters:
gas
: au64
that represents the gas being forwarded to the contract when it is called.coins
: au64
that represents how many coins are being forwarded with this call.asset_id
: ab256
that represents the ID of the asset type of the coins being forwarded.
Each special parameter is optional and assumes a default value when skipped:
- The default value for
gas
is the context gas (i.e. the content of the special register$cgas
). Refer to the FuelVM specifications for more information about context gas. - The default value for
coins
is 0. - The default value for
asset_id
isZERO_B256
.
Libraries
Libraries in Sway are files used to define new common behavior. The most prominent example of this is the Sway Standard Library.
Writing Libraries
Libraries are defined using the library
keyword at the beginning of a file, followed by a name so that they can be imported.
library my_library;
// library code
A good reference library to use when learning library design is the Sway Standard Library. For example, the standard library offers an implementation of enum Option<T>
which is a generic type that represents either the existence of a value using the variant Some(..)
or a value's absence using the variant None
. The Sway file implementing Option<T>
has the following structure:
- The
library
keyword followed by the name of the library:
library option;
- A
use
statement that importsrevert
from another library inside the standard library:
use ::revert::revert;
- The
enum
definition which starts with the keywordpub
to indicate that thisOption<T>
is publically available outside theoption
library:
pub enum Option<T> {
// variants
}
- An
impl
block that implements some methods forOption<T>
:
impl<T> Option<T> {
fn is_some(self) -> bool {
// body of is_some
}
// other methods
}
Now that the library option
is fully written, and because Option<T>
is defined with the pub
keyword, we are now able to import Option<T>
using use std::option::Option;
from any Sway project and have access to all of its variants and methods. That being said, Option
is automatically available in the standard library prelude so you never actually have to import it manually.
Libraries are composed of just a Forc.toml
file and a src
directory, unlike contracts which usually contain a tests
directory and a Cargo.toml
file as well. An example of a library's Forc.toml
:
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "lib.sw"
license = "Apache-2.0"
name = "my_library"
[dependencies]
which denotes the authors, an entry file, the name by which it can be imported, and any dependencies.
For large libraries, it is recommended to have a lib.sw
entry point re-export all other sub-libraries. For example, the lib.sw
of the standard library looks like:
library std;
dep block;
dep storage;
dep constants;
// .. Other deps
with other libraries contained in the src
folder, like the block library (inside of block.sw
):
library block;
// Implementation of the `block` library
The dep
keyword in the main library includes a dependency on another library, making all of its items (such as functions and structs) accessible from the main library. The dep
keyword simply makes the library a dependency and fully accessible within the current context.
Using Libraries
Libraries can be imported using the use
keyword and with a ::
separating the name of the library and the import.
Here is an example of importing the get<T>
and store<T>
functions from the storage
library.
use std::storage::{get, store};
Wildcard imports using *
are supported, but it is always recommended to use explicit imports where possible.
Libraries other than the standard library have to be added as a dependency in Forc.toml
. This can be done by adding a path to the library in the [dependencies]
section. For example:
wallet_lib = { path = "/path/to/wallet_lib" }
Note: the standard library is implicitly available to all Forc projects, that is, you are not required to manually specify
std
as an explicit dependency inForc.toml
.
Scripts
A script is runnable bytecode on the chain which executes once to perform some task. It does not represent ownership of any resources and it cannot be called by a contract. A script can return a single value of any type.
Scripts are state-aware in that while they have no persistent storage (because they only exist during the transaction) they can call contracts and act based upon the returned values and results.
This example script calls a contract:
script;
use std::constants::ZERO_B256;
use wallet_abi::Wallet;
fn main() {
let contract_address = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b;
let caller = abi(Wallet, contract_address);
let amount_to_send = 200;
let recipient_address = Address::from(0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b);
caller.send_funds {
gas: 10000,
coins: 0,
asset_id: ZERO_B256,
}(amount_to_send, recipient_address);
}
Scripts, similar to predicates, rely on a main()
function as an entry point. You can call other functions defined in a script from the main()
function or call another contract via an abi cast.
An example use case for a script would be a router that trades funds through multiple DEXes to get the price for the input asset, or a script to re-adjust a Collateralized Debt Position via a flashloan.
Scripts and the SDKs
Unlike EVM transactions which can call a contract directly (but can only call a single contract), Fuel transactions execute a script, which may call zero or more contracts. The Rust and TypeScript SDKs provide functions to call contract methods as if they were calling contracts directly. Under the hood, the SDKs wrap all contract calls with scripts that contain minimal code to simply make the call and forward script data as call parameters.
Predicates
From the perspective of Sway, predicates are programs that return a Boolean value and which represent ownership of some resource upon execution to true. They have no access to contract storage. Here is a trivial predicate, which always evaluates to true:
predicate;
// All predicates require a main function which returns a Boolean value.
fn main() -> bool {
true
}
Debugging Predicates
Because they don't have any side effects (they are pure), predicates cannot create receipts. Therefore, they cannot have logging or create a stack backtrace. This means that there is no naive way to debug them aside from using a single-stepping debugger (which is a work-in-progress).
As a workaround, the predicate can be written, tested, and debugged first as a script
, and then changed back into a predicate
.
Sway Language basics
Sway is a programming language designed for the FuelVM. It is a statically typed, compiled language with type inference and traits. Sway aims to make smart contract development safer and more performant through the use of strong static analysis and compiler feedback.
Sway basics.
- Variables
- Built-in Types
- Blockchain Types
- Functions
- Structs, Tuples, and Enums
- Methods and Associated Functions
- Control Flow
- Comments and Logging
Variables
Variables in Sway are immutable by default. This means that, by default, once a variable is declared, its value cannot change. This is one of the ways how Sway encourages safe programming, and many modern languages have this same default. Let's take a look at variables in detail.
Declaring a Variable
Let's look at a variable declaration:
let foo = 5;
Great! We have just declared a variable, foo
. What do we know about foo
?
- It is immutable.
- Its value is
5
. - Its type is
u64
, a 64-bit unsigned integer.
u64
is the default numeric type, and represents a 64-bit unsigned integer. See the section Built-in Types for more details.
We can also make a mutable variable. Let's take a look:
let mut foo = 5;
foo = 6;
Now, foo
is mutable, and the reassignment to the number 6
is valid. That is, we are allowed to mutate the variable foo
to change its value.
Type Annotations
A variable declaration can contain a type annotation. A type annotation serves the purpose of declaring the type, in addition to the value, of a variable. Let's take a look:
let foo: u32 = 5;
We have just declared the type of the variable foo
as a u32
, which is an unsigned 32-bit integer. Let's take a look at a few other type annotations:
let bar: str[4] = "sway";
let baz: bool = true;
If the value declared cannot be assigned to the declared type, there will be an error generated by the compiler.
Configuration-time Constants
It is possible to define and initialize constant variables in the manifest file Forc.toml
of a Sway project. These constants then become visible and usable in the corresponding Sway program. Such variables are called configuration-time constants and have to be defined in their own section called [constants]
in the manifest file. The syntax for declaring such constants is as follows:
[constants]
some_contract_addr = { type = "b256", value = "0x580acb6ee759d9be0c0f78d3ef24e1b59300c625b3c61999967366dbbebad31c" }
some_num = { type = "u64", value = "42" }
some_string = { type = "str[4]", value = "\"fuel\"" }
true_bool = { type = "bool", value = "true" }
Notice that each constant requires two fields: a type
and a value
.
Note Because configuration-time constants are constants, they are immutable and cannot be made otherwise.
The constants defined above can now be used in a Sway program that uses the manifest file as follows:
script;
struct S {
x: u64,
}
fn main() -> u64 {
let addr = some_contract_addr;
let string = some_string;
return if true_bool { some_num } else { 0 };
}
Note Currently, it is only possible to define configuration-time constants that have primitive types and that are initialized using literals. This will change in the future.
Built-in Types
Every value in Sway is of a certain type. Although deep down, all values are just ones and zeroes in the underlying virtual machine, Sway needs to know what those ones and zeroes actually mean. This is accomplished with types.
Sway is a statically typed language. At compile time, the types of every value must be known. This does not mean you need to specify every single type: usually, the type can be reasonably inferred by the compiler.
Primitive Types
Sway has the following primitive types:
u8
(8-bit unsigned integer)u16
(16-bit unsigned integer)u32
(32-bit unsigned integer)u64
(64-bit unsigned integer)str[]
(fixed-length string)bool
(Booleantrue
orfalse
)b256
(256 bits (32 bytes), i.e. a hash)
All other types in Sway are built up of these primitive types, or references to these primitive types. You may notice that there are no signed integers—this is by design. In the blockchain domain that Sway occupies, floating-point values and negative numbers have smaller utility, so their implementation has been left up to libraries for specific use cases.
Numeric Types
All of the unsigned integer types are numeric types.
Numbers can be declared with binary syntax, hexadecimal syntax, base-10 syntax, and underscores for delineation. Let's take a look at the following valid numeric primitives:
0xffffff // hexadecimal
0b10101010 // binary
10 // base-10
100_000 // underscore delineated base-10
0x1111_0000 // underscore delineated binary
0xfff_aaa // underscore delineated hexadecimal
The default numeric type is u64
. The FuelVM's word size is 64 bits, and the cases where using a smaller numeric type saves space are minimal.
Boolean Type
The boolean type (bool
) has two potential values: true
or false
. Boolean values are typically used for conditional logic or validation, for example in if
expressions. Booleans can be negated, or flipped, with the unary negation operator !
. For example:
fn returns_false() -> bool {
let boolean_value: bool = true;
!boolean_value
}
String Type
In Sway, static-length strings are a primitive type. This means that when you declare a string, its size is a part of its type. This is necessary for the compiler to know how much memory to give for the storage of that data. The size of the string is denoted with square brackets. Let's take a look:
let my_string: str[4] = "fuel";
Because the string literal "fuel"
is four letters, the type is str[4]
, denoting a static length of 4 characters. Strings default to UTF-8 in Sway.
Compound Types
Compound types are types that group multiple values into one type. In Sway, we have arrays and tuples.
Tuple Types
A tuple is a general-purpose static-length aggregation of types. In more plain terms, a tuple is a single type that consists of an aggregate of zero or more types. The internal types that make up a tuple, and the tuple's arity, define the tuple's type. Let's take a look at some examples.
let x: (u64, u64) = (0, 0);
This is a tuple, denoted by parenthesized, comma-separated values. Note that the type annotation, (u64, u64)
, is similar in syntax to the expression which instantiates that type, (0, 0)
.
let x: (u64, bool) = (42, true);
assert(x.1);
In this example, we have created a new tuple type, (u64, bool)
, which is a composite of a u64
and a bool
. To access a value within a tuple, we use tuple indexing: x.1
stands for the first (zero-indexed, so the bool
) value of the tuple. Likewise, x.0
would be the zeroth, u64
value of the tuple. Tuple values can also be accessed via destructuring:
struct Foo {}
let x: (u64, Foo, bool) = (42, Foo {}, true);
let (number, foo, boolean) = x;
To create one-arity tuples, we will need to add a trailing comma:
let x: u64 = (42); // x is of type u64
let y: (u64) = (42); // y is of type u64
let z: (u64,) = (42,); // z is of type (u64), i.e. a one-arity tuple
let w: (u64) = (42,); // type error
Arrays
An array is similar to a tuple, but an array's values must all be of the same type. Arrays can hold arbitrary types include non-primitive types.
An array is written as a comma-separated list inside square brackets:
let x = [1, 2, 3, 4, 5];
Arrays are allocated on the stack since their size is known. An array's size is always static, i.e. it cannot change. An array of five elements cannot become an array of six elements.
Arrays can be iterated over, unlike tuples. An array's type is written as the type the array contains followed by the number of elements, semicolon-separated and within square brackets, e.g. [u64; 5]
. To access an element in an array, use the array indexing syntax, i.e. square brackets.
script;
struct Foo {
f1: u32,
f2: b256,
}
fn main() {
// Array of integers with type ascription
let array_of_integers: [u8; 5] = [1, 2, 3, 4, 5];
// Array of strings
let array_of_strings = ["Bob", "Jan", "Ron"];
// Array of structs
let array_of_structs: [Foo; 2] = [
Foo {
f1: 11,
f2: 0x1111111111111111111111111111111111111111111111111111111111111111,
},
Foo {
f1: 22,
f2: 0x2222222222222222222222222222222222222222222222222222222222222222,
},
];
// Accessing an element of an array
let array_of_bools: [bool; 2] = [true, false];
assert(array_of_bools[0]);
}
Note: Arrays are currently immutable which means that changing elements of an array once initialized is not yet possible.
Blockchain Types
Sway is fundamentally a blockchain language, and it offers a selection of types tailored for the blockchain use case.
These are provided via the standard library (lib-std
) which both add a degree of type-safety, as well as make the intention of the developer more clear.
Address
Type
The Address
type is a type-safe wrapper around the primitive b256
type. Unlike the EVM, an address never refers to a deployed smart contract (see the ContractId
type below). An Address
can be either the hash of a public key (effectively an externally owned account if you're coming from the EVM) or the hash of a predicate. Addresses own UTXOs.
An Address
is implemented as follows.
pub struct Address {
value: b256,
}
Casting between the b256
and Address
types must be done explicitly:
let my_number: b256 = 0x000000000000000000000000000000000000000000000000000000000000002A;
let my_address: Address = Address::from(my_number);
let forty_two: b256 = my_address.into();
ContractId
Type
The ContractId
type is a type-safe wrapper around the primitive b256
type. A contract's ID is a unique, deterministic identifier analogous to a contract's address in the EVM. Contracts cannot own UTXOs but can own assets.
A ContractId
is implemented as follows.
pub struct ContractId {
value: b256,
}
Casting between the b256
and ContractId
types must be done explicitly:
let my_number: b256 = 0x000000000000000000000000000000000000000000000000000000000000002A;
let my_contract_id: ContractId = ContractId::from(my_number);
let forty_two: b256 = my_contract_id.into();
Identity
Type
The Identity
type is an enum that allows for the handling of both Address
and ContractId
types. This is useful in cases where either type is accepted, e.g. receiving funds from an identified sender, but not caring if the sender is an address or a contract.
An Identity
is implemented as follows.
pub enum Identity {
Address: Address,
ContractId: ContractId,
}
Casting to an Identity
must be done explicitly:
let raw_address: b256 = 0xddec0e7e6a9a4a4e3e57d08d080d71a299c628a46bc609aab4627695679421ca;
let my_identity: Identity = Identity::Address(Address::from(raw_address));
A match
statement can be used to return to an Address
or ContractId
as well as handle cases in which their execution differs.
let my_contract_id: ContractId = match my_identity {
Identity::ContractId(identity) => identity,
_ => revert(0),
};
match my_identity {
Identity::Address(address) => transfer_to_address(amount, token_id, address),
Identity::ContractId(contract_id) => force_transfer_to_contract(amount, token_id, contract_id),
};
A common use case for Identity
is for access control. The use of Identity
uniquely allows both ContractId
and Address
to have access control inclusively.
let sender: Result<Identity, AuthError> = msg_sender();
require(sender.unwrap() == storage.owner, MyError::UnauthorizedUser);
Functions
Functions in Sway are declared with the fn
keyword. Let's take a look:
fn equals(first_param: u64, second_param: u64) -> bool {
first_param == second_param
}
We have just declared a function named equals
which takes two parameters: first_param
and second_param
. The parameters must both be 64-bit unsigned integers.
This function also returns a bool
value, i.e. either true
or false
. This function returns true
if the two given parameters are equal, and false
if they are not. If we want to use this function, we can do so like this:
fn main() {
equals(5, 5); // evaluates to `true`
equals(5, 6); // evaluates to `false`
}
Mutable Parameters
We can make a function parameter mutable by adding ref mut
before the parameter name. This allows mutating the argument passed into the function when the function is called. For example:
fn increment(ref mut num: u32) {
let prev = num;
num = prev + 1u32;
}
This function is allowed to mutate its parameter num
because of the mut
keyword. In addition, the ref
keyword instructs the function to modify the argument passed to it when the function is called, instead of modifying a local copy of it.
let mut num: u32 = 0;
increment(num);
assert(num == 1u32); // The function `increment()` modifies `num`
Note that the variable num
itself has to be declared as mutable for the above to compile.
Note It is not currently allowed to use
mut
withoutref
or vice versa for a function parameter.
Similarly, ref mut
can be used with more complex data types such as:
fn swap_tuple(ref mut pair: (u64, u64)) {
let temp = pair.0;
pair.0 = pair.1;
pair.1 = temp;
}
fn update_color(ref mut color: Color, new_color: Color) {
color = new_color;
}
We can then call these functions as shown below:
let mut tuple = (42, 24);
swap_tuple(tuple);
assert(tuple.0 == 24); // The function `swap_tuple()` modifies `tuple.0`
assert(tuple.1 == 42); // The function `swap_tuple()` modifies `tuple.1`
let mut color = Color::Red;
update_color(color, Color::Blue);
assert(match color {
Color::Blue => true,
_ => false,
}); // The function `update_color()` modifies the color to Blue
Note The only place, in a Sway program, where the
ref
keyword is valid is before a mutable function parameter.
Structs, Tuples, and Enums
Structs
Structs in Sway are a named grouping of types. You may also be familiar with structs via another name: product types. Sway does not make any significantly unique usages of structs; they are similar to most other languages which have structs. If you're coming from an object-oriented background, a struct is like the data attributes of an object.
Firstly, we declare a struct named Foo
with two fields. The first field is named bar
and it accepts values of type u64
, the second field is named baz
and it accepts bool
values.
library data_structures;
// Declare a struct type
pub struct Foo {
bar: u64,
baz: bool,
}
// Struct types for destructuring
pub struct Point {
x: u64,
y: u64,
}
pub struct Line {
p1: Point,
p2: Point,
}
pub struct TupleInStruct {
nested_tuple: (u64, (u32, (bool, str[2]))),
}
In order to instantiate the struct we use struct instantiation syntax, which is very similar to the declaration syntax except with expressions in place of types.
There are three ways to instantiate the struct.
- Hardcoding values for the fields
- Passing in variables with names different than the struct fields
- Using a shorthand notation via variables that are the same as the field names
library utils;
dep data_structures;
use data_structures::{Foo, Line, Point, TupleInStruct};
fn hardcoded_instantiation() -> Foo {
// Instantiate `foo` as `Foo`
let mut foo = Foo {
bar: 42,
baz: false,
};
// Access and write to "baz"
foo.baz = true;
// Return the struct
foo
}
fn variable_instantiation() -> Foo {
// Declare variables with the same names as the fields in `Foo`
let number = 42;
let truthness = false;
// Instantiate `foo` as `Foo`
let mut foo = Foo {
bar: number,
baz: truthness,
};
// Access and write to "baz"
foo.baz = true;
// Return the struct
foo
}
fn shorthand_instantiation() -> Foo {
// Declare variables with the same names as the fields in `Foo`
let bar = 42;
let baz = false;
// Instantiate `foo` as `Foo`
let mut foo = Foo { bar, baz };
// Access and write to "baz"
foo.baz = true;
// Return the struct
foo
}
fn struct_destructuring() {
let point1 = Point { x: 0, y: 0 };
// Destructure the values from the struct into variables
let Point { x, y } = point1;
let point2 = Point { x: 1, y: 1 };
// If you do not care about specific struct fields then use ".." at the end of your variable list
let Point { x, .. } = point2;
let line = Line {
p1: point1,
p2: point2,
};
// Destructure the values from the nested structs into variables
let Line {
p1: Point { x: x0, y: y0 },
p2: Point { x: x1, y: y1 },
} = line;
// You may also destructure tuples nested in structs and structs nested in tuples
let tuple_in_struct = TupleInStruct {
nested_tuple: (42u64, (42u32, (true, "ok"))),
};
let TupleInStruct {
nested_tuple: (a, (b, (c, d))),
} = tuple_in_struct;
let struct_in_tuple = (Point { x: 2, y: 4 }, Point { x: 3, y: 6 });
let (Point { x: x0, y: y0 }, Point { x: x1, y: y1 }) = struct_in_tuple;
}
Note You can mix and match all 3 ways to instantiate the struct at the same time. Moreover, the order of the fields does not matter when instantiating however we encourage declaring the fields in alphabetical order and instantiating them in the same alphabetical order
Furthermore, multiple variables can be extracted from a struct using the destructuring syntax.
Struct Memory Layout
Note This information is not vital if you are new to the language, or programming in general
Structs have zero memory overhead. What that means is that in memory, each struct field is laid out sequentially. No metadata regarding the struct's name or other properties is preserved at runtime. In other words, structs are compile-time constructs. This is the same in Rust, but different in other languages with runtimes like Java.
Tuples
Tuples are a basic static-length type which contain multiple different types within themselves. The type of a tuple is defined by the types of the values within it, and a tuple can contain basic types as well as structs and enums.
You can access values directly by using the .
syntax. Moreover, multiple variables can be extracted from a tuple using the destructuring syntax.
library tuples;
fn tuple() {
// You can declare the types youself
let tuple1: (u8, bool, u64) = (100, false, 10000);
// Or have the types be inferred
let mut tuple2 = (5, true, ("Sway", 8));
// Retrieve values from tuples
let number = tuple1.0;
let sway = tuple2.2.1;
// Destructure the values from the tuple into variables
let (n1, truthness, n2) = tuple1;
// If you do not care about specific values then use "_"
let (_, truthness, _) = tuple2;
// Internally mutate the tuple
tuple2.1 = false;
// Or change the values all at once (must keep the same data types)
tuple2 = (9, false, ("Fuel", 99));
}
Enums
Enumerations, or enums, are also known as sum types. An enum is a type that could be one of several variants. To declare an enum, you enumerate all potential variants.
Here, we have defined five potential colors. Each enum variant is just the color name. As there is no extra data associated with each variant, we say that each variant is of type ()
, or unit.
library basic_enum;
// Declare the enum
enum Color {
Blue: (),
Green: (),
Red: (),
Silver: (),
Grey: (),
}
fn main() {
// To instantiate a variable with the value of an enum the syntax is
let blue = Color::Blue;
let silver = Color::Silver;
}
Enums of Structs
It is also possible to have an enum variant contain extra data. Take a look at this more substantial example, which combines struct declarations with enum variants:
library enum_of_structs;
struct Item {
price: u64,
amount: u64,
id: u64,
}
enum MyEnum {
Item: Item,
}
fn main() {
let my_enum = MyEnum::Item(Item {
price: 5,
amount: 2,
id: 42,
});
}
Enums of Enums
It is possible to define enums of enums:
library enum_of_enums;
pub enum Error {
StateError: StateError,
UserError: UserError,
}
pub enum StateError {
Void: (),
Pending: (),
Completed: (),
}
pub enum UserError {
InsufficientPermissions: (),
Unauthorized: (),
}
Preferred usage
The preferred way to use enums is to use the individual (not nested) enums directly because they are easy to follow and the lines are short:
library enums_preferred;
dep enum_of_enums;
use enum_of_enums::{StateError, UserError};
fn preferred() {
let error1 = StateError::Void;
let error2 = UserError::Unauthorized;
}
Inadvisable
If you wish to use the nested form of enums via the Error
enum from the example above, then you can instantiate them into variables using the following syntax:
library enums_avoid;
dep enum_of_enums;
use enum_of_enums::{Error, StateError, UserError};
fn avoid() {
let error1 = Error::StateError(StateError::Void);
let error2 = Error::UserError(UserError::Unauthorized);
}
Key points to note:
- You must import all of the enums you need instead of just the
Error
enum - The lines may get unnecessarily long (depending on the names)
- The syntax is not the most ergonomic
Enum Memory Layout
Note This information is not vital if you are new to the language, or programming in general.
Enums do have some memory overhead. To know which variant is being represented, Sway stores a one-word (8-byte) tag for the enum variant. The space reserved after the tag is equivalent to the size of the largest enum variant. So, to calculate the size of an enum in memory, add 8 bytes to the size of the largest variant. For example, in the case of Color
above, where the variants are all ()
, the size would be 8 bytes since the size of the largest variant is 0 bytes.
Methods and Associated Functions
Methods are similar to functions in that we declare them with the fn
keyword and they have parameters and return a value. However, unlike functions, Methods are defined within the context of a struct (or enum), and either refers to that type or mutates it. The first parameter of a method is always self
, which represents the instance of the struct the method is being called on.
Associated functions are very similar to methods, in that they are also defined in the context of a struct or enum, but they do not actually use any of the data in the struct and as a result do not take self as a parameter. Associated functions could be standalone functions, but they are included in a specific type for organizational or semantic reasons.
To declare methods and associated functions for a struct or enum, use an impl block. Here, impl
stands for implementation.
script;
struct Foo {
bar: u64,
baz: bool,
}
impl Foo {
// this is a _method_, as it takes `self` as a parameter.
fn is_baz_true(self) -> bool {
self.baz
}
// this is an _associated function_, since it does not take `self` as a parameter.
fn new_foo(number: u64, boolean: bool) -> Foo {
Foo {
bar: number,
baz: boolean,
}
}
}
fn main() {
let foo = Foo::new_foo(42, true);
assert(foo.is_baz_true());
}
To call a method, simply use dot syntax: foo.iz_baz_true()
.
Similarly to free functions, methods and associated functions may accept ref mut
parameters. For example:
struct Coordinates {
x: u64,
y: u64,
}
impl Coordinates {
fn move_right(ref mut self, distance: u64) {
self.x += distance;
}
}
and when called:
let mut point = Coordinates { x: 1, y: 1 };
point.move_right(5);
assert(point.x == 6);
assert(point.y == 1);
Comments and Logging
Comments
Comments in Sway start with two slashes and continue until the end of the line. For comments that extend beyond a single line, you'll need to include //
on each line.
// hello world
// let's make a couple of lines
// commented.
You can also place comments at the ends of lines containing code.
fn main() {
let baz = 8; // Eight is a lucky number
}
You can also do block comments
fn main() {
/*
You can write on multiple lines
like this if you want
*/
let baz = 8;
}
Logging
The logging
library provides a generic log
function that can be imported using use std::logging::log
and used to log variables of any type. Each call to log
appends a receipt
to the list of receipts. There are two types of receipts that a log
can generate: Log
and LogData
.
Log
Receipt
The Log
receipt is generated for non-reference types, namely bool
, u8
, u16
, u32
, and u64
. For example, logging an integer variable x
that holds the value 42
using log(x)
may generate the following receipt:
"Log": {
"id": "0000000000000000000000000000000000000000000000000000000000000000",
"is": 10352,
"pc": 10404,
"ra": 42,
"rb": 1018205,
"rc": 0,
"rd": 0
}
Note that ra
will include the value being logged. The additional registers rc
and rd
will be zero when using log
while rb
may include a non-zero value representing a unique ID for the log
instance. The unique ID is not meaningful on its own but allows the Rust and the TS SDKs to know the type of the data being logged, by looking up the log ID in the JSON ABI file.
LogData
Receipt
LogData
is generated for reference types which include all types except for the non_reference types mentioned above. For example, logging a b256
variable b
that holds the value 0x1111111111111111111111111111111111111111111111111111111111111111
using log(b)
may generate the following receipt:
"LogData": {
"data": "1111111111111111111111111111111111111111111111111111111111111111",
"digest": "02d449a31fbb267c8f352e9968a79e3e5fc95c1bbeaa502fd6454ebde5a4bedc",
"id": "0000000000000000000000000000000000000000000000000000000000000000",
"is": 10352,
"len": 32,
"pc": 10444,
"ptr": 10468,
"ra": 0,
"rb": 1018194
}
Note that data
in the receipt above will include the value being logged as a hexadecimal. Similarly to the Log
receipt, additional registers are written: ra
will always be zero when using log
, while rb
will contain a unique ID for the log
instance.
Note The Rust SDK exposes APIs that allow you to retrieve the logged values and display them nicely based on their types as indicated in the JSON ABI file.
Control Flow
if
expressions
Sway supports if, else, and else if expressions that allow you to branch your code depending on conditions.
For example:
fn main() {
let number = 6;
if number % 4 == 0 {
// do something
} else if number % 3 == 0 {
// do something else
} else {
// do something else
}
}
Using if
in a let
statement
Like Rust, if
s are expressions in Sway. What this means is you can use if
expressions on the right side of a let
statement to assign the outcome to a variable.
let my_data = if some_bool < 10 { foo() } else { bar() };
Note that all branches of the if
expression must return a value of the same type.
match
expressions
Sway supports advanced pattern matching through exhaustive match
expressions.
script;
fn foo() {}
// do something
fn bar() {}
// do something
enum SomeEnum {
A: u64,
B: bool,
C: b256,
}
fn main() -> u64 {
let x = 5;
// Match as an expression.
let a = match 8 {
7 => 4,
9 => 5,
8 => 6,
_ => 100,
};
// Match as a statement for control flow.
match x {
5 => foo(),
_ => bar(),
};
// Match an enum
let e = SomeEnum::A(42);
let v = match e {
SomeEnum::A(val) => val,
SomeEnum::B(true) => 1,
SomeEnum::B(false) => 0,
_ => 0,
};
// Match as expression used for a return.
match 42 {
0 => 24,
foo => foo,
}
}
Loops
while
Loops in Sway are currently limited to while
loops. This is what they look like:
while counter < 10 {
counter = counter + 1;
}
You need the while
keyword, some condition (value < 10
in this case) which will be evaluated each iteration, and a block of code inside the curly braces ({...}
) to execute each iteration.
break
and continue
break
and continue
keywords are available to use inside the body of a while
loop. The purpose of the break
statement is to break out of a loop early:
fn break_example() -> u64 {
let mut counter = 1;
let mut sum = 0;
let num = 10;
while true {
if counter > num {
break;
}
sum += counter;
counter += 1;
}
sum // 1 + 2 + .. + 10 = 55
}
The purpose of the continue
statement is to skip a portion of a loop in an iteration and jump directly into the next iteration:
fn continue_example() -> u64 {
let mut counter = 0;
let mut sum = 0;
let num = 10;
while counter < num {
counter += 1;
if counter % 2 == 0 {
continue;
}
sum += counter;
}
sum // 1 + 3 + .. + 9 = 25
}
Nested loops
You can also use nested while
loops if needed:
while condition_1 == true {
// do stuff...
while condition_2 == true {
// do more stuff...
}
}
Blockchain Development with Sway
Sway is fundamentally a blockchain language. Because of this, it has some features and requirements that you may not have seen in general-purpose programming languages.
These are also some concepts related to the FuelVM and Fuel ecosystem that you may utilize when writing Sway.
- Hashing and Cryptography
- Contract Storage
- Function Purity
- Identifiers
- Native Assets
- Access Control
- Calling Contracts
Hashing and Cryptography
The Sway standard library provides easy access to a selection of cryptographic hash functions (sha256
and EVM-compatible keccak256
), and EVM-compatible secp256k1
-based signature recovery operations.
Hashing
script;
use core::num::*;
use std::{hash::{keccak256, sha256}, logging::log};
const VALUE_A = 0x9280359a3b96819889d30614068715d634ad0cf9bba70c0f430a8c201138f79f;
enum Location {
Earth: (),
Mars: (),
}
struct Person {
name: str[4],
age: u64,
alive: bool,
location: Location,
stats: Stats,
some_tuple: (bool, u64),
some_array: [u64; 2],
some_b256: b256,
}
struct Stats {
strength: u64,
agility: u64,
}
fn main() {
let zero = b256::min();
// Use the generic sha256 to hash some integers
let sha_hashed_u8 = sha256(u8::max());
let sha_hashed_u16 = sha256(u16::max());
let sha_hashed_u32 = sha256(u32::max());
let sha_hashed_u64 = sha256(u64::max());
// Or hash a b256
let sha_hashed_b256 = sha256(VALUE_A);
// You can hash booleans too
let sha_hashed_bool = sha256(true);
// Strings are not a problem either
let sha_hashed_str = sha256("Fastest Modular Execution Layer!");
// Tuples of any size work too
let sha_hashed_tuple = sha256((true, 7));
// As do arrays
let sha_hashed_array = sha256([4, 5, 6]);
// Enums work too
let sha_hashed_enum = sha256(Location::Earth);
// Complex structs are not a problem
let sha_hashed_struct = sha256(Person {
name: "John",
age: 9000,
alive: true,
location: Location::Mars,
stats: Stats {
strength: 10,
agility: 9,
},
some_tuple: (true, 8),
some_array: [17, 76],
some_b256: zero,
});
log(sha_hashed_u8);
log(sha_hashed_u16);
log(sha_hashed_u32);
log(sha_hashed_u64);
log(sha_hashed_b256);
log(sha_hashed_bool);
log(sha_hashed_str);
log(sha_hashed_tuple);
log(sha_hashed_array);
log(sha_hashed_enum);
log(sha_hashed_struct);
// Use the generic keccak256 to hash some integers
let keccak_hashed_u8 = keccak256(u8::max());
let keccak_hashed_u16 = keccak256(u16::max());
let keccak_hashed_u32 = keccak256(u32::max());
let keccak_hashed_u64 = keccak256(u64::max());
// Or hash a b256
let keccak_hashed_b256 = keccak256(VALUE_A);
// You can hash booleans too
let keccak_hashed_bool = keccak256(true);
// Strings are not a problem either
let keccak_hashed_str = keccak256("Fastest Modular Execution Layer!");
// Tuples of any size work too
let keccak_hashed_tuple = keccak256((true, 7));
// As do arrays
let keccak_hashed_array = keccak256([4, 5, 6]);
// Enums work too
let keccak_hashed_enum = keccak256(Location::Earth);
// Complex structs are not a problem
let keccak_hashed_struct = keccak256(Person {
name: "John",
age: 9000,
alive: true,
location: Location::Mars,
stats: Stats {
strength: 10,
agility: 9,
},
some_tuple: (true, 8),
some_array: [17, 76],
some_b256: zero,
});
log(keccak_hashed_u8);
log(keccak_hashed_u16);
log(keccak_hashed_u32);
log(keccak_hashed_u64);
log(keccak_hashed_b256);
log(keccak_hashed_bool);
log(keccak_hashed_str);
log(keccak_hashed_tuple);
log(keccak_hashed_array);
log(keccak_hashed_enum);
log(keccak_hashed_struct);
}
Signature Recovery
script;
use std::{b512::B512, ecr::{ec_recover, ec_recover_address, EcRecoverError}, logging::log};
const MSG_HASH = 0xee45573606c96c98ba970ff7cf9511f1b8b25e6bcd52ced30b89df1e4a9c4323;
fn main() {
let hi = 0xbd0c9b8792876713afa8bff383eebf31c43437823ed761cc3600d0016de5110c;
let lo = 0x44ac566bd156b4fc71a4a4cb2655d3dd360c695edb17dc3b64d611e122fea23d;
let signature: B512 = B512::from((hi, lo));
// A recovered public key pair.
let public_key = ec_recover(signature, MSG_HASH);
// A recovered Fuel address.
let result_address: Result<Address, EcRecoverError> = ec_recover_address(signature, MSG_HASH);
if let Result::Ok(address) = result_address {
log(address.value);
} else {
revert(0);
}
}
Note: Recovery of EVM addresses is also supported via
std::vm::evm
.
Storage
When developing a smart contract, you will typically need some sort of persistent storage. In this case, persistent storage, often just called storage in this context, is a place where you can store values that are persisted inside the contract itself. This is in contrast to a regular value in memory, which disappears after the contract exits.
Put in conventional programming terms, contract storage is like saving data to a hard drive. That data is saved even after the program which saved it exits. That data is persistent. Using memory is like declaring a variable in a program: it exists for the duration of the program and is non-persistent.
Some basic use cases of storage include declaring an owner address for a contract and saving balances in a wallet.
Storage Accesses Via the storage
Keyword
Declaring variables in storage requires a storage
declaration that contains a list of all your variables, their types, and their initial values as follows:
struct Type1 {
x: u64,
y: u64,
}
struct Type2 {
w: b256,
z: bool,
}
storage {
var1: Type1 = Type1 { x: 0, y: 0 },
var2: Type2 = Type2 {
w: 0x0000000000000000000000000000000000000000000000000000000000000000,
z: false,
},
}
To write into a storage variable, you need to use the storage
keyword as follows:
#[storage(write)]
fn store_something() {
storage.var1.x = 42;
storage.var1.y = 77;
storage.var2.w = 0x1111111111111111111111111111111111111111111111111111111111111111;
storage.var2.z = true;
}
To read a storage variable, you also need to use the storage
keyword as follows:
#[storage(read)]
fn get_something() -> (u64, u64, b256, bool) {
(
storage.var1.x,
storage.var1.y,
storage.var2.w,
storage.var2.z,
)
}
Storage Maps
Generic storage maps are available in the standard library as StorageMap<K, V>
which have to be defined inside a storage
block and allow you to call insert()
and get()
to insert values at specific keys and get those values respectively. Refer to Storage Maps for more information about StorageMap<K, V>
.
Manual Storage Management
It is possible to leverage FuelVM storage operations directly using the std::storage::store
and std::storage::get
functions provided in the standard library. With this approach you will have to manually assign the internal key used for storage. An example is as follows:
contract;
use std::storage::{get, store};
abi StorageExample {
#[storage(write)]
fn store_something(amount: u64);
#[storage(read)]
fn get_something() -> u64;
}
const STORAGE_KEY: b256 = 0x0000000000000000000000000000000000000000000000000000000000000000;
impl StorageExample for Contract {
#[storage(write)]
fn store_something(amount: u64) {
store(STORAGE_KEY, amount);
}
#[storage(read)]
fn get_something() -> u64 {
let value = get::<u64>(STORAGE_KEY);
value
}
}
Note: Though these functions can be used for any data type, they should mostly be used for arrays because arrays are not yet supported in
storage
blocks. Note, however, that all data types can be used as types for keys and/or values inStorageMap<K, V>
without any restrictions.
Purity
A function is pure if it does not access any persistent storage. Conversely, the function is impure if it does access any storage. Naturally, as storage is only available in smart contracts, impure functions cannot be used in predicates, scripts, or libraries. A pure function cannot call an impure function.
In Sway, functions are pure by default but can be opted into impurity via the storage
function attribute. The storage
attribute may take read
and/or write
arguments indicating which type of access the function requires.
#[storage(read)]
fn get_amount() -> u64 {
...
}
#[storage(read, write)]
fn increment_amount(increment: u64) -> u64 {
...
}
Impure functions which call other impure functions must have at least the same storage privileges or a superset of those for the function called. For example, to call a function with write access a caller must also have write access, or both read and write access. To call a function with read and write access the caller must also have both privileges.
The storage
attribute may also be applied to methods and associated functions, trait and ABI declarations.
A pure function gives you some guarantees: you will not incur excessive storage gas costs, the compiler can apply additional optimizations, and they are generally easy to reason about and audit. A similar concept exists in Solidity. Note that Solidity refers to contract storage as contract state, and in the Sway/Fuel ecosystem, these two terms are largely interchangeable.
Identifiers
Addresses in Sway are similar to EVM addresses. The two major differences are:
- Sway addresses are 32 bytes long (instead of 20)
- Sway addresses are computed with the SHA-256 hash of the public key instead of the keccak-256 hash.
Contracts, on the other hand, are uniquely identified with a contract ID rather than an address. A contract's ID is also 32 bytes long and is calculated here.
Native Support for Multiple Asset Types
The FuelVM has built-in support for working with multiple assets.
What does this mean in practice?
As in the EVM, sending ETH to an address or contract is an operation built into the FuelVM, meaning it doesn't rely on the existence of some token smart contract to update balances to track ownership.
However, unlike the EVM, the process for sending any native asset is the same. This means that while you would still need a smart contract to handle the minting and burning of fungible tokens, the sending and receiving of these tokens can be done independently of the token contract.
Liquidity Pool Example
All contracts in Fuel can mint and burn their own native token. Contracts can also receive and transfer any native asset including their own. Internal balances of all native assets pushed through calls or minted by the contract are tracked by the FuelVM and can be queried at any point using the balance_of function from the std
library. Therefore, there is no need for any manual accounting of the contract's balances using persistent storage.
The std
library provides handy methods for accessing Fuel's native assset operations.
In this example, we show a basic liquidity pool contract minting its own native asset LP token.
contract;
use std::{
call_frames::{
contract_id,
msg_asset_id,
},
context::msg_amount,
token::{
mint_to_address,
transfer_to_address,
},
};
abi LiquidityPool {
fn deposit(recipient: Address);
fn withdraw(recipient: Address);
}
const BASE_TOKEN = ContractId::from(0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c);
impl LiquidityPool for Contract {
fn deposit(recipient: Address) {
assert(msg_asset_id() == BASE_TOKEN);
assert(msg_amount() > 0);
// Mint two times the amount.
let amount_to_mint = msg_amount() * 2;
// Mint some LP token based upon the amount of the base token.
mint_to_address(amount_to_mint, recipient);
}
fn withdraw(recipient: Address) {
assert(msg_asset_id() == contract_id());
assert(msg_amount() > 0);
// Amount to withdraw.
let amount_to_transfer = msg_amount() / 2;
// Transfer base token to recipient.
transfer_to_address(amount_to_transfer, BASE_TOKEN, recipient);
}
}
Native Token Example
In this example, we show a native token contract with more minting, burning and transferring capabilities.
contract;
use std::{context::*, token::*};
abi NativeAssetToken {
fn mint_coins(mint_amount: u64);
fn burn_coins(burn_amount: u64);
fn force_transfer_coins(coins: u64, asset_id: ContractId, target: ContractId);
fn transfer_coins_to_output(coins: u64, asset_id: ContractId, recipient: Address);
fn deposit();
fn get_balance(target: ContractId, asset_id: ContractId) -> u64;
fn mint_and_send_to_contract(amount: u64, destination: ContractId);
fn mint_and_send_to_address(amount: u64, recipient: Address);
}
impl NativeAssetToken for Contract {
/// Mint an amount of this contracts native asset to the contracts balance.
fn mint_coins(mint_amount: u64) {
mint(mint_amount);
}
/// Burn an amount of this contracts native asset.
fn burn_coins(burn_amount: u64) {
burn(burn_amount);
}
/// Transfer coins to a target contract.
fn force_transfer_coins(coins: u64, asset_id: ContractId, target: ContractId) {
force_transfer_to_contract(coins, asset_id, target);
}
/// Transfer coins to a transaction output to be spent later.
fn transfer_coins_to_output(coins: u64, asset_id: ContractId, recipient: Address) {
transfer_to_address(coins, asset_id, recipient);
}
/// Get the internal balance of a specific coin at a specific contract.
fn get_balance(target: ContractId, asset_id: ContractId) -> u64 {
balance_of(target, asset_id)
}
/// Deposit tokens back into the contract.
fn deposit() {
assert(msg_amount() > 0);
}
/// Mint and send this contracts native token to a destination contract.
fn mint_and_send_to_contract(amount: u64, destination: ContractId) {
mint_to_contract(amount, destination);
}
/// Mind and send this contracts native token to a destination address.
fn mint_and_send_to_address(amount: u64, recipient: Address) {
mint_to_address(amount, recipient);
}
}
Access Control
Smart contracts require the ability to restrict access to and identify certain users or contracts. Unlike account-based blockchains, transactions in UTXO-based blockchains (i.e. Fuel) do not necessarily have a unique transaction sender. Additional logic is needed to handle this difference, and is provided by the standard library.
msg_sender
To deliver an experience akin to the EVM's access control, the std
library provides a msg_sender
function, which identifies a unique caller based upon the call and/or transaction input data.
contract;
use std::auth::{AuthError, msg_sender};
abi MyOwnedContract {
fn receive(field_1: u64) -> bool;
}
const OWNER = Address::from(0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c);
impl MyOwnedContract for Contract {
fn receive(field_1: u64) -> bool {
let sender: Result<Identity, AuthError> = msg_sender();
if let Identity::Address(addr) = sender.unwrap() {
assert(addr == OWNER);
} else {
revert(0);
}
true
}
}
The msg_sender
function works as follows:
- If the caller is a contract, then
Result::Ok(Sender)
is returned with theContractId
sender variant. - If the caller is external (i.e. from a script), then all coin input owners in the transaction are checked. If all owners are the same, then
Result::Ok(Sender)
is returned with theAddress
sender variant. - If the caller is external and coin input owners are different, then the caller cannot be determined and a
Result::Err(AuthError)
is returned.
Contract Ownership
Many contracts require some form of ownership for access control. To accomplish this, it is recommended that a storage variable of type Option<Identity>
is used to keep track of the owner. This allows setting and revoking ownership using the variants Some(..)
and None
respectively. This is better, safer, and more readable than using the Identity
type directly where revoking ownership has to be done using some magic value such as std::constants::ZERO_B256
or otherwise.
The following is an example of how to properly set ownership of a contract:
#[storage(write)]
fn set_owner(identity: Identity) {
storage.owner = Option::Some(identity);
}
The following is an example of how to properly revoke ownership of a contract:
#[storage(write)]
fn revoke_ownership() {
storage.owner = Option::None();
}
Calling Contracts
Smart contracts can be called by other contracts or scripts. In the FuelVM, this is done primarily with the call
instruction.
Sway provides a nice way to manage callable interfaces with its abi
system. The Fuel ABI specification can be found here.
Example
Here is an example of a contract calling another contract in Sway. A script can call a contract in the same way.
// ./contract_a.sw
contract;
abi ContractA {
fn receive(field_1: bool, field_2: u64) -> u64;
}
impl ContractA for Contract {
fn receive(field_1: bool, field_2: u64) -> u64 {
assert(field_1 == true);
assert(field_2 > 0);
return_45()
}
}
fn return_45() -> u64 {
45
}
// ./contract_b.sw
contract;
use contract_a::ContractA;
abi ContractB {
fn make_call();
}
const contract_id = 0x79fa8779bed2f36c3581d01c79df8da45eee09fac1fd76a5a656e16326317ef0;
impl ContractB for Contract {
fn make_call() {
let x = abi(ContractA, contract_id);
let return_value = x.receive(true, 3); // will be 45
}
}
Note: The ABI is for external calls only therefore you cannot define a method in the ABI and call it in the same contract. If you want to define a function for a contract, but keep it private so that only your contract can call it, you can define it outside of the
impl
and call it inside the contract, similar to thereturn_45()
function above.
Advanced Calls
All calls forward a gas stipend, and may additionally forward one native asset with the call.
Here is an example of how to specify the amount of gas (gas
), the asset ID of the native asset (asset_id
), and the amount of the native asset (amount
) to forward:
script;
abi MyContract {
fn foo(field_1: bool, field_2: u64);
}
fn main() {
let x = abi(MyContract, 0x79fa8779bed2f36c3581d01c79df8da45eee09fac1fd76a5a656e16326317ef0);
let asset_id = 0x7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777;
x.foo {
gas: 5000, asset_id: asset_id, amount: 5000
}
(true, 3);
}
Handling Re-entrancy
A common attack vector for smart contracts is re-entrancy. Similar to the EVM, the FuelVM allows for re-entrancy.
A stateless re-entrancy guard is included in the Sway standard library. The guard will panic (revert) at run time if re-entrancy is detected.
contract;
use std::reentrancy::reentrancy_guard;
abi MyContract {
fn some_method();
}
impl ContractB for Contract {
fn some_method() {
reentrancy_guard();
// do something
}
}
Differences from the EVM
While the Fuel contract calling paradigm is similar to the EVM's (using an ABI, forwarding gas and data), it differs in two key ways:
-
Native assets: FuelVM calls can forward any native asset not just base asset.
-
No data serialization: Contract calls in the FuelVM do not need to serialize data to pass it between contracts; instead they simply pass a pointer to the data. This is because the FuelVM has a shared global memory which all call frames can read from.
Advanced Concepts
Advanced concepts.
Generic Types
Basics
In Sway, generic types follow a very similar pattern to those in Rust. Let's look at some example syntax, starting with a generic function:
fn noop<T>(argument: T) -> T {
argument
}
Here, the noop()
function trivially returns exactly what was given to it. T
is a type parameter, and it says
that this function exists for all types T. More formally, this function could be typed as:
noop :: ∀T. T -> T
Generic types are a way to refer to types in general, meaning without specifying a single type. Our noop
function
would work with any type in the language, so we don't need to specify noop(argument: u8) -> u8
, noop(argument: u16) -> u16
, etc.
Code Generation
One question that arises when dealing with generic types is: how does the assembly handle this? There are a few approaches to handling generic types at the lowest level. Sway uses a technique called monomorphization. This means that the generic function is compiled to a non-generic version for every type it is called on. In this way, generic functions are purely shorthand for the sake of ergonomics.
Trait Constraints
Note Trait constraints have not yet been implemented
Important background to know before diving into trait constraints is that the where
clause can be used to specify the required traits for the generic argument. So, when writing something like a HashMap
you may
want to specify that the generic argument implements a Hash
trait.
fn get_hashmap_key<T>(Key : T) -> b256
where T: Hash
{
// Code within here can then call methods associated with the Hash trait on Key
}
Of course, our noop()
function is not useful. Often, a programmer will want to declare functions over types which satisfy certain traits.
For example, let's try to implement the successor function, successor()
, for all numeric types.
fn successor<T>(argument: T)
where T: Add
{
argument + 1
}
Run forc build
, and you will get:
.. |
9 | where T: Add
10 | {
11 | argument + 1
| ^ Mismatched types: expected type "T" but saw type "u64"
12 | }
13 |
This is because we don't know for a fact that 1
, which in this case defaulted to 1u64
, actually can be added to T
. What if T
is f64
? Or b256
? What does it mean to add 1u64
in these cases?
We can solve this problem with another trait constraint. We can only find the successor of some value of type T
if that type T
defines some incrementor. Let's make a trait:
trait Incrementable {
/// Returns the value to add when calculating the successor of a value.
fn incrementor() -> Self;
}
Now, we can modify our successor()
function:
fn successor<T>(argument: T)
where T: Add,
T: Incrementable
{
argument + T::incrementor()
}
Generic Structs and Enums
Just like functions, structs and enums can be generic. Let's take a look at the standard library version of Option<T>
:
enum Option<T> {
Some: T,
None: (),
}
Just like an unconstrained generic function, this type exists for all (∀) types T
. Result<T, E>
is another example:
enum Result<T, E> {
Ok: T,
Err: E,
}
Both generic enums and generic structs can be trait constrained, as well. Consider this struct:
struct Foo<T>
where T: Add
{
field_one: T,
}
Type Arguments
Similar to Rust, Sway has what is colloquially known as the turbofish. The turbofish looks like this: ::<>
(see the little fish with bubbles behind it?). The turbofish is used to annotate types in a generic context. Say you have the following function:
fn foo<T, E>(t: T) -> Result<T, E> {
Result::Ok(t)
}
In this code example, which is admittedly asinine, you can't possibly know what type E
is. You'd need to provide the type manually, with a turbofish:
fn foo<T, E>(t: T) -> Result<T, E> {
Result::Ok::<T, MyErrorType>(t)
}
It is also common to see the turbofish used on the function itself:
fn main() {
foo::<Bar, Baz>()
}
Traits
Declaring a Trait
A trait opts a type into a certain type of behavior or functionality that can be shared among types. This allows for easy reuse of code and generic programming. If you have ever used a typeclass in Haskell, a trait in Rust, or even an interface in Java, these are similar concepts.
Let's take a look at some code:
trait Compare {
fn equals(self, b: Self) -> bool;
} {
fn not_equals(self, b: Self) -> bool {
!self.equals(b)
}
}
We have just declared a trait called Compare
. After the name of the trait, there are two blocks of code (a block is code enclosed in {
curly brackets }
). The first block is the interface surface. The second block is the methods provided by the trait. If a type can provide the methods in the interface surface, then it gets access to the methods in the trait for free! What the above trait is saying is: if you can determine if two values are equal, then for free, you can determine that they are not equal. Note that trait methods have access to the methods defined in the interface surface.
Implementing a Trait
Ok, so I know that numbers can be equal. I want to implement my Compare
trait for u64
. Let's take a look at how that is done:
impl Compare for u64 {
fn equals(self, b: Self) -> bool {
self == b
}
}
The above snippet declares all of the methods in the trait Compare
for the type u64
. Now, we have access to both the equals
and not_equals
methods for u64
, as long as the trait Compare
is in scope.
Supertraits
When using multiple traits, scenarios often come up where one trait may require functionality from another trait. This is where supertraits come in as they allow you to require a trait when implementing another
trait (ie. a trait with a trait). A good example of this is the Ord
trait of the core
library of Sway. The Ord
trait requires the Eq
trait, so Eq
is kept as a separate trait as one may decide to implement Eq
without implementing other parts of the Ord
trait.
trait Eq {
fn equals(self, b: Self) -> bool;
}
trait Ord: Eq {
fn gte(self, b: Self) -> bool;
}
impl Ord for u64 {
fn gte(self, b: Self) -> bool {
// As `Eq` is a supertrait of `Ord`, `Ord` can access the equals method
self.equals(b) || self.gt(b)
}
}
To require a supertrait, add a :
after the trait name and then list the traits you would like to require and separate them with a +
.
Use Cases
Custom Types (structs, enums)
Often, libraries and APIs have interfaces that are abstracted over a type that implements a certain trait. It is up to the consumer of the interface to implement that trait for the type they wish to use with the interface. For example, let's take a look at a trait and an interface built off of it.
library games;
pub enum Suit {
Hearts: (),
Diamonds: (),
Clubs: (),
Spades: (),
}
pub trait Card {
fn suit(self) -> Suit;
fn value(self) -> u8;
}
fn play_game_with_deck<T>(a: Vec<T>) where T: Card {
// insert some creative card game here
}
Note Trait constraints (i.e. using the
where
keyword) have not yet been implemented
Now, if you want to use the function play_game_with_deck
with your struct, you must implement Card
for your struct. Note that the following code example assumes a dependency games has been included in the Forc.toml
file.
script;
use games::*;
struct MyCard {
suit: Suit,
value: u8
}
impl Card for MyCard {
fn suit(self) -> Suit {
self.suit
}
fn value(self) -> u8 {
self.value
}
}
fn main() {
let mut i = 52;
let mut deck: Vec<MyCard> = Vec::with_capacity(50);
while i > 0 {
i = i - 1;
deck.push(MyCard { suit: generate_random_suit(), value: i % 4}
}
play_game_with_deck(deck);
}
fn generate_random_suit() -> Suit {
[ ... ]
}
Inline Assembly in Sway
While many users will never have to touch assembly language while writing sway code, it is a powerful tool that enables many advanced use-cases (ie: optimizations, building libraries, etc).
ASM Block
In Sway, the way we use assembly inline is to declare an asm block like this:
asm() {...}
Declaring an asm
block is similar to declaring a function.
We can specify register names to operate on as arguments, we can perform operations within the block, and we can return a value.
Here's an example showing what this might look like:
pub fn add_1(num: u32) -> u32 {
asm(r1: num, r2) {
add r2 r1 one;
r2: u32
}
}
An asm
block can only return a single register. If you really need to return more than one value, you can modify a tuple. Here's an example showing how can implement this (u64, u64)
:
script;
fn adder(a: u64, b: u64, c: u64) -> (u64, u64) {
let empty_tuple = (0u64, 0u64);
asm(output: empty_tuple, r1: a, r2: b, r3: c, r4, r5) {
add r4 r1 r2; // add a & b and put the result in r4
add r5 r2 r3; // add b & c and put the result in r5
sw output r4 i0; // store the word in r4 in output + 0 words
sw output r5 i1; // store the word in r5 in output + 1 word
output: (u64, u64) // return both values
}
}
fn main() -> bool {
let (first, second) = adder(1, 2, 3);
assert(first == 3);
assert(second == 5);
true
}
Note that this is contrived example meant to demonstrate the syntax; there's absolutely no need to use assembly to add integers!
Note that in the above example:
- we initialized the register
r1
with the value ofnum
. - we declared a second register
r2
(you may choose any register names you want). - we use the
add
opcode to addone
to the value ofr1
and store it inr2
. one
is an example of a "reserved register", of which there are 16 in total. Further reading on this is linked below under "Semantics".- we return
r2
& specify the return type as being u32 (the return type is u64 by default).
An important note is that the ji
and jnei
opcodes are not available within an asm
block. For those looking to introduce control flow to asm
blocks, it is recommended to surround smaller chunks of asm
with control flow (if
, else
, and while
).
Helpful Links
For examples of assembly in action, check out the Sway standard library.
For a complete list of all instructions supported in the FuelVM: Instructions.
And to learn more about the FuelVM semantics: Semantics.
Common Collections
Sway’s standard library includes a number of very useful data structures called collections. Most other data types represent one specific value, but collections can contain multiple values. Unlike the built-in array and tuple types which are allocated on the "stack" and cannot grow in size, the data these collections point to is stored either on the "heap" or in contract "storage", which means the amount of data does not need to be known at compile time and can grow as the program runs. Each kind of collection has different capabilities and costs, and choosing an appropriate one for your current situation is a skill you’ll develop over time. In this chapter, we’ll discuss three collections that are used very often in Sway programs:
A vector on the heap allows you to store a variable number of values next to each other.
A storage vector is similar to a vector on the heap but uses persistent storage.
A storage map allows you to associate a value with a particular key.
We’ll discuss how to create and update vectors, storage vectors, and storage maps, as well as what makes each special.
Vectors on the Heap
The first collection type we’ll look at is Vec<T>
, also known as a vector. Vectors allow you to store more than one value in a single data structure that puts all the values next to each other in memory. Vectors can only store values of the same type. They are useful when you have a list of items, such as the lines of text in a file or the prices of items in a shopping cart.
Vec<T>
is included in the standard library prelude which means that there is no need to import it manually.
Creating a New Vector
To create a new empty vector, we call the Vec::new
function, as shown below:
let v: Vec<u64> = Vec::new();
Note that we added a type annotation here. Because we aren’t inserting any values into this vector, the Sway compiler doesn’t know what kind of elements we intend to store. Vectors are implemented using generics which means that the Vec<T>
type provided by the standard library can hold any type. When we create a vector to hold a specific type, we can specify the type within angle brackets. In the example above, we’ve told the Sway compiler that the Vec<T>
in v
will hold elements of the u64
type.
Updating a Vector
To create a vector and then add elements to it, we can use the push
method, as shown below:
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
As with any variable, if we want to be able to change its value, we need to make it mutable using the mut
keyword, as discussed in the section Declaring a Variable. The numbers we place inside are all of type u64
, and the Sway compiler infers this from the data, so we don’t need the Vec<u64>
annotation.
Reading Elements of Vectors
To read a value stored in a vector at a particular index, you can use the get
method as shown below:
let third = v.get(2);
match third {
Option::Some(third) => log(third),
Option::None => revert(42),
}
Note two details here. First, we use the index value of 2
to get the third element because vectors are indexed by number, starting at zero. Second, we get the third element by using the get
method with the index passed as an argument, which gives us an Option<T>
.
When the get
method is passed an index that is outside the vector, it returns None
without panicking. This is particularly useful if accessing an element beyond the range of the vector may happen occasionally under normal circumstances. Your code will then have logic to handle having either Some(element)
or None
. For example, the index could be coming as a contract method argument. If the argument passed is too large, the method get
will return a None
value, and the contract method may then decide to revert when that happens or return a meaningful error that tells the user how many items are in the current vector and give them another chance to pass a valid value.
Iterating over the Values in a Vector
To access each element in a vector in turn, we would iterate through all of the valid indices using a while
loop and the len
method as shown below:
let mut i = 0;
while i < v.len() {
log(v.get(i).unwrap());
i += 1;
}
Note two details here. First, we use the method len
which returns the length of the vector. Second, we call the method unwrap
to extract the Option
returned by get
. We know that unwrap
will not fail (i.e. will not cause a revert) because each index i
passed to get
is known to be smaller than the length of the vector.
Using an Enum to store Multiple Types
Vectors can only store values that are the same type. This can be inconvenient; there are definitely use cases for needing to store a list of items of different types. Fortunately, the variants of an enum are defined under the same enum type, so when we need one type to represent elements of different types, we can define and use an enum!
For example, say we want to get values from a row in a table in which some of the columns in the row contain integers, some b256
values, and some Booleans. We can define an enum whose variants will hold the different value types, and all the enum variants will be considered the same type: that of the enum. Then we can create a vector to hold that enum and so, ultimately, holds different types. We’ve demonstrated this below:
enum TableCell {
Int: u64,
B256: b256,
Boolean: bool,
}
let mut row = Vec::new();
row.push(TableCell::Int(3));
row.push(TableCell::B256(0x0101010101010101010101010101010101010101010101010101010101010101));
row.push(TableCell::Boolean(true));
Now that we’ve discussed some of the most common ways to use vectors, be sure to review the API documentation for all the many useful methods defined on Vec<T>
by the standard library. For now, these can be found in the source code for Vec<T>
. For example, in addition to push
, a pop
method removes and returns the last element, a remove
method removes and returns the element at some chosen index within the vector, an insert
method inserts an element at some chosen index within the vector, etc.
Storage Vectors
The second collection type we’ll look at is StorageVec<T>
. Just like vectors on the heap (i.e. Vec<T>
), storage vectors allow you to store more than one value in a single data structure where each value is assigned an index and can only store values of the same type. However, unlike Vec<T>
, the elements of a StorageVec
are stored in persistent storage, and consecutive elements are not necessarily stored in storage slots that have consecutive keys.
In order to use StorageVec<T>
, you must first import StorageVec
as follows:
use std::storage::StorageVec;
Another major difference between Vec<T>
and StorageVec<T>
is that StorageVec<T>
can only be used in a contract because only contracts are allowed to access persistent storage.
Creating a New Storage Vector
To create a new empty storage vector, we have to declare the vector in a storage
block as follows:
v: StorageVec<u64> = StorageVec {},
Just like any other storage variable, two things are required when declaring a StorageVec
: a type annotation and an initializer. The initializer is just an empty struct of type StorageVec
because StorageVec<T>
itself is an empty struct! Everything that is interesting about StorageVec<T>
is implemented in its methods.
Storage vectors, just like Vec<T>
, are implemented using generics which means that the StorageVec<T>
type provided by the standard library can hold any type. When we create a storage vector to hold a specific type, we can specify the type within angle brackets. In the example above, we’ve told the Sway compiler that the StorageVec<T>
in v
will hold elements of the u64
type.
Updating a Storage Vector
To add elements to a storage vector, we can use the push
method, as shown below:
#[storage(read, write)]
fn push_to_storage_vec() {
storage.v.push(5);
storage.v.push(6);
storage.v.push(7);
storage.v.push(8);
}
Note two details here. First, in order to use push
, we need to first access the vector using the storage
keyword. Second, because push
requires accessing storage, a storage
annotation is required on the ABI function that calls push
. While it may seem that #[storage(write)]
should be enough here, the read
annotation is also required because each call to push
requires reading (and then updating) the length of the storage vector which is also stored in persistent storage.
Note The storage annotation is also required for any private function defined in the contract that tries to push into the vector.
Note There is no need to add the
mut
keyword when declaring aStorageVec<T>
. All storage variables are mutable by default.
Reading Elements of Storage Vectors
To read a value stored in a vector at a particular index, you can use the get
method as shown below:
#[storage(read)]
fn read_from_storage_vec() {
let third = storage.v.get(2);
match third {
Option::Some(third) => log(third),
Option::None => revert(42),
}
}
Note three details here. First, we use the index value of 2
to get the third element because vectors are indexed by number, starting at zero. Second, we get the third element by using the get
method with the index passed as an argument, which gives us an Option<T>
. Third, the ABI function calling get
only requires the annotation #[storage(read)]
as one might expect because get
does not write to storage.
When the get
method is passed an index that is outside the vector, it returns None
without panicking. This is particularly useful if accessing an element beyond the range of the vector may happen occasionally under normal circumstances. Your code will then have logic to handle having either Some(element)
or None
. For example, the index could be coming as a contract method argument. If the argument passed is too large, the method get
will return a None
value, and the contract method may then decide to revert when that happens or return a meaningful error that tells the user how many items are in the current vector and give them another chance to pass a valid value.
Iterating over the Values in a Vector
To access each element in a vector in turn, we would iterate through all of the valid indices using a while
loop and the len
method as shown below:
#[storage(read)]
fn iterate_over_a_storage_vec() {
let mut i = 0;
while i < storage.v.len() {
log(storage.v.get(i).unwrap());
i += 1;
}
}
Again, this is quite similar to iterating over the elements of a Vec<T>
where we use the method len
to return the length of the vector. We also call the method unwrap
to extract the Option
returned by get
. We know that unwrap
will not fail (i.e. will not cause a revert) because each index i
passed to get
is known to be smaller than the length of the vector.
Using an Enum to store Multiple Types
Storage vectors, just like Vec<T>
, can only store values that are the same type. Similarly to what we did for Vec<T>
in the section Using an Enum to store Multiple Types, we can define an enum whose variants will hold the different value types, and all the enum variants will be considered the same type: that of the enum. This is shown below:
enum TableCell {
Int: u64,
B256: b256,
Boolean: bool,
}
Then we can declare a storage vector in a storage
block to hold that enum and so, ultimately, holds different types:
row: StorageVec<TableCell> = StorageVec {},
We can now push different enum variants to the storage vector as follows:
#[storage(read, write)]
fn push_to_multiple_types_storage_vec() {
storage.row.push(TableCell::Int(3));
storage.row.push(TableCell::B256(0x0101010101010101010101010101010101010101010101010101010101010101));
storage.row.push(TableCell::Boolean(true));
}
Now that we’ve discussed some of the most common ways to use storage vectors, be sure to review the API documentation for all the many useful methods defined on StorageVec<T>
by the standard library. For now, these can be found in the source code for StorageVec<T>
. For example, in addition to push
, a pop
method removes and returns the last element, a remove
method removes and returns the element at some chosen index within the vector, an insert
method inserts an element at some chosen index within the vector, etc.
Storage Maps
Another important common collection is the storage map. The type StorageMap<K, V>
from the standard library stores a mapping of keys of type K
to values of type V
using a hashing function, which determines how it places these keys and values into storage slots. This is similar to Rust's HashMap<K, V>
but with a few differences.
Storage maps are useful when you want to look up data not by using an index, as you can with vectors, but by using a key that can be of any type. For example, when building a ledger-based sub-currency smart contract, you could keep track of the balance of each wallet in a storage map in which each key is a wallet’s Address
and the values are each wallet’s balance. Given an Address
, you can retrieve its balance.
Similarly to StorageVec<T>
, StorageMap<K, V>
can only be used in a contract because only contracts are allowed to access persistent storage.
StorageMap<T>
is included in the standard library prelude which means that there is no need to import it manually.
Creating a New Storage Map
To create a new empty storage map, we have to declare the map in a storage
block as follows:
map: StorageMap<Address, u64> = StorageMap {},
Just like any other storage variable, two things are required when declaring a StorageMap
: a type annotation and an initializer. The initializer is just an empty struct of type StorageMap
because StorageMap<K, V>
itself is an empty struct! Everything that is interesting about StorageMap<K, V>
is implemented in its methods.
Storage maps, just like Vec<T>
and StorageVec<T>
, are implemented using generics which means that the StorageMap<K, V>
type provided by the standard library can map keys of any type K
to values of any type V
. In the example above, we’ve told the Sway compiler that the StorageMap<K, V>
in map
will map keys of type Address
to values of type u64
.
Updating a Storage Map
To insert key-value pairs into a storage map, we can use the insert
method, as shown below:
#[storage(write)]
fn insert_into_storage_map() {
let addr1 = Address::from(0x0101010101010101010101010101010101010101010101010101010101010101);
let addr2 = Address::from(0x0202020202020202020202020202020202020202020202020202020202020202);
storage.map.insert(addr1, 42);
storage.map.insert(addr2, 77);
}
Note two details here. First, in order to use insert
, we need to first access the storage map using the storage
keyword. Second, because insert
requires writing into storage, a #[storage(write)]
annotation is required on the ABI function that calls insert
.
Note The storage annotation is also required for any private function defined in the contract that tries to insert into the map.
Note There is no need to add the
mut
keyword when declaring aStorageMap<K, V>
. All storage variables are mutable by default.
Accessing Values in a Storage Map
We can get a value out of the storage map by providing its key
to the get
method, as shown below:
#[storage(read, write)]
fn get_from_storage_map() {
let addr1 = Address::from(0x0101010101010101010101010101010101010101010101010101010101010101);
let addr2 = Address::from(0x0202020202020202020202020202020202020202020202020202020202020202);
storage.map.insert(addr1, 42);
storage.map.insert(addr2, 77);
let value1 = storage.map.get(addr1);
}
Here, value1
will have the value that's associated with the first address, and the result will be 42
. You might expect get
to return an Option<V>
where the return value would be None
if the value does not exist. However, that is not case for StorageMap
. In fact, storage maps have no way of knowing whether insert
has been called with a given key or not as it would be too expensive to keep track of that information. Instead, a default value whose byte-representation is all zeros is returned if get
is called with a key that has no value in the map. Note that each type interprets that default value differently:
- The default value for a
bool
isfalse
. - The default value for a integers is
0
. - The default value for a
b256
is0x0000000000000000000000000000000000000000000000000000000000000000
. - The default value for a
str[n]
is a string ofNull
characters. - The default value for a tuple is a tuple of the default values of its components.
- The default value for a struct is a struct of the default values of its components.
- The default value for an enum is an instance of its first variant containing the default for its associated value.
Storage maps with multiple keys
You might find yourself needing a StorageMap<K1, V1>
where the type V1
is itself another StorageMap<K2, V2>
. This is not allowed in Sway. The right approach is to use a single StorageMap<K, V>
where K
is a tuple (K1, K2)
. For example:
map_two_keys: StorageMap<(b256, bool), b256> = StorageMap {},
Limitations
It is not currently allowed to have a StorageMap<K, V>
as a component of a complex type such as a struct or an enum. For example, the code below is not legal:
Struct Wrapper {
map1: StorageMap<u64, u64>,
map2: StorageMap<u64, u64>,
}
storage {
w: Wrapper
}
...
storage.w.map1.insert(..);
Testing
Sway aims to provide facilities for both unit testing and integration testing.
Unit testing refers to "in-language" testing which can be triggered via the
forc test
command. Sway unit testing is currently a high-priority
work-in-progress, you can follow along at this
issue.
Integration testing refers to the testing of your Sway project's integration within some wider application. You can add integration testing to your Sway+Rust projects today using the cargo generate template and Rust SDK.
Testing with Rust
A common use of Sway is for writing contracts or scripts that exist as part of a wider Rust application. In order to test the interaction between our Sway code and our Rust code we can add integration testing.
Adding Rust Integration Testing
To add Rust integration testing to a Forc project we can use the sway-test-rs
cargo generate
template.
This template makes it easy for Sway devs to add the boilerplate required when
setting up their Rust integration testing.
Let's add a Rust integration test to the fresh project we created in the introduction.
1. Enter the project
To recap, here's what our empty project looks like:
$ cd my-fuel-project
$ tree .
├── Forc.toml
└── src
└── main.sw
2. Install cargo generate
We're going to add a Rust integration test harness using a cargo generate
template. Let's make sure we have the cargo generate
command installed!
cargo install cargo-generate
Note: You can learn more about cargo generate by visiting its repository.
3. Generate the test harness
Let's generate the default test harness with the following:
cargo generate --init fuellabs/sway templates/sway-test-rs --name my-fuel-project --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.
If all goes well, the output should look as follows:
⚠️ Favorite `fuellabs/sway` not found in config, using it as a git repository: https://github.com/fuellabs/sway
🤷 Project Name : my-fuel-project
🔧 Destination: /home/user/path/to/my-fuel-project ...
🔧 Generating template ...
[1/3] Done: Cargo.toml
[2/3] Done: tests/harness.rs
[3/3] Done: tests
🔧 Moving generated files into: `/home/user/path/to/my-fuel-project`...
✨ Done! New project created /home/user/path/to/my-fuel-project
Let's have a look at the result:
$ tree .
├── Cargo.toml
├── Forc.toml
├── src
│ └── main.sw
└── tests
└── harness.rs
We have two new files!
- The
Cargo.toml
is the manifest for our new test harness and specifies the required dependencies includingfuels
the Fuel Rust SDK. - The
tests/harness.rs
contains some boilerplate test code to get us started, though doesn't call any contract methods just yet.
4. Build the forc project
Before running the tests, we need to build our contract so that the necessary
ABI, storage and bytecode artifacts are available. We can do so with forc build
:
$ forc build
Creating a new `Forc.lock` file. (Cause: lock file did not exist)
Adding core
Adding std git+https://github.com/fuellabs/sway?tag=v0.24.5#e695606d8884a18664f6231681333a784e623bc9
Created new lock file at /home/user/path/to/my-fuel-project/Forc.lock
Compiled library "core".
Compiled library "std".
Compiled contract "my-fuel-project".
Bytecode size is 60 bytes.
At this point, our project should look like the following:
$ tree
├── Cargo.toml
├── Forc.lock
├── Forc.toml
├── out
│ └── debug
│ ├── my-fuel-project-abi.json
│ ├── my-fuel-project.bin
│ └── my-fuel-project-storage_slots.json
├── src
│ └── main.sw
└── tests
└── harness.rs
We now have an out
directory with our required JSON files!
Note: This step may no longer be required in the future as we plan to enable the integration testing to automatically build the artifacts as necessary so that files like the ABI JSON are always up to date.
5. Build and run the tests
Now we're ready to build and run the default integration test.
$ cargo test
Updating crates.io index
Compiling version_check v0.9.4
Compiling proc-macro2 v1.0.46
Compiling quote v1.0.21
...
Compiling fuels v0.24.0
Compiling my-fuel-project v0.1.0 (/home/user/path/to/my-fuel-project)
Finished test [unoptimized + debuginfo] target(s) in 1m 03s
Running tests/harness.rs (target/debug/deps/integration_tests-373971ac377845f7)
running 1 test
test can_get_contract_id ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.36s
Note: The first time we run
cargo test
, cargo will spend some time fetching and building the dependencies for Fuel's Rust SDK. This might take a while, but only the first time!
If all went well, we should see some output that looks like the above!
Writing Tests
Now that we've learned how to setup Rust integration testing in our project, let's try to write some of our own tests!
First, let's update our contract code with a simple counter example:
contract;
abi TestContract {
#[storage(write)]
fn initialize_counter(value: u64) -> u64;
#[storage(read, write)]
fn increment_counter(amount: u64) -> u64;
}
storage {
counter: u64 = 0,
}
impl TestContract for Contract {
#[storage(write)]
fn initialize_counter(value: u64) -> u64 {
storage.counter = value;
value
}
#[storage(read, write)]
fn increment_counter(amount: u64) -> u64 {
let incremented = storage.counter + amount;
storage.counter = incremented;
incremented
}
}
To test our initialize_counter
and increment_counter
contract methods from
the Rust test harness, we could update our tests/harness.rs
file with the
following:
use fuels::{prelude::*, tx::ContractId};
// Load abi from json
abigen!(TestContract, "out/debug/my-fuel-project-abi.json");
async fn get_contract_instance() -> (TestContract, ContractId) {
// Launch a local network and deploy the contract
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(
Some(1), /* Single wallet */
Some(1), /* Single coin (UTXO) */
Some(1_000_000_000), /* Amount per coin */
),
None,
)
.await;
let wallet = wallets.pop().unwrap();
let id = Contract::deploy(
"./out/debug/my-fuel-project.bin",
&wallet,
TxParameters::default(),
StorageConfiguration::with_storage_path(Some(
"./out/debug/my-fuel-project-storage_slots.json".to_string(),
)),
)
.await
.unwrap();
let instance = TestContract::new(id.to_string(), wallet);
(instance, id.into())
}
#[tokio::test]
async fn initialize_and_increment() {
let (contract_instance, _id) = get_contract_instance().await;
// Now you have an instance of your contract you can use to test each function
let result = contract_instance
.methods()
.initialize_counter(42)
.call()
.await
.unwrap();
assert_eq!(42, result.value);
// Call `increment_counter()` method in our deployed contract.
let result = contract_instance
.methods()
.increment_counter(10)
.call()
.await
.unwrap();
assert_eq!(52, result.value);
}
Let's build our project once more and run the test:
forc build
$ cargo test
Compiling my-fuel-project v0.1.0 (/home/mindtree/programming/sway/my-fuel-project)
Finished test [unoptimized + debuginfo] target(s) in 11.61s
Running tests/harness.rs (target/debug/deps/integration_tests-373971ac377845f7)
running 1 test
test initialize_and_increment ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.25s
When cargo runs our test, our test uses the SDK to spin up a local in-memory Fuel network, deploy our contract to it, and call the contract methods via the ABI.
You can add as many functions decorated with #[tokio::test]
as you like, and
cargo test
will automatically test each of them!
Application Frontend Development
TypeScript SDK
The TypeScript SDK supports common tasks like:
- Deploying and calling contracts
- Generating contract types with TypeChain
- Building and sending transactions
- Encoding and decoding contract ABI
Refer here for full documentation.
Sway Reference
- Style Guide
- Known Issues and Workarounds
- Differences from Rust
- Differences from Solidity
- Contributing to Sway
Style Guide
Capitalization
In Sway, structs, traits, and enums are CapitalCase
. Modules, variables, and functions are snake_case
, constants are SCREAMING_SNAKE_CASE
. The compiler will warn you if your capitalization is ever unidiomatic.
Known Issues and Workarounds
Known Issues
- #870: All
impl
blocks need to be defined before any of the functions they define can be called. This includes sibling functions in the sameimpl
declaration, i.e., functions in animpl
can't call each other yet.
Missing Features
-
#1182 Arrays in a
storage
block are not yet supported. See the Manual Storage Management section for details on how to usestore
andget
from the standard library to manage storage slots directly. Note, however, thatStorageMap<K, V>
does support arbitrary types forK
andV
without any limitations. -
#428: Arrays are currently immutable which means that changing elements of an array once initialized is not yet possible.
-
#1796: It is not yet allowed to use
StorageMap<K, V>
as a component of a complex type such as a struct or an enum. -
#2647: Currently, it is only possible to define configuration-time constants that have primitive types and that are initialized using literals.
General
- No compiler optimization passes have been implemented yet, therefore bytecode will be more expensive and larger than it would be in production. Note that eventually the optimizer will support zero-cost abstractions, avoiding the need for developers to go down to inline assembly to produce optimal code.
Differences From Solidity
This page outlines some of the critical differences between Sway and Solidity, and between the FuelVM and the EVM.
Underlying Virtual Machine
The underlying virtual machine targeted by Sway is the FuelVM, specified here. Solidity targets the Ethereum Virtual Machine (EVM), specified here.
Word Size
Words in the FuelVM are 64 bits (8 bytes), rather than the 256 bits (32 bytes) of the EVM. Therefore, primitive integers only go up to u64
, and hashes (the b256
type) are not in registers but rather in memory. A b256
is therefore a pointer to a 32-byte memory region containing the hash value.
Unsigned Integers Only
Only unsigned integers are provided as primitives: u8
, u16
, u32
, and u64
. Signed integer arithmetic is not available in the FuelVM. Signed integers and signed integer arithmetic can be implemented in high-level libraries if needed.
Global Revert
Panics in the FuelVM (called "reverts" in Solidity and the EVM) are global, i.e. they cannot be caught. A panic will completely and unconditionally revert the stateful effects of a transaction, minus gas used.
Default Safe Math
Math in the FuelVM is by default safe (i.e. any overflow or exception is a panic). Safety checks are performed natively in the VM implementation, rather than at the bytecode level like Solidity's default safe math.
No* Code Size Limit
There is no practical code size limit to Sway contracts. The physical limit is governed by the VM_MAX_RAM
VM parameter, which at the time of writing is 64 MiB.
Differences From Rust
Sway shares a lot with Rust, especially its syntax. Because they are so similar, you may be surprised or caught off guard when they differ. This page serves to outline, from a high level, some of the syntactic gotchas that you may encounter.
Enum Variant Syntax
In Rust, enums generally take one of three forms: unit variants, which have no inner data, struct variants, which contain named fields, and tuple variants, which contain within them a tuple of data. If you are unfamiliar with these terms, this is what they look like:
// note to those skimming the docs: this is Rust syntax! Not Sway! Don't copy/paste this into a Sway program.
enum Foo {
UnitVariant,
TupleVariant(u32, u64, bool),
StructVariant {
field_one: bool,
field_two: bool
}
}
In Sway, enums are simplified. Enums variants must all specify exactly one type. This type represents their interior data. This is actually isomorphic to what Rust offers, just with a different syntax. I'll now rewrite the above enum but with Sway syntax:
// This is equivalent Sway syntax for the above Rust enum.
enum Foo {
UnitVariant: (),
TupleVariant: (u32, u64, bool),
StructVariant: MyStruct,
}
struct MyStruct {
field_one: bool,
field_two: bool,
}
Contributing To Sway
Thanks for your interest in contributing to Sway! This document outlines the process for installing and setting up the Sway toolchain for development, as well as some conventions on contributing to Sway.
If you run into any difficulties getting started, you can always ask questions on our Discord.
Building and setting up a development workspace
See the introduction section for instructions on installing and setting up the Sway toolchain.
Getting the repository
- Visit the Sway repo and fork the project.
- Then clone your forked copy to your local machine and get to work.
git clone https://github.com/FuelLabs/sway
cd sway
Building and testing
The following steps will run the sway test suite and ensure that everything is set up correctly.
First, open a new terminal and start fuel-core
with:
fuel-core
Then open a second terminal, cd into the sway
repo and run:
cargo run --bin test
After the test suite runs, you should see:
Tests passed.
_n_ tests run (0 skipped)
Congratulations! You've now got everything setup and are ready to start making contributions.
Finding something to work on
There are many ways in which you may contribute to the Sway project, some of which involve coding knowledge and some which do not. A few examples include:
- Reporting bugs
- Adding documentation to the Sway book
- Adding new features or bugfixes for which there is already an open issue
- Making feature requests
Check out our Help Wanted, Sway Book or Good First Issue issues to find a suitable task.
If you are planning something big, for example, related to multiple components or changes current behaviors, make sure to open an issue to discuss with us before starting on the implementation.
Contribution flow
This is a rough outline of what a contributor's workflow looks like:
- Make sure what you want to contribute is already tracked as an issue.
- We may discuss the problem and solution in the issue.
- Create a Git branch from where you want to base your work. This is usually master.
- Write code, add test cases, and commit your work.
- Run tests and make sure all tests pass.
- If the PR contains any breaking changes, add the breaking label to your PR.
- Push your changes to a branch in your fork of the repository and submit a pull request.
- Make sure mention the issue, which is created at step 1, in the commit message.
- Your PR will be reviewed and some changes may be requested.
- Once you've made changes, your PR must be re-reviewed and approved.
- If the PR becomes out of date, you can use GitHub's 'update branch' button.
- If there are conflicts, you can merge and resolve them locally. Then push to your PR branch. Any changes to the branch will require a re-review.
- Our CI system (Github Actions) automatically tests all authorized pull requests.
- Use Github to merge the PR once approved.
Thanks for your contributions!
Linking issues
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 separate by a comma, like:
close #123, ref #456
Forc Reference
Forc stands for Fuel Orchestrator. Forc provides a variety of tools and commands for developers working with the Fuel ecosystem, such as scaffolding a new project, formatting, running scripts, deploying contracts, testing contracts, and more. If you're coming from a Rust background, forc is similar to cargo.
If you are new to Forc, see the Forc Project introduction section.
For a comprehensive overview of the Forc CLI commands, see the Commands section.
Manifest Reference
The Forc.toml
(the manifest file) is a compulsory file for each package and it is written in [TOML] format. Forc.toml
consists of the following fields:
-
[project]
— Defines a sway project.name
— The name of the project.authors
— The authors of the project.organization
— The organization of the project.license
— The project license.entry
— The entry point for the compiler to start parsing from.- For the recomended way of selecting an entry point of large libraries please take a look at: Libraries
implicit-std
- Controls whether providedstd
version (with the currentforc
version) will get added as a dependency implicitly. Unless you know what you are doing, leave this as default.forc-version
- The minimum forc version required for this project to work properly.
-
[dependencies]
— Defines the dependencies. -
[network]
— Defines a network for forc to interact with.url
— URL of the network.
-
[build-profiles]
- Defines the build profiles. -
[patch]
- Defines the patches. -
[contract-dependencies]
- Defines the contract dependencies.
The [project]
section
An example Forc.toml
is shown below. Under [project]
the following fields are optional:
authors
organization
Also for the following fields, a default value is provided so omitting them is allowed:
entry
- (default : main.sw)implicit-std
- (default : true)
[project]
authors = ["user"]
entry = "main.sw"
organization = "Fuel_Labs"
license = "Apache-2.0"
name = "wallet_contract"
The [dependencies]
section
The following fields can be provided with a dependency:
version
- Desired version of the dependencypath
- The path of the dependency (if it is local)git
- The URL of the git repo hosting the dependencybranch
- The desired branch to fetch from the git repotag
- The desired tag to fetch from the git reporev
- The desired rev (i.e. commit hash) reference
Please see dependencies for details
The [network]
section
For the following fields, a default value is provided so omitting them is allowed:
URL
- (default: http://127.0.0.1:4000)
The [build-profiles-*]
section
The [build-profiles]
tables provide a way to customize compiler settings such as debug options.
The following fields needs to be provided for a build-profile:
print-ast
- Whether to print out the generated AST (true) or not (false).print-finalized-asm
- Whether to compile to bytecode (false) or to print out the generated ASM (true).print-intermediate-asm
- Whether to compile to bytecode (false) or to print out the generated ASM (true).print-ir
- Whether to compile to bytecode (false) or to print out the generated IR (true).terse-mode
- Terse mode. Limited warning and error output.
There are two default [build-profile]
available with every manifest file. These are debug
and release
profiles. If you want to override these profiles, you can provide them explicitly in the manifest file like the following example:
[project]
authors = ["user"]
entry = "main.sw"
organization = "Fuel_Labs"
license = "Apache-2.0"
name = "wallet_contract"
[build-profiles.debug]
print-finalized-asm = false
print-intermediate-asm = false
print-ir = false
terse = false
[build-profiles.release]
print-finalized-asm = false
print-intermediate-asm = false
print-ir = false
terse = true
Since release
and debug
implicitly included in every manifest file, you can use them by just passing --release
or by not passing anything (debug is default). For using a user defined build profile there is --build-profile <profile name>
option available to the relevant commands. (For an example see forc-build)
Note that providing the corresponding cli options (like --print-finalized-asm
) will override the selected build profile. For example if you pass both --release
and --print-finalized-asm
, release build profile is omitted and resulting build profile would have a structure like the following:
- print-finalized-asm - true
- print-intermediate-asm - false
- print-ir - false
- terse - false
The [patch]
section
The [patch] section of Forc.toml
can be used to override dependencies with other copies. The example provided below patches https://github.com/fuellabs/sway source with master branch of the same repo.
[project]
authors = ["user"]
entry = "main.sw"
organization = "Fuel_Labs"
license = "Apache-2.0"
name = "wallet_contract"
[dependencies]
[patch.'https://github.com/fuellabs/sway']
std = { git = "https://github.com/fuellabs/sway", branch = "test" }
In the example above, std
is patched with the test
branch from std
repo. You can also patch git dependencies with dependencies defined with a path.
[patch.'https://github.com/fuellabs/sway']
std = { path = "/path/to/local_std_version" }
Just like std
or core
you can also patch dependencies you declared with a git repo.
[project]
authors = ["user"]
entry = "main.sw"
organization = "Fuel_Labs"
license = "Apache-2.0"
name = "wallet_contract"
[dependencies]
foo = { git = "https://github.com/foo/foo", branch = "master" }
[patch.'https://github.com/foo']
foo = { git = "https://github.com/foo/foo", branch = "test" }
Note that each key after the [patch]
is a URL of the source that is being patched.
The [contract-dependencies]
section
The [contract-dependenices]
table can be used to declare contract dependencies for a Sway contract or script. Contract dependencies are the set of contracts that our contract or script may interact with. Declaring [contract-dependencies]
makes it easier to refer to contracts in your Sway source code without having to manually update IDs each time a new version is deployed. Instead, we can use forc to pin and update contract dependencies just like we do for regular library dependencies.
Contracts declared under [contract-dependencies]
are built and pinned just like regular [dependencies]
however rather than importing each contract dependency's entire public namespace we instead import their respective contract IDs as CONTRACT_ID
constants available via each contract dependency's namespace root. This means you can use a contract dependency's ID as if it were declared as a pub const
in the root of the contract dependency package as demonstrated in the example below.
Entries under [contract-dependencies]
can be declared in the same way that [dependencies]
can be declared. That is, they can refer to the path
or git
source of another contract. Note that entries under [contract-dependencies]
must refer to contracts and will otherwise produce an error.
Example Forc.toml
:
[project]
authors = ["user"]
entry = "main.sw"
organization = "Fuel_Labs"
license = "Apache-2.0"
name = "wallet_contract"
[contract-dependencies]
foo = { path = "../foo" }
Example usage:
script;
fn main() {
let foo_id = foo::CONTRACT_ID;
}
Dependencies
Forc has a dependency management system which can pull packages using git. This allows users to build and share Forc libraries.
Adding a dependency
If your Forc.toml
doesn't already have a [dependencies]
table, add one. Below, list the package name alongside its source. Currently, forc
supports both git
and path
sources.
If a git
source is specified, forc
will fetch the git repository at the given URL and then search for a Forc.toml
for a package with the given name anywhere inside the git repository.
The following example adds a library dependency named custom_lib
. For git dependencies you may optionally specify a branch
, tag
, or rev
(i.e. commit hash) reference.
[dependencies]
custom_lib = { git = "https://github.com/FuelLabs/custom_lib", branch = "master" }
# custom_lib = { git = "https://github.com/FuelLabs/custom_lib", tag = "v0.0.1" }
# custom_lib = { git = "https://github.com/FuelLabs/custom_lib", rev = "87f80bdf323e2d64e213895d0a639ad468f4deff" }
Depending on a local library using path
:
[dependencies]
custom_lib = { path = "../custom_lib" }
Once the package is added, running forc build
will automatically download added dependencies.
Updating dependencies
To update dependencies in your Forc directory you can run forc update
. For path
dependencies this will have no effect. For git
dependencies with a branch
reference, this will update the project to use the latest commit for the given branch.
Here are a list of commands available to forc:
- forc addr2line
- forc build
- forc check
- forc clean
- forc completions
- forc init
- forc new
- forc parse-bytecode
- forc plugins
- forc test
- forc update
- forc template
forc-addr2line
Show location and context of an opcode address in its source file
USAGE:
forc addr2line [OPTIONS] --sourcemap-path <SOURCEMAP_PATH> --opcode-index <OPCODE_INDEX>
OPTIONS:
-c
, --context
<CONTEXT>
How many lines of context to show [default: 2]
-g
, --sourcemap-path
<SOURCEMAP_PATH>
Source file mapping in JSON format
-h
, --help
Print help information
-i
, --opcode-index
<OPCODE_INDEX>
Opcode index
-L
, --log-level
<LOG_LEVEL>
Set the log level
-s
, --silent
Silence all output
-S
, --search-dir
<SEARCH_DIR>
Where to search for the project root [default: .]
-v
, --verbose
Use verbose output
forc-build
Compile the current or target project.
The output produced will depend on the project's program type.
-
script
,predicate
andcontract
projects will produce their bytecode in binary format<project-name>.bin
. -
script
projects will also produce a file containing the hash of the bytecode binary<project-name>-bin-hash
(usingfuel_cypto::Hasher
). -
predicate
projects will also produce a file containing the root hash of the bytecode binary<project-name>-bin-root
(usingfuel_tx::Contract::root_from_code
). -
contract
andlibrary
projects will also produce the public ABI in JSON format<project-name>-abi.json
.
USAGE:
forc build [OPTIONS]
OPTIONS:
--build-profile
<BUILD_PROFILE>
Name of the build profile to use. If it is not specified, forc will use debug build profile
-g
, --debug-outfile
<DEBUG_OUTFILE>
If set, outputs source file mapping in JSON format
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
--locked
Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it needs to be updated, Forc will exit with an error
--minify-json-abi
By default the JSON for ABIs is formatted for human readability. By using this option JSON output will be "minified", i.e. all on one line without whitespace
--minify-json-storage-slots
By default the JSON for initial storage slots is formatted for human readability. By using this option JSON output will be "minified", i.e. all on one line without whitespace
-o
<BINARY_OUTFILE>
If set, outputs a binary file representing the script bytes
--offline
Offline mode, prevents Forc from using the network when managing dependencies. Meaning it will only try to use previously downloaded dependencies
--output-directory
<OUTPUT_DIRECTORY>
The directory in which the sway compiler output artifacts are placed.
By default, this is <project-root>/out
.
-p
, --path
<PATH>
Path to the project, if not specified, current working directory will be used
--print-ast
Print the generated Sway AST (Abstract Syntax Tree)
--print-finalized-asm
Print the finalized ASM.
This is the state of the ASM with registers allocated and optimisations applied.
--print-intermediate-asm
Print the generated ASM.
This is the state of the ASM prior to performing register allocation and other ASM optimisations.
--print-ir
Print the generated Sway IR (Intermediate Representation)
--release
Use release build plan. If a custom release plan is not specified, it is implicitly added to the manifest file.
If --build-profile is also provided, forc omits this flag and uses provided build-profile.
-s
, --silent
Silence all output
-t
, --terse
Terse mode. Limited warning and error output
--tests
Also build all tests within the project
--time-phases
Output the time elapsed over each part of the compilation process
-v
, --verbose
Use verbose output
EXAMPLE
Compile the sway files of the current project.
$ forc build
Compiled script "my-fuel-project".
Bytecode size is 28 bytes.
The output produced will depend on the project's program type. Building script, predicate and contract projects will produce their bytecode in binary format <project-name>.bin
. Building contracts and libraries will also produce the public ABI in JSON format <project-name>-abi.json
.
By default, these artifacts are placed in the out/
directory.
If a Forc.lock
file did not yet exist, it will be created in order to pin each of the dependencies listed in Forc.toml
to a specific commit or version.
forc-check
Check the current or target project and all of its dependencies for errors.
This will essentially compile the packages without performing the final step of code generation, which is faster than running forc build.
USAGE:
forc check [OPTIONS]
OPTIONS:
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
--locked
Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it needs to be updated, Forc will exit with an error
--offline
Offline mode, prevents Forc from using the network when managing dependencies. Meaning it will only try to use previously downloaded dependencies
-p
, --path
<PATH>
Path to the project, if not specified, current working directory will be used
-s
, --silent
Silence all output
-t
, --terse
Terse mode. Limited warning and error output
-v
, --verbose
Use verbose output
forc-clean
Removes the default forc compiler output artifact directory, i.e. <project-name>/out
USAGE:
forc clean [OPTIONS]
OPTIONS:
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
-p
, --path
<PATH>
Path to the project, if not specified, current working directory will be used
-s
, --silent
Silence all output
-v
, --verbose
Use verbose output
forc-completions
Generate tab-completion scripts for your shell
USAGE:
forc completions [OPTIONS] --shell
OPTIONS:
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
-s
, --silent
Silence all output
-S
, --shell
<SHELL>
Specify shell to enable tab-completion for
[possible values: zsh, bash, fish, powershell, elvish]
For more info: https://fuellabs.github.io/sway/latest/forc/commands/forc_completions.html
-v
, --verbose
Use verbose output
DISCUSSION
Enable tab completion for Bash, Fish, Zsh, or PowerShell
The script is output on stdout
, allowing one to re-direct the
output to the file of their choosing. Where you place the file
will depend on which shell, and which operating system you are
using. Your particular configuration may also determine where
these scripts need to be placed.
Here are some common set ups for the three supported shells under Unix and similar operating systems (such as GNU/Linux).
BASH
Completion files are commonly stored in /etc/bash_completion.d/
for
system-wide commands, but can be stored in
~/.local/share/bash-completion/completions
for user-specific commands.
Run the command:
mkdir -p ~/.local/share/bash-completion/completions
forc completions --shell=bash >> ~/.local/share/bash-completion/completions/forc
This installs the completion script. You may have to log out and log back in to your shell session for the changes to take effect.
BASH (macOS/Homebrew)
Homebrew stores bash completion files within the Homebrew directory.
With the bash-completion
brew formula installed, run the command:
mkdir -p $(brew --prefix)/etc/bash_completion.d
forc completions --shell=bash > $(brew --prefix)/etc/bash_completion.d/forc.bash-completion
FISH
Fish completion files are commonly stored in
$HOME/.config/fish/completions
. Run the command:
mkdir -p ~/.config/fish/completions
forc completions --shell=fish > ~/.config/fish/completions/forc.fish
This installs the completion script. You may have to log out and log back in to your shell session for the changes to take effect.
ZSH
ZSH completions are commonly stored in any directory listed in
your $fpath
variable. To use these completions, you must either
add the generated script to one of those directories, or add your
own to this list.
Adding a custom directory is often the safest bet if you are
unsure of which directory to use. First create the directory; for
this example we'll create a hidden directory inside our $HOME
directory:
mkdir ~/.zfunc
Then add the following lines to your .zshrc
just before
compinit
:
fpath+=~/.zfunc
Now you can install the completions script using the following command:
forc completions --shell=zsh > ~/.zfunc/_forc
You must then either log out and log back in, or simply run
exec zsh
for the new completions to take effect.
CUSTOM LOCATIONS
Alternatively, you could save these files to the place of your
choosing, such as a custom directory inside your $HOME. Doing so
will require you to add the proper directives, such as source
ing
inside your login script. Consult your shells documentation for
how to add such directives.
POWERSHELL
The powershell completion scripts require PowerShell v5.0+ (which comes with Windows 10, but can be downloaded separately for windows 7 or 8.1).
First, check if a profile has already been set
Test-Path $profile
If the above command returns False
run the following
New-Item -path $profile -type file -force
Now open the file provided by $profile
(if you used the
New-Item
command it will be
${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Next, we either save the completions file into our profile, or into a separate file and source it inside our profile. To save the completions into our profile simply use
forc completions --shell=powershell >> ${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
forc-init
Create a new Forc project in an existing directory
USAGE:
forc init [OPTIONS]
OPTIONS:
--contract
The default program type, excluding all flags or adding this flag creates a basic contract program
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
--library
Create a package with a library target (src/lib.sw)
--name
<NAME>
Set the package name. Defaults to the directory name
--path
<PATH>
The directory in which the forc project will be initialized
--predicate
Create a package with a predicate target (src/predicate.rs)
-s
, --silent
Silence all output
--script
Create a package with a script target (src/main.sw)
-v
, --verbose
Use verbose output
EXAMPLE
$ mkdir my-fuel-project
$ cd my-fuel-project
$ forc init
$ tree
.
├── Forc.toml
└── src
└── main.sw
Forc.toml
is the Forc manifest file, containing information about the project and dependencies.
A src/
directory is created, with a single main.sw
Sway file in it.
forc-new
Create a new Forc project at <path>
USAGE:
forc new [OPTIONS]
ARGS:
<PATH>
The path at which the project directory will be created
OPTIONS:
--contract
The default program type. Excluding all flags or adding this flag creates a basic contract program
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
--library
Adding this flag creates an empty library program
--name
<NAME>
Set the package name. Defaults to the directory name
--predicate
Adding this flag creates an empty predicate program
-s
, --silent
Silence all output
--script
Adding this flag creates an empty script program
-v
, --verbose
Use verbose output
EXAMPLE
$ forc new my-fuel-project
$ cd my-fuel-project
$ tree
.
├── Forc.toml
└── src
└── main.sw
Forc.toml
is the Forc manifest file, containing information about the project and dependencies.
A src/
directory is created, with a single main.sw
Sway file in it.
forc-parse-bytecode
Parse bytecode file into a debug format
USAGE:
forc parse-bytecode [OPTIONS] <FILE_PATH>
ARGS:
<FILE_PATH>
OPTIONS:
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
-s
, --silent
Silence all output
-v
, --verbose
Use verbose output
EXAMPLE
We can try this command with the initial project created using forc init
, with the counter template:
forc new --template counter counter
cd counter
forc build -o obj
counter$ forc parse-bytecode obj
half-word byte op raw notes
0 0 JI(4) 90 00 00 04 conditionally jumps 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 c8 data section offset hi (200)
4 16 LW(63, 12, 1) 5d fc c0 01
5 20 ADD(63, 63, 12) 10 ff f3 00
...
...
...
60 240 Undefined 00 00 00 00
61 244 Undefined fa f9 0d d3
62 248 Undefined 00 00 00 00
63 252 Undefined 00 00 00 c8
forc-plugins
Find all forc plugins available via PATH
.
Prints information about each discovered plugin.
USAGE:
forc plugins [OPTIONS]
OPTIONS:
-d
, --describe
Prints the long description associated with each listed plugin
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
-p
, --paths
Prints the absolute path to each discovered plugin
-s
, --silent
Silence all output
-v
, --verbose
Use verbose output
forc-test
Run the Sway unit tests for the current project.
NOTE: This feature is not yet implemented. Track progress at the following link: https://github.com/FuelLabs/sway/issues/1832
NOTE: Previously this command was used to support Rust integration testing, however the provided
behaviour served no benefit over running cargo test
directly. The proposal to change the behaviour
to support unit testing can be found at the following link:
https://github.com/FuelLabs/sway/issues/1833
Sway unit tests are functions decorated with the #[test]
attribute. Each test is compiled as a
unique entry point for a single program and has access to the namespace of the module in which it is
declared.
Unit tests decorated with the #[test(script)]
attribute that are declared within contract
projects may also call directly into their associated contract's ABI.
Upon successful compilation, test scripts are executed to their completion. A test is considered a
failure in the case that a revert (rvrt
) instruction is encountered during execution. Otherwise,
it is considered a success.
USAGE:
forc test [OPTIONS] [FILTER]
ARGS:
<FILTER> When specified, only tests containing the given string will be executed
OPTIONS:
--build-profile
<BUILD_PROFILE>
Name of the build profile to use. If it is not specified, forc will use debug build profile
-g
, --debug-outfile
<DEBUG_OUTFILE>
If set, outputs source file mapping in JSON format
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
--locked
Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it needs to be updated, Forc will exit with an error
--minify-json-abi
By default the JSON for ABIs is formatted for human readability. By using this option JSON output will be "minified", i.e. all on one line without whitespace
--minify-json-storage-slots
By default the JSON for initial storage slots is formatted for human readability. By using this option JSON output will be "minified", i.e. all on one line without whitespace
-o
<BINARY_OUTFILE>
If set, outputs a binary file representing the script bytes
--offline
Offline mode, prevents Forc from using the network when managing dependencies. Meaning it will only try to use previously downloaded dependencies
--output-directory
<OUTPUT_DIRECTORY>
The directory in which the sway compiler output artifacts are placed.
By default, this is <project-root>/out
.
-p
, --path
<PATH>
Path to the project, if not specified, current working directory will be used
--print-ast
Print the generated Sway AST (Abstract Syntax Tree)
--print-finalized-asm
Print the finalized ASM.
This is the state of the ASM with registers allocated and optimisations applied.
--print-intermediate-asm
Print the generated ASM.
This is the state of the ASM prior to performing register allocation and other ASM optimisations.
--print-ir
Print the generated Sway IR (Intermediate Representation)
--release
Use release build plan. If a custom release plan is not specified, it is implicitly added to the manifest file.
If --build-profile is also provided, forc omits this flag and uses provided build-profile.
-s
, --silent
Silence all output
-t
, --terse
Terse mode. Limited warning and error output
--time-phases
Output the time elapsed over each part of the compilation process
-v
, --verbose
Use verbose output
forc-update
Update dependencies in the Forc dependencies directory
USAGE:
forc update [OPTIONS]
OPTIONS:
-c
, --check
Checks if the dependencies have newer versions. Won't actually perform the update, will output which ones are up-to-date and outdated
-d
<TARGET_DEPENDENCY>
Dependency to be updated. If not set, all dependencies will be updated
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
-p
, --path
<PATH>
Path to the project, if not specified, current working directory will be used
-s
, --silent
Silence all output
-v
, --verbose
Use verbose output
forc-template
Create a new Forc project from a git template
USAGE:
forc template [OPTIONS] <PROJECT_NAME>
ARGS:
<PROJECT_NAME>
The name of the project that will be created
OPTIONS:
-h
, --help
Print help information
-L
, --log-level
<LOG_LEVEL>
Set the log level
-s
, --silent
Silence all output
-t
, --template-name
<TEMPLATE_NAME>
The name of the template that needs to be fetched and used from git repo provided
-u
, --url
<URL>
The template url, should be a git repo [default: https://github.com/fuellabs/sway]
-v
, --verbose
Use verbose output
EXAMPLE
forc template --url https://github.com/owner/template/ --project_name my_example_project
The command above fetches the HEAD
of the template
repo and searches for Forc.toml
at the root of the fetched repo. It will fetch the repo and prepare a new Forc.toml
with the new project name. Outputs everything to current_dir/project_name
.
forc template --url https://github.com/FuelLabs/sway --template_name counter --project_name my_example_project
The command above fetches the HEAD of the sway
repo and searches for counter
example inside it (there is an example called counter
under sway/examples
). It will fetch the counter
example and prepare a new Forc.toml
with the new project name. Outputs everything to current_dir/project_name
.
Plugins
Plugins can be used to extend forc
with new commands that go beyond the native commands mentioned in the previous chapter. While the Fuel ecosystem provides a few commonly useful plugins (forc-fmt
, forc-client
, forc-lsp
, forc-explore
), anyone can write their own!
Let's install a plugin, forc-explore
, and see what's underneath the plugin:
cargo install forc-explore
Check that we have installed forc-explore
:
$ forc plugins
Installed Plugins:
forc-explore
forc-explore
runs the Fuel Network Explorer, which you can run and check out for yourself:
$ forc explore
Fuel Network Explorer 0.1.1
Running server on http://127.0.0.1:3030
Server::run{addr=127.0.0.1:3030}: listening on http://127.0.0.1:3030
You can visit http://127.0.0.1:3030 to check out the network explorer!
Note that some plugin crates can also provide more than one command. For example, installing the forc-client
plugin provides the forc deploy
and forc run
commands. This is achieved by specifying multiple [[bin]]
targets within the forc-client
manifest.
Writing your own plugin
We encourage anyone to write and publish their own forc
plugin to enhance their development experience.
Your plugin must be named in the format forc-<MY_PLUGIN>
and you may use the above template as a starting point. You can use clap and add more subcommands, options and configurations to suit your plugin's needs.
forc-client
Forc plugin for interacting with a Fuel node.
Initializing the wallet and adding accounts
If you don't have an initialized wallet or any account for your wallet you won't be able to sign transactions.
To initialize a wallet you can use forc wallet init
. It will ask you to choose a password to encrypt your wallet. After the initialization is done you will have your mnemonic phrase.
After you have an initialized wallet, you can create an account for it by simply running forc wallet new
. It will ask your password to decrypt the wallet before creating an account.
Signing transactions using forc-wallet
CLI
To submit the transactions created by forc deploy
or forc run
, you need to sign them first (unless you are using a client without UTXO validation). To sign a transaction you can use forc-wallet
CLI. This section is going to walk you through the whole signing process.
By default fuel-core
runs without UTXO validation, which means you can run unsigned transactions. This allows you to send invalid inputs to emulate different conditions.
If you want to run fuel-core
with UTXO validation, you can pass --utxo-validation
to fuel-core run
. If UTXO validation is enabled, unsigned transactions will return an error.
To install forc-wallet
please refer to forc-wallet
's github repo.
- Construct the transaction by using either
forc deploy
orforc run
. To do so simply runforc deploy
orforc run
with your desired parameters. For a list of parameters please refer to the forc-deploy or forc-run section of the book. Once you run either command you will be asked the address of the wallet you are going to be signing with. After the address is given the transaction will be generated and you will be given a transaction ID. At this point CLI will actively wait for you to insert the signature. - Take the transaction ID generated in the first step and sign it with
forc wallet sign <transaction_id> <account_index>
. This will generate a signature. - Take the signature generated in the second step and provide it to
forc-deploy
(orforc-run
). Once the signature is provided, the signed transaction will be submitted.
Other useful commands of forc-wallet
- You can see a list of existing accounts with
list
command.
forc wallet list
- If you want to retrieve the address for an account by its index you can use
account
command.
forc wallet account <account_index>
If you don't want to sign the transaction generated by
forc-deploy
orforc-run
you can pass--unsigned
to them.forc-deploy --unsigned
forc-run --unsigned
Interacting with the testnet
While using forc-deploy
or forc-run
to interact with the testnet you need to pass the testnet end point with --url
forc-deploy --url https://node-beta-1.fuel.network/graphql:443
Since deploying and running projects on the testnet cost gas, you will need coins to pay for them. You can get some using the testnet faucet.
Also the default value of the "gas price" parameter is 0 for both forc-deploy
and forc-run
. Without changing it you will get an error complaining about gas price being too low. While using testnet you can pass --gas-price 1
to overcome this issue. So a complete command for deploying to the testnet would look like:
forc-deploy --url https://node-beta-1.fuel.network/graphql:443 --gas-price 1
forc-deploy
USAGE:
forc deploy [OPTIONS] [SIGNING_KEY]
ARGS:
<SIGNING_KEY> Set the key to be used for signing
OPTIONS:
--build-profile
<BUILD_PROFILE>
Name of the build profile to use. If it is not specified, forc will use debug build profile
-g
, --debug-outfile
<DEBUG_OUTFILE>
If set, outputs source file mapping in JSON format
--gas-limit
<GAS_LIMIT>
Set the transaction gas limit. Defaults to the maximum gas limit
--gas-price
<GAS_PRICE>
Set the transaction gas price. Defaults to 0
-h
, --help
Print help information
--locked
Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it needs to be updated, Forc will exit with an error
--minify-json-abi
By default the JSON for ABIs is formatted for human readability. By using this option JSON output will be "minified", i.e. all on one line without whitespace
--minify-json-storage-slots
By default the JSON for initial storage slots is formatted for human readability. By using this option JSON output will be "minified", i.e. all on one line without whitespace
-o
<BINARY_OUTFILE>
If set, outputs a binary file representing the script bytes
--offline
Offline mode, prevents Forc from using the network when managing dependencies. Meaning it will only try to use previously downloaded dependencies
--output-directory
<OUTPUT_DIRECTORY>
The directory in which the sway compiler output artifacts are placed.
By default, this is <project-root>/out
.
-p
, --path
<PATH>
Path to the project, if not specified, current working directory will be used
--print-ast
Print the generated Sway AST (Abstract Syntax Tree)
--print-finalized-asm
Print the finalized ASM.
This is the state of the ASM with registers allocated and optimisations applied.
--print-intermediate-asm
Print the generated ASM.
This is the state of the ASM prior to performing register allocation and other ASM optimisations.
--print-ir
Print the generated Sway IR (Intermediate Representation)
--release
Use release build plan. If a custom release plan is not specified, it is implicitly added to the manifest file.
If --build-profile is also provided, forc omits this flag and uses provided build-profile.
-t
, --terse
Terse mode. Limited warning and error output
--time-phases
Output the time elapsed over each part of the compilation process
-u
, --url
<URL>
The node url to deploy, if not specified uses DEFAULT_NODE_URL. If url is specified overrides network url in manifest file (if there is one)
--unsigned
Do not sign the transaction
-V
, --version
Print version information
EXAMPLE
You can use forc deploy
, which triggers a contract deployment transaction and sends it to a running node.
Alternatively, you can deploy your Sway contract programmatically using fuels-rs, our Rust SDK.
You can find an example within our fuels-rs book.
forc-run
Run script project. Crafts a script transaction then sends it to a running node
USAGE:
forc run [OPTIONS] [ARGS]
ARGS:
<NODE_URL> URL of the Fuel Client Node
[env: FUEL_NODE_URL=]
<SIGNING_KEY> Set the key to be used for signing
OPTIONS:
--build-profile
<BUILD_PROFILE>
Name of the build profile to use. If it is not specified, forc will use debug build profile
--contract
<CONTRACT>
32-byte contract ID that will be called during the transaction
-d
, --data
<DATA>
Hex string of data to input to script
--dry-run
Only craft transaction and print it out
-g
, --debug-outfile
<DEBUG_OUTFILE>
If set, outputs source file mapping in JSON format
--gas-limit
<GAS_LIMIT>
Set the transaction gas limit. Defaults to the maximum gas limit
--gas-price
<GAS_PRICE>
Set the transaction gas price. Defaults to 0
-h
, --help
Print help information
-k
, --kill-node
Kill Fuel Node Client after running the code. This is only available if the node is
started from forc run
--locked
Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it needs to be updated, Forc will exit with an error
--minify-json-abi
By default the JSON for ABIs is formatted for human readability. By using this option JSON output will be "minified", i.e. all on one line without whitespace
--minify-json-storage-slots
By default the JSON for initial storage slots is formatted for human readability. By using this option JSON output will be "minified", i.e. all on one line without whitespace
-o
<BINARY_OUTFILE>
If set, outputs a binary file representing the script bytes
--output-directory
<OUTPUT_DIRECTORY>
The directory in which the sway compiler output artifacts are placed.
By default, this is <project-root>/out
.
-p
, --path
<PATH>
Path to the project, if not specified, current working directory will be used
--print-ast
Print the generated Sway AST (Abstract Syntax Tree)
--print-finalized-asm
Print the finalized ASM.
This is the state of the ASM with registers allocated and optimisations applied.
--print-intermediate-asm
Print the generated ASM.
This is the state of the ASM prior to performing register allocation and other ASM optimisations.
--print-ir
Print the generated Sway IR (Intermediate Representation)
-r
, --pretty-print
Pretty-print the outputs from the node
--release
Use release build plan. If a custom release plan is not specified, it is implicitly added to the manifest file.
If --build-profile is also provided, forc omits this flag and uses provided build-profile.
--simulate
Execute the transaction and return the final mutated transaction along with receipts (which includes whether the transaction reverted or not). The transaction is not inserted in the node's view of the blockchain, (i.e. it does not affect the chain state)
-t
, --terse
Terse mode. Limited warning and error output
--time-phases
Output the time elapsed over each part of the compilation process
--unsigned
Do not sign the transaction
-V
, --version
Print version information
forc-explore
Forc plugin for running the Fuel Block Explorer.
USAGE:
forc-explore [OPTIONS] [SUBCOMMAND]
OPTIONS:
-h
, --help
Print help information
-p
, --port
<PORT>
The port number at which the explorer will run on localhost [default: 3030]
-V
, --version
Print version information
SUBCOMMANDS:
clean
Cleans up any existing state associated with the fuel block explorer
help
Print this message or the help of the given subcommand(s)
forc-fmt
Forc plugin for running the Sway code formatter.
USAGE:
forc-fmt [OPTIONS]
OPTIONS:
-c
, --check
Run in 'check' mode.
- Exits with
0
if input is formatted correctly. - Exits with1
and prints a diff if formatting is required.
-h
, --help
Print help information
-p
, --path
<PATH>
Path to the project, if not specified, current working directory will be used
-V
, --version
Print version information
forc-lsp
Forc plugin for the Sway LSP (Language Server Protocol) implementation.
USAGE:
forc-lsp
OPTIONS:
-h
, --help
Print help information
-V
, --version
Print version information