The Sway Programming Language
Welcome to the Sway programming language book 🌴.
Q: Hi! What is Sway?
Sway is a domain-specific programming language for implementing smart contracts on blockchain platforms, most notably for the Fuel Virtual Machine (Fuel VM).
Heavily inspired by Rust's approach to systems programming, Sway aims to bring modern programming language features and tooling to smart contract development whilst retaining performance, fine grained control and making extensive use of static analysis to prevent common security issues.
Q: What does "domain-specific" mean?
Sway is specifically made to be used within a blockchain environment, which behaves very differently than traditional computers. This domain specific design permits it to make the right decisions about trade-offs at every level of the stack, enabling you to write fast, secure and cost effective smart contracts with features suited to your specific needs.
Q: Why not use Solidity?
Solidity is a venerable pioneer but it suffers from being tied to a lot of the historical quirks of the EVM. It lacks common features programmers have come to expect, has a relatively inexpressive type system, and it lacks a unified tooling ecosystem.
In Sway, we let you design smart contracts with a full modern box of tools. You get a fully featured language with generics, algebraic types and trait based polymorphism. You also get an integrated, unified and easy to use toolchain with code completion LSP server, formatter, documentation generation and everything you need to run and deploy your contracts so that nothing comes between you and implementing what you want.
Our expressive type system allows you to catch semantic mistakes, we provide good defaults and we do extensive static analysis checks (such as enforcing the Checks, Effects, Interactions pattern) so that you can make sure you write secure and correct code at compile time.
Q: Why not use Rust?
Whilst Rust is a great systems programming language (and Sway itself is written in Rust), it isn't suited for smart contract development.
Rust shines because it can use zero-cost abstractions and its sophisticated borrow-checker memory model to achieve impressive runtime performance for complex programs without a garbage collector.
On a blockchain, cost of execution and deployment is the scarce resource. Memory usage is low and execution time is short. This makes complex memory management in general much too expensive to be worthwhile and Rust's borrow checker a burden with no upside.
General purpose programming languages in general are ill suited to this environment because their design has to assume execution on a general-purpose computing environment.
Sway attempts to bring all the other advantages of Rust, including its modern type system, approach to safety and good defaults to smart contract developers by providing familiar syntax and features adapted to the specific needs of the blockchain environment.
Q: I don't know Rust or Solidity. Can I still learn Sway?
Yes! If you are familiar with the basics of programming, blockchain, and using a terminal you can build with Sway.
Q: What can I build with Sway?
You can build smart contracts and their components and libraries for them. You can learn more about the different program types and how they fit together in the Program Types section.
Q: Do I need to install anything?
If you want to develop with Sway in your local environment, you need to install fuelup
and your editor of choice that supports LSP, such as VSCode.
If you don't want to install anything just yet, you can use the Sway Playground to edit, compile, and deploy Sway code.
Q: Where can I find example Sway code?
You can find example applications built with Sway in the Sway Applications repository on GitHub. You can also find projects building on Fuel in the Fuel ecosystem home.
Q: What is the standard library?
The standard library, also referred to as std
, is a library that offers core functions and helpers for developing in Sway. The standard library has it's own reference documentation that has detailed information about each module in std
.
Q: What are Sway standards?
Similar to ERC standards for Ethereum and Solidity, Sway has it's own SRC standards that help enable cross compatibility across different smart contracts. For more information on using a Sway Standard, you can check out the Sway-Standards Repository.
Q: How can I make a token?
Sway has multiple native assets. To mint a new native asset, check out the native assets page.
Q: How can I make an NFT?
You can find an example of an NFT contract in Sway in the Sway Applications repo.
Q: How can I test Sway code?
Sway provides unit testing, so you can test your Sway code with Sway. You can also use the Fuel Rust SDK or TypeScript SDK to test your Sway programs.
Q: How can I deploy a contract?
You can use the forc deploy
command to deploy a contract. For a detailed guide on how to deploy a contract, refer to the quickstart guide.
Q: Is there a way to convert Solidity code to Sway?
Yes! You can use the Solidity to Sway transpiler built in to the Sway Playground to convert Solidity code into Sway code. Note that the transpiler is still experimental, and may not work in every case.
Q: How can I get help with Sway?
If you run into an issue or have a question, post it on the Fuel forum so someone in the Fuel community can help.
Q: Where should I get started?
Ready to build? You can find step-by-step guides for how to build an application with Sway in the Fuel Developer Guides.
Want to read? Get started by reading the Introduction and Basics sections of this book.
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.
Getting Started
Installing the Fuel
toolchain
Please visit the Fuel Installation Guide to install the Fuel toolchain binaries and prerequisites.
Sway Quickstart
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.
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.
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, though you might have to explicitly set the Sway plugin as the
default formatter, like this:
"[sway]": {
"editor.defaultFormatter": "FuelLabs.sway-vscode-plugin"
}
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 --asm final
to view the generated assembly:
$ forc build --asm final
...
.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. Navigate to the appropriate tagged release if the latest master
is not compatible. You can find the latest std
documentation here.
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::storage_vec::*;
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::storage_key::*
, contains the API for accessing acore::storage::StorageKey
which describes a location in storage.std::storage::storage_map::*
, 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::assert::assert_eq
, a function that reverts the VM and logs its two inputsv1
andv2
if the conditionv1
==v2
isfalse
.std::assert::assert_ne
, a function that reverts the VM and logs its two inputsv1
andv2
if the conditionv1
!=v2
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.std::logging::log
, a function that logs arbitrary stack types.std::auth::msg_sender
, a function that gets theIdentity
from which a call was made.
Sway Standards
Just like many other smart contract languages, usage standards have been developed to enable cross compatibility between smart contracts.
For more information on using a Sway Standard, please refer to the Sway-Standards Repository.
Standards
Native Asset Standards
- SRC-20; Native Asset Standard defines the implementation of a standard API for Native Assets using the Sway Language.
- SRC-3; Mint and Burn is used to enable mint and burn functionality for Native Assets.
- SRC-7; Arbitrary Asset Metadata Standard is used to store metadata for Native Assets, usually as NFTs.
- SRC-9; Metadata Keys Standard is used to store standardized metadata keys for Native Assets in combination with the SRC-7 standard.
- SRC-6; Vault Standard defines the implementation of a standard API for asset vaults developed in Sway.
Predicate Standards
- SRC-13; Soulbound Address Standard defines a specific
Address
as a Soulbound Address for Soulbound Assets to become non-transferable.
Access Control Standards
- SRC-5; Ownership Standard is used to restrict function calls to admin users in contracts.
Contract Standards
- SRC-12; Contract Factory defines the implementation of a standard API for contract factories.
Bridge Standards
- SRC-8; Bridged Asset defines the metadata required for an asset bridged to the Fuel Network.
- SRC-10; Native Bridge Standard defines the standard API for the Native Bridge between the Fuel Chain and the canonical base chain.
Documentation Standards
- SRC-2; Inline Documentation defines how to document your Sway files.
Standards Support
Libraries have also been developed to support Sway Standards. These can be in Sway-Libs.
Example
Some basic example contracts to see how Sway and Forc work.
Additional examples can be found in the Sway Applications repository.
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.write(value);
value
}
#[storage(read, write)]
fn increment_counter(amount: u64) -> u64 {
let incremented = storage.counter.read() + amount;
storage.counter.write(incremented);
incremented
}
}
Build and deploy
The following commands can be used to build and deploy the contract. For a detailed tutorial, refer to Building and Deploying.
# Build the contract
forc build
# Deploy the contract
forc deploy --testnet
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 the result 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
The ABI declaration is a separate project from your ABI implementation. The project structure for the code should be organized as follows with the wallet_abi
treated as an external library:
.
├── wallet_abi
│ ├── Forc.toml
│ └── src
│ └── main.sw
└── wallet_smart_contract
├── Forc.toml
└── src
└── main.sw
It's also important to specify the source of the dependency within the project's Forc.toml
file when using external libraries. Inside the wallet_smart_contract
project, it requires a declaration like this:
[dependencies]
wallet_abi = { path = "../wallet_abi/" }
ABI Declaration
library;
abi Wallet {
#[storage(read, write), payable]
fn receive_funds();
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address);
}
ABI Implementation
contract;
use std::{asset::transfer, call_frames::msg_asset_id, context::msg_amount,};
use wallet_abi::Wallet;
const OWNER_ADDRESS = Address::from(0x8900c5bec4ca97d4febf9ceb4754a60d782abbf3cd815836c1872116f203f861);
storage {
balance: u64 = 0,
}
impl Wallet for Contract {
#[storage(read, write), payable]
fn receive_funds() {
if msg_asset_id() == AssetId::base() {
// If we received the base asset then keep track of the balance.
// Otherwise, we're receiving other native assets and don't care
// about our balance of coins.
storage.balance.write(storage.balance.read() + msg_amount());
}
}
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address) {
let sender = msg_sender().unwrap();
match sender {
Identity::Address(addr) => assert(addr == OWNER_ADDRESS),
_ => revert(0),
};
let current_balance = storage.balance.read();
assert(current_balance >= amount_to_send);
storage.balance.write(current_balance - amount_to_send);
// Note: `transfer()` is not a call and thus not an
// interaction. Regardless, this code conforms to
// checks-effects-interactions to avoid re-entrancy.
transfer(
Identity::Address(recipient_address),
AssetId::base(),
amount_to_send,
);
}
}
Liquidity Pool Example
All contracts in Fuel can mint and burn their own native asset. 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 asset operations.
In this example, we show a basic liquidity pool contract minting its own native asset LP asset.
contract;
use std::{
asset::{
mint_to,
transfer,
},
call_frames::msg_asset_id,
constants::DEFAULT_SUB_ID,
context::msg_amount,
hash::*,
};
abi LiquidityPool {
fn deposit(recipient: Address);
fn withdraw(recipient: Address);
}
const BASE_ASSET: AssetId = AssetId::from(0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c);
impl LiquidityPool for Contract {
fn deposit(recipient: Address) {
assert(msg_asset_id() == BASE_ASSET);
assert(msg_amount() > 0);
// Mint two times the amount.
let amount_to_mint = msg_amount() * 2;
// Mint some LP assets based upon the amount of the base asset.
mint_to(Identity::Address(recipient), DEFAULT_SUB_ID, amount_to_mint);
}
fn withdraw(recipient: Address) {
let asset_id = AssetId::default();
assert(msg_asset_id() == asset_id);
assert(msg_amount() > 0);
// Amount to withdraw.
let amount_to_transfer = msg_amount() / 2;
// Transfer base asset to recipient.
transfer(Identity::Address(recipient), BASE_ASSET, amount_to_transfer);
}
}
Sway Applications
The Sway-Applications Repository contains end-to-end example applications that are written in Sway in order to demonstrate what can be built.
Asset Management
- Airdrop is an asset distribution program where users are able to claim assets given a valid merkle proof.
- Escrow is a third party that keeps an asset on behalf of multiple parties.
- Non-Fungible Native Asset (NFT) is an asset contract which provides unique collectibles, identified and differentiated by IDs, where assets contain metadata giving them distinctive characteristics.
- Fractional Non-Fungible Token (F-NFT) is a token contract which issues shares or partial ownership upon locking an NFT into a vault.
- Timelock is a contract which restricts the execution of a transaction to a specified time range.
- Native Asset is a basic asset contract that enables the use of Native Assets on Fuel using existing standards and libraries.
Decentralized Finance
- English Auction is an auction where users bid up the price of an asset until the bidding period has ended or a reserve has been met.
- Fundraiser is a program allowing users to pledge towards a goal.
- OTC Swap Predicate is a predicate that can be used to propose and execute an atomic swap between two parties without requiring any on-chain state.
Governance
- Decentralized Autonomous Organization (DAO) is an organization where users get to vote on governance proposals using governance assets.
- Multi-Signature Wallet is a wallet that requires multiple signatures to execute a transaction.
Games
- TicTacToe is a game where two players compete to align three markers in a row.
Other
- Counter-Script is a script that calls a contract to increment a counter.
- Name-Registry allows users to perform transactions with human readable names instead of addresses.
- Oracle is a smart contract that provides off-chain data to on-chain applications.
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 (also called a DEX).
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;
abi Wallet {
#[storage(read, write), payable]
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), payable]
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), payable]
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), payable]
fn receive_funds() {
if msg_asset_id() == AssetId::base() {
// If we received the base asset then keep track of the balance.
// Otherwise, we're receiving other native assets and don't care
// about our balance of coins.
storage.balance.write(storage.balance.read() + msg_amount());
}
}
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address) {
let sender = msg_sender().unwrap();
match sender {
Identity::Address(addr) => assert(addr == OWNER_ADDRESS),
_ => revert(0),
};
let current_balance = storage.balance.read();
assert(current_balance >= amount_to_send);
storage.balance.write(current_balance - amount_to_send);
// Note: `transfer()` is not a call and thus not an
// interaction. Regardless, this code conforms to
// checks-effects-interactions to avoid re-entrancy.
transfer(
Identity::Address(recipient_address),
AssetId::base(),
amount_to_send,
);
}
}
You may notice once again the similarities between traits and ABIs. And, indeed, as a bonus, you can define methods in addition to the interface surface of an ABI, just like a trait. These pre-implemented ABI methods automatically become available as part of the contract interface that implements the corresponding ABI.
Note that the above implementation of the ABI follows the Checks, Effects, Interactions pattern.
The ContractId
type
Contracts have an associated ContractId
type in Sway. The ContractId
type allows for Sway programs to refer to contracts in the Sway language. Please refer to the ContractId section of the book for more information on ContractId
s.
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 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: b256::zero(),
}(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
isb256::zero()
.
Libraries
Libraries in Sway are files used to define new common behavior.
The most prominent example of this is the Sway Standard Library that is made implicitly available to all Forc projects created using forc new
.
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;
// 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:
library;
- 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 publicly 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.
The mod
keyword registers a submodule, making its items (such as functions and structs) accessible from the parent library.
If used at the top level it will refer to a file in the src
folder and in other cases in a folder named after the library in which it is defined.
For example, the lib.sw
of the standard library looks like:
library;
mod block;
mod storage;
mod constants;
mod vm;
// .. Other deps
with other libraries contained in the src
folder, like the vm
library (inside of src/vm.sw
):
library;
mod evm;
// ...
and it's own sub-library evm
located in src/vm/evm.sw
:
library;
// ...
Using Libraries
There are two types of Sway libraries, based on their location and how they can be imported.
Internal Libraries
Internal libraries are located within the project's src
directory alongside
main.sw
or in the appropriate folders as shown below:
$ tree
.
├── Cargo.toml
├── Forc.toml
└── src
├── internal_lib.sw
├── main.sw
└── internal_lib
└── nested_lib.sw
As internal_lib
is an internal library, it can be imported into main.sw
as follows:
- Use the
mod
keyword followed by the library name to make the internal library a dependency - Use the
use
keyword with a::
separating the name of the library and the imported item(s)
mod internal_lib; // Assuming the library name in `internal_lib.sw` is `internal_lib`
use internal_lib::mint;
// `mint` from `internal_library` is now available in this file
External Libraries
External libraries are located outside the main src
directory as shown below:
$ tree
.
├── my_project
│ ├── Cargo.toml
│ ├── Forc.toml
│ └─── src
│ └── main.sw
│
└── external_lib
├── Cargo.toml
├── Forc.toml
└─── src
└── lib.sw
As external_lib
is outside the src
directory of my_project
, it needs to be added as a dependency in the Forc.toml
file of my_project
, by adding the library path in the dependencies
section as shown below, before it can be imported:
[dependencies]
external_library = { path = "../external_library" }
Once the library dependency is added to the toml
file, you can import items from it as follows:
- Make sure the item you want imported are declared with the
pub
keyword (if applicable, for instance:pub fn mint() {}
) - Use the
use
keyword to selectively import items from the library
use external_library::mint;
// `mint` from `external_library` is now available in this file
Wildcard imports using *
are supported, but it is generally recommended to use explicit imports where possible.
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
.
Reference Sway Libraries
The repository sway-libs
is a collection of external libraries that you can import and make use of in your Fuel applications. These libraries are meant to be implementations of common use-cases valuable for dapp development.
Some Sway Libraries to try out:
Example
You can import and use a Sway Library such as the Ownership library just like any other external library.
use ownership::Ownership;
Once imported, you can use the following basic functionality of the library in your smart contract:
- Declaring an owner
- Changing ownership
- Renouncing ownership
- Ensuring a function may only be called by the owner
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 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: b256::zero(),
}(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 decentralized exchanges to get the price for the input asset, or a script to re-adjust a Collateralized Debt Position via a flash loan.
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
}
The address of this predicate is 0xd19a5fe4cb9baf41ad9813f1a6fef551107c8e8e3f499a6e32bccbb954a74764
. Any assets sent to this address can be unlocked or claimed by executing the predicate above as it always evaluates to true.
It does not need to be deployed to a blockchain because it only exists during a transaction. That being said, the predicate address is on-chain as the owner of one or more UTXOs.
Transfer Coins to a Predicate
In Fuel, coins can be sent to a predicate's address(the bytecode root, calculated here).
Spending Predicate Coins
The coin UTXOs become spendable not on the provision of a valid signature, but rather if the supplied predicate both has a root that matches their owner, and evaluates to true
.
If a predicate reverts, or tries to access impure VM opcodes, the evaluation is automatically false
.
An analogy for predicates is rather than a traditional 12 or 24 word seed phrase that generates a private key and creates a valid signature, a predicate's code can be viewed as the private key. Anyone with the code may execute a predicate, but only when the predicate evaluates to true may the assets owned by that address be released.
Spending Conditions
Predicates may introspect the transaction spending their coins (inputs, outputs, script bytecode, etc.) and may take runtime arguments, either or both of which may affect the evaluation of the predicate.
It is important to note that predicates cannot read or write memory. They may however check the inputs and outputs of a transaction. For example in the OTC Predicate Swap Example, a user may specify they would like to swap asset1
for asset2
and with amount of 5
. The user would then send asset1
to the predicate. Only when the predicate can verify that the outputs include 5
coins of asset2
being sent to the original user, may asset1
be transferred out of the predicate.
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 native way to debug them aside from using a single-stepping debugger.
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 efficient through the use of strong static analysis and compiler feedback.
Get started with the basics of Sway:
- Variables
- Built-in Types
- Commonly Used Library Types
- Blockchain Types
- Functions
- Structs, Tuples, and Enums
- Methods and Associated Functions
- Constants
- Comments and Logging
- Control Flow
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.
When assigning to a mutable variable, the right-hand side of the assignment is evaluated before the left-hand side. In the below example, the mutable variable i
will first be increased and the resulting value of 1
will be stored to array[1]
, thus resulting in array
being changed to [0, 1, 0]
.
let mut array = [0, 0, 0];
let mut i = 0;
array[i] = {
i += 1;
i
};
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] = __to_str_array("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.
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)u256
(256-bit unsigned integer)str[]
(fixed-length string)str
(string slices)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.
If a 64-bit or 256-bit arithmetic operation produces an overflow or an underflow, computation gets reverted automatically by FuelVM.
8/16/32-bit arithmetic operations are emulated using their 64-bit analogues with additional overflow/underflow checks inserted, which generally results in somewhat higher gas consumption.
The same does not happen with 256-bit operations, including b256
, which uses specialized operations and are as efficient as possible.
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 Slices
In Sway, string literals are stored as variable length string slices. Which means that they are stored as a pointer to the actual string data and its length.
let my_string: str = "fuel";
String slices, because they contain pointers have limited usage. They cannot be used as constants, storage fields, or configurable constants, nor as main function arguments or returns.
For these cases one must use string arrays, as described below.
String Arrays
In Sway, static-length strings are a primitive type. This means that when you declare a string array, 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] = __to_str_array("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.
As above, string literals are typed as string slices. So that is why the need for __to_str_array
that convert them to string arrays at compile time.
Conversion during runtime can be done with from_str_array
and try_as_str_array
. The latter can fail, given that the specified string array must be big enough for the string slice content.
let a: str = "abcd";
let b: str[4] = a.try_as_str_array().unwrap();
let c: str = from_str_array(b);
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 including 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.
Array elements can also be mutated if the underlying array is declared as mutable:
let mut x = [1, 2, 3, 4, 5];
x[0] = 0;
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 mut array_of_bools: [bool; 2] = [true, false];
assert(array_of_bools[0]);
// Mutating the element of an array
array_of_bools[1] = true;
assert(array_of_bools[1]);
}
Commonly Used Library Types
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, 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. Reference the standard library docs here.
Result<T, E>
Type Result
is the type used for returning and propagating errors. It is an enum
with two variants: Ok(T)
, representing success and containing a value, and Err(E)
, representing error and containing an error value. The T
and E
in this definition are type parameters, allowing Result
to be generic and to be used with any types.
/// `Result` is a type that represents either success (`Ok`) or failure (`Err`).
pub enum Result<T, E> {
/// Contains the success value.
Ok: T,
/// Contains the error value.
Err: E,
}
Functions return Result
whenever errors are expected and recoverable.
Take the following example:
script;
enum MyContractError {
DivisionByZero: (),
}
fn divide(numerator: u64, denominator: u64) -> Result<u64, MyContractError> {
if (denominator == 0) {
return Err(MyContractError::DivisionByZero);
} else {
Ok(numerator / denominator)
}
}
fn main() -> Result<u64, str[4]> {
let result = divide(20, 2);
match result {
Ok(value) => Ok(value),
Err(MyContractError::DivisionByZero) => Err(__to_str_array("Fail")),
}
}
Option<T>
Type Option
represents an optional value: every Option
is either Some
and contains a value, or None
, and does not. Option
types are very common in Sway code, as they have a number of uses:
- Initial values where
None
can be used as an initializer. - Return value for otherwise reporting simple errors, where
None
is returned on error.
The implementation of Option
matches on the variant: if it's Ok
it returns the inner value, if it's None
, it reverts.
/// A type that represents an optional value, either `Some(val)` or `None`.
pub enum Option<T> {
/// No value.
None: (),
/// Some value of type `T`.
Some: T,
}
Option
is commonly paired with pattern matching to query the presence of a value and take action, allowing developers to choose how to handle the None
case.
Below is an example that uses pattern matching to handle invalid divisions by 0 by returning an Option
:
script;
fn divide(numerator: u64, denominator: u64) -> Option<u64> {
if denominator == 0 {
None
} else {
Some(numerator / denominator)
}
}
fn main() {
let result = divide(6, 2);
// Pattern match to retrieve the value
match result {
// The division was valid
Some(x) => std::logging::log(x),
// The division was invalid
None => std::logging::log("Cannot divide by 0"),
}
}
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();
Getting a Contract's ContractId
To get the ContractId
of a contract in an internal context use the ContractId::this()
function:
impl MyContract for Contract {
fn foo() {
let this_contract_id: ContractId = ContractId::this();
}
}
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) => takes_address(address),
Identity::ContractId(contract_id) => takes_contract_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 = msg_sender().unwrap();
require(
sender == storage
.owner
.read(),
MyError::UnauthorizedUser(sender),
);
Converting Types
Below are some common type conversions in Sway:
Identity Conversions
Convert to Identity
let identity_from_b256: Identity = Identity::Address(Address::from(b256_address));
let identity_from_address: Identity = Identity::Address(address);
let identity_from_contract_id: Identity = Identity::ContractId(contract_id);
Convert Identity
to ContractId
or Address
match my_identity {
Identity::Address(address) => log(address),
Identity::ContractId(contract_id) => log(contract_id),
};
Convert ContractId
or Address
to b256
let b256_from_address: b256 = address.into();
let b256_from_contract_id: b256 = contract_id.into();
Convert b256
to ContractId
or Address
let address_from_b256: Address = Address::from(b256_address);
let contract_id_from_b256: ContractId = ContractId::from(b256_address);
String Conversions
Convert str
to str[]
let fuel_str: str = "fuel";
let fuel_str_array: str[4] = fuel_str.try_as_str_array().unwrap();
Convert str[]
to str
let fuel_str_array: str[4] = __to_str_array("fuel");
let fuel_str: str = from_str_array(fuel_str_array);
Number Conversions
Convert to u256
let u8_1: u8 = 2u8;
let u16_1: u16 = 2u16;
let u32_1: u32 = 2u32;
let u64_1: u64 = 2u64;
let b256_1: b256 = 0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20;
let u256_from_u8: u256 = u8_1.as_u256();
let u256_from_u16: u256 = u16_1.as_u256();
let u256_from_u32: u256 = u32_1.as_u256();
let u256_from_u64: u256 = u64_1.as_u256();
let u256_from_b256: u256 = b256_1.as_u256();
Convert to u64
let u8_1: u8 = 2u8;
let u16_1: u16 = 2u16;
let u32_1: u32 = 2u32;
let u256_1: u256 = 0x0000000000000000000000000000000000000000000000000000000000000002u256;
let u64_from_u8: u64 = u8_1.as_u64();
let u64_from_u16: u64 = u16_1.as_u64();
let u64_from_u32: u64 = u32_1.as_u64();
let u64_from_u256: Option<u64> = <u64 as TryFrom<u256>>::try_from(u256_1);
Convert to u32
let u8_1: u8 = 2u8;
let u16_1: u16 = 2u16;
let u64_1: u64 = 2;
let u256_1: u256 = 0x0000000000000000000000000000000000000000000000000000000000000002u256;
let u32_from_u8: u32 = u8_1.as_u32();
let u32_from_u16: u32 = u16_1.as_u32();
let u32_from_u64_1: Option<u32> = u64_1.try_as_u32();
let u32_from_u64_2: Option<u32> = <u32 as TryFrom<u64>>::try_from(u64_1);
let u32_from_u256: Option<u32> = <u32 as TryFrom<u256>>::try_from(u256_1);
Convert to u16
let u8_1: u8 = 2u8;
let u32_1: u32 = 2u32;
let u64_1: u64 = 2;
let u256_1: u256 = 0x0000000000000000000000000000000000000000000000000000000000000002u256;
let u16_from_u8: u16 = u8_1.as_u16();
let u16_from_u32_1: Option<u16> = u32_1.try_as_u16();
let u16_from_u32_2: Option<u16> = <u16 as TryFrom<u32>>::try_from(u32_1);
let u16_from_u64_1: Option<u16> = u64_1.try_as_u16();
let u16_from_u64_2: Option<u16> = <u16 as TryFrom<u64>>::try_from(u64_1);
let u16_from_u256: Option<u16> = <u16 as TryFrom<u256>>::try_from(u256_1);
Convert to u8
let u16_1: u16 = 2u16;
let u32_1: u32 = 2u32;
let u64_1: u64 = 2;
let u256_1: u256 = 0x0000000000000000000000000000000000000000000000000000000000000002u256;
let u8_from_u16_1: Option<u8> = u16_1.try_as_u8();
let u8_from_u16_2: Option<u8> = <u8 as TryFrom<u16>>::try_from(u16_1);
let u8_from_u32_1: Option<u8> = u32_1.try_as_u8();
let u8_from_u32_2: Option<u8> = <u8 as TryFrom<u32>>::try_from(u32_1);
let u8_from_u64_1: Option<u8> = u64_1.try_as_u8();
let u8_from_u64_2: Option<u8> = <u8 as TryFrom<u64>>::try_from(u64_1);
let u8_from_u256: Option<u8> = <u8 as TryFrom<u256>>::try_from(u256_1);
Convert to Bytes
use std::{bytes::Bytes, bytes_conversions::{b256::*, u16::*, u256::*, u32::*, u64::*,}};
let num = 5;
let little_endian_bytes: Bytes = num.to_le_bytes();
let big_endian_bytes: Bytes = num.to_be_bytes();
Convert from Bytes
use std::{bytes::Bytes, bytes_conversions::{b256::*, u16::*, u256::*, u32::*, u64::*,}};
let u16_from_le_bytes: u16 = u16::from_le_bytes(little_endian_bytes);
let u16_from_be_bytes: u16 = u16::from_be_bytes(big_endian_bytes);
let u32_from_le_bytes: u32 = u32::from_le_bytes(little_endian_bytes);
let u32_from_be_bytes: u32 = u32::from_be_bytes(big_endian_bytes);
let u64_from_le_bytes: u64 = u64::from_le_bytes(little_endian_bytes);
let u64_from_be_bytes: u64 = u64::from_be_bytes(big_endian_bytes);
let u256_from_le_bytes = u256::from_le_bytes(little_endian_bytes);
let u256_from_be_bytes = u256::from_be_bytes(big_endian_bytes);
let b256_from_le_bytes = b256::from_le_bytes(little_endian_bytes);
let b256_from_be_bytes = b256::from_be_bytes(big_endian_bytes);
Byte Array Conversions
Convert to a Byte Array
use std::array_conversions::{b256::*, u16::*, u256::*, u32::*, u64::*,};
let u16_1: u16 = 2u16;
let u32_1: u32 = 2u32;
let u64_1: u64 = 2u64;
let u256_1: u256 = 0x0000000000000000000000000000000000000000000000000000000000000002u256;
let b256_1: b256 = 0x000000000000000000000000000000000000000000000000000000000000002A;
// little endian
let le_byte_array_from_u16: [u8; 2] = u16_1.to_le_bytes();
let le_byte_array_from_u32: [u8; 4] = u32_1.to_le_bytes();
let le_byte_array_from_u64: [u8; 8] = u64_1.to_le_bytes();
let le_byte_array_from_u256: [u8; 32] = u256_1.to_le_bytes();
let le_byte_array_from_b256: [u8; 32] = b256_1.to_le_bytes();
// big endian
let be_byte_array_from_u16: [u8; 2] = u16_1.to_be_bytes();
let be_byte_array_from_u32: [u8; 4] = u32_1.to_be_bytes();
let be_byte_array_from_u64: [u8; 8] = u64_1.to_be_bytes();
let be_byte_array_from_u256: [u8; 32] = u256_1.to_be_bytes();
let be_byte_array_from_b256: [u8; 32] = b256_1.to_be_bytes();
Convert from a Byte Array
use std::array_conversions::{b256::*, u16::*, u256::*, u32::*, u64::*,};
let u16_byte_array: [u8; 2] = [2_u8, 1_u8];
let u32_byte_array: [u8; 4] = [4_u8, 3_u8, 2_u8, 1_u8];
let u64_byte_array: [u8; 8] = [8_u8, 7_u8, 6_u8, 5_u8, 4_u8, 3_u8, 2_u8, 1_u8];
let u256_byte_array: [u8; 32] = [
32_u8, 31_u8, 30_u8, 29_u8, 28_u8, 27_u8, 26_u8, 25_u8, 24_u8, 23_u8, 22_u8,
21_u8, 20_u8, 19_u8, 18_u8, 17_u8, 16_u8, 15_u8, 14_u8, 13_u8, 12_u8, 11_u8,
10_u8, 9_u8, 8_u8, 7_u8, 6_u8, 5_u8, 4_u8, 3_u8, 2_u8, 1_u8,
];
// little endian
let le_u16_from_byte_array: u16 = u16::from_le_bytes(u16_byte_array);
let le_u32_from_byte_array: u32 = u32::from_le_bytes(u32_byte_array);
let le_u64_from_byte_array: u64 = u64::from_le_bytes(u64_byte_array);
let le_u256_from_byte_array: u256 = u256::from_le_bytes(u256_byte_array);
let le_b256_from_byte_array: b256 = b256::from_le_bytes(u256_byte_array);
// big endian
let be_u16_from_byte_array: u16 = u16::from_be_bytes(u16_byte_array);
let be_u32_from_byte_array: u32 = u32::from_be_bytes(u32_byte_array);
let be_u64_from_byte_array: u64 = u64::from_be_bytes(u64_byte_array);
let be_u256_from_byte_array: u256 = u256::from_be_bytes(u256_byte_array);
let be_b256_from_byte_array: b256 = b256::from_be_bytes(u256_byte_array);
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.
Those data attributes are called fields and can be either public or private.
Private struct fields can be accessed only within the module in which their struct is declared. Public fields are accessible everywhere where the struct is accessible. This access control on the field level allows more fine grained encapsulation of data.
To explain these concepts, let's take a look at the following example, in which we have a module called data_structures.
In that module, we declare a struct named Foo
with two fields. The first field is named bar
, it is public and it accepts values of type u64
. The second field is named baz
, it is also public and it accepts bool
values.
In a similar way, we define the structs Point
, Line
, and TupleInStruct
. Since all those structs are public, and all their fields are public, they can be instantiated in other modules using the struct instantiation syntax as shown below.
On the other hand, the struct StructWithPrivateFields
can be instantiated only within the data_structures module, because it contains private fields. To be able to create instances of such structs outside of the module in which they are declared, the struct must offer constructor associated functions.
// the _data_structures_ module
library;
// Declare a struct type
pub struct Foo {
pub bar: u64,
pub baz: bool,
}
// Struct types for destructuring
pub struct Point {
pub x: u64,
pub y: u64,
}
pub struct Line {
pub p1: Point,
pub p2: Point,
}
pub struct TupleInStruct {
pub nested_tuple: (u64, (u32, (bool, str))),
}
// Struct type instantiable only in the module _data_structures_
pub struct StructWithPrivateFields {
pub public_field: u64,
private_field: u64,
other_private_field: u64,
}
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.
- Hard coding 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;
mod 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;
fn tuple() {
// You can declare the types yourself
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;
// 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;
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;
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;
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;
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
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 (or enum) the method is being called on.
Associated Functions
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.
Constructors
Constructors are associated functions that construct, or in other words instantiate, new instances of a type. Their return type is always the type itself. E.g., public structs that have private fields must provide a public constructor, or otherwise they cannot be instantiated outside of the module in which they are declared.
Declaring Methods and Associated Functions
To declare methods and associated functions for a struct or enum, use an impl
block. Here, impl
is short 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.
// it is at the same time a _constructor_ because it instantiates and returns
// a new instance of `Foo`.
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);
Constants
Constants are similar to variables; however, there are a few differences:
- Constants are always evaluated at compile-time.
- Constants can be declared both inside of a function and at global /
impl
scope. - The
mut
keyword cannot be used with constants.
const ID: u32 = 0;
Constant initializer expressions can be quite complex, but they cannot use, for
instance, assembly instructions, storage access, mutable variables, loops and
return
statements. Although, function calls, primitive types and compound data
structures are perfectly fine to use:
fn bool_to_num(b: bool) -> u64 {
if b {
1
} else {
0
}
}
fn arr_wrapper(a: u64, b: u64, c: u64) -> [u64; 3] {
[a, b, c]
}
const ARR2 = arr_wrapper(bool_to_num(1) + 42, 2, 3);
Associated Constants
Associated constants are constants associated with a type and can be declared in an impl
block or in a trait
definition.
Associated constants declared inside a trait
definition may omit their initializers to indicate that each implementation of the trait must specify those initializers.
The identifier is the name of the constant used in the path. The type is the type that the definition has to implement.
You can define an associated const
directly in the interface surface of a trait:
script;
trait ConstantId {
const ID: u32 = 0;
}
Alternatively, you can also declare it in the trait, and implement it in the interface of the types implementing the trait.
script;
trait ConstantId {
const ID: u32;
}
struct Struct {}
impl ConstantId for Struct {
const ID: u32 = 1;
}
fn main() -> u32 {
Struct::ID
}
impl self
Constants
Constants can also be declared inside non-trait impl
blocks.
script;
struct Point {
x: u64,
y: u64,
}
impl Point {
const ZERO: Point = Point { x: 0, y: 0 };
}
fn main() -> u64 {
Point::ZERO.x
}
Configurable Constants
Configurable constants are special constants that behave like regular constants in the sense that they cannot change during program execution, but they can be configured after the Sway program has been built. The Rust and TS SDKs allow updating the values of these constants by injecting new values for them directly in the bytecode without having to build the program again. These are useful for contract factories and behave somewhat similarly to immutable
variables from languages like Solidity.
Configurable constants are declared inside a configurable
block and require a type ascription and an initializer as follows:
configurable {
U8: u8 = 8u8,
BOOL: bool = true,
ARRAY: [u32; 3] = [253u32, 254u32, 255u32],
STR_4: str[4] = __to_str_array("fuel"),
STRUCT: StructWithGeneric<u8> = StructWithGeneric {
field_1: 8u8,
field_2: 16,
},
ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true),
}
At most one configurable
block is allowed in a Sway project. Moreover, configurable
blocks are not allowed in libraries.
Configurable constants can be read directly just like regular constants:
fn return_configurables() -> (u8, bool, [u32; 3], str[4], StructWithGeneric<u8>) {
(U8, BOOL, ARRAY, STR_4, STRUCT)
}
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
.
fn log_values(){
// Generates a Log receipt
log(42);
// Generates a LogData receipt
let string = "sway";
log(string);
}
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 non_reference types; and for non-reference types bigger than 64-bit integers, for example, u256
;
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. Unlike an if
expression, a match
expression asserts at compile time that all possible patterns have been matched. If you don't handle all the patterns, you will get compiler error indicating that your match
expression is non-exhaustive.
The basic syntax of a match
expression is as follows:
let result = match expression {
pattern1 => code_to_execute_if_expression_matches_pattern1,
pattern2 => code_to_execute_if_expression_matches_pattern2,
pattern3 | pattern4 => code_to_execute_if_expression_matches_pattern3_or_pattern4
...
_ => code_to_execute_if_expression_matches_no_pattern,
}
Some examples of how you can use a match
expression:
script;
// helper functions for our example
fn on_even(num: u64) {
// do something with even numbers
}
fn on_odd(num: u64) {
// do something with odd numbers
}
fn main(num: u64) -> u64 {
// Match as an expression
let is_even = match num % 2 {
0 => true,
_ => false,
};
// Match as control flow
let x = 12;
match x {
5 => on_odd(x),
_ => on_even(x),
};
// Match an enum
enum Weather {
Sunny: (),
Rainy: (),
Cloudy: (),
Snowy: (),
}
let current_weather = Weather::Sunny;
let avg_temp = match current_weather {
Weather::Sunny => 80,
Weather::Rainy => 50,
Weather::Cloudy => 60,
Weather::Snowy => 20,
};
let is_sunny = match current_weather {
Weather::Sunny => true,
Weather::Rainy | Weather::Cloudy | Weather::Snowy => false,
};
// match expression used for a return
let outside_temp = Weather::Sunny;
match outside_temp {
Weather::Sunny => 80,
Weather::Rainy => 50,
Weather::Cloudy => 60,
Weather::Snowy => 20,
}
}
Loops
while
This is what a while
loop looks 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.
for
This is what a for
loop that computes the sum of a vector of numbers looks like:
for element in vector.iter() {
sum += element;
}
You need the for
keyword, some pattern that contains variable names such as element
in this case, the ìn
keyword followed by an iterator, and a block of code inside the curly braces ({...}
) to execute each iteration. vector.iter()
in the example above returns an iterator for the vector
. In each iteration, the value of element
is updated with the next value in the iterator until the end of the vector is reached and the for
loop iteration ends.
break
and continue
break
and continue
keywords are available to use inside the body of a while
or for
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
- External Code Execution
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 std::hash::*;
impl Hash for Location {
fn hash(self, ref mut state: Hasher) {
match self {
Location::Earth => {
0_u8.hash(state);
}
Location::Mars => {
1_u8.hash(state);
}
}
}
}
impl Hash for Stats {
fn hash(self, ref mut state: Hasher) {
self.strength.hash(state);
self.agility.hash(state);
}
}
impl Hash for Person {
fn hash(self, ref mut state: Hasher) {
self.name.hash(state);
self.age.hash(state);
self.alive.hash(state);
self.location.hash(state);
self.stats.hash(state);
self.some_tuple.hash(state);
self.some_array.hash(state);
self.some_b256.hash(state);
}
}
const VALUE_A = 0x9280359a3b96819889d30614068715d634ad0cf9bba70c0f430a8c201138f79f;
enum Location {
Earth: (),
Mars: (),
}
struct Person {
name: str,
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}};
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 Ok(address) = result_address {
log(address.bits());
} 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 that 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
block that contains a list of all your variables, their types, and their initial values. The initial value can be any expression that can be evaluated to a constant during compilation, as follows:
storage {
var1: u64 = 1,
var2: b256 = b256::zero(),
var3: Address = Address::zero(),
var4: Option<u8> = None,
}
To write into a storage variable, you need to use the storage
keyword as follows:
storage.var1.write(42);
storage
.var2
.write(0x1111111111111111111111111111111111111111111111111111111111111111);
storage
.var3
.write(Address::from(0x1111111111111111111111111111111111111111111111111111111111111111));
storage.var4.write(Some(2u8));
To read a storage variable, you also need to use the storage
keyword. You may use read()
or try_read()
, however we recommend using try_read()
for additional safety.
let var1: u64 = storage.var1.read();
let var2: b256 = storage.var2.try_read().unwrap_or(b256::zero());
let var3: Address = storage.var3.try_read().unwrap_or(Address::zero());
let var4: Option<u8> = storage.var4.try_read().unwrap_or(None);
Storing Structs
To store a struct in storage, each variable must be assigned in the storage
block. This can be either my assigning the fields individually or using a public constructor that can be evaluated to a constant during compilation.
struct Type1 {
x: u64,
y: u64,
}
struct Type2 {
w: b256,
z: bool,
}
impl Type2 {
// a constructor that evaluates to a constant during compilation
fn default() -> Self {
Self {
w: 0x0000000000000000000000000000000000000000000000000000000000000000,
z: true,
}
}
}
storage {
var1: Type1 = Type1 { x: 0, y: 0 },
var2: Type2 = Type2::default(),
}
You may write to both fields of a struct and the entire struct as follows:
// Store individual fields
storage.var1.x.write(42);
storage.var1.y.write(77);
// Store an entire struct
let new_struct = Type2 {
w: 0x1111111111111111111111111111111111111111111111111111111111111111,
z: false,
};
storage.var2.write(new_struct);
The same applies to reading structs from storage, where both the individual and struct as a whole may be read as follows:
let var1_x: u64 = storage.var1.x.try_read().unwrap_or(0);
let var1_y: u64 = storage.var1.y.try_read().unwrap_or(0);
let var2: Type2 = storage.var2.try_read().unwrap_or(Type2::default());
Common Storage Collections
We support the following common storage collections:
StorageMap<K, V>
StorageVec<T>
StorageBytes
StorageString
Please note that these types are not initialized during compilation. This means that if you try to access a key from a storage map before the storage has been set, for example, the call will revert.
Declaring these variables in storage requires a storage
block as follows:
storage {
storage_map: StorageMap<u64, bool> = StorageMap {},
storage_vec: StorageVec<b256> = StorageVec {},
storage_string: StorageString = StorageString {},
storage_bytes: StorageBytes = StorageBytes {},
}
StorageMaps<K, V>
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>
.
Warning While the StorageMap<K, V>
is currently included in the prelude, to use it the Hash
trait must still be imported. This is a known issue and will be resolved.
use std::hash::Hash;
use std::storage::storage_vec::*;
use std::storage::storage_bytes::*;
use std::storage::storage_string::*;
storage {
storage_map: StorageMap<u64, bool> = StorageMap {},
storage_vec: StorageVec<b256> = StorageVec {},
storage_string: StorageString = StorageString {},
storage_bytes: StorageBytes = StorageBytes {},
}
abi StorageExample {
#[storage(write)]
fn store_map();
#[storage(read)]
fn get_map();
#[storage(write)]
fn store_vec();
#[storage(read, write)]
fn get_vec();
#[storage(write)]
fn store_string();
#[storage(read)]
fn get_string();
#[storage(write)]
fn store_bytes();
#[storage(read)]
fn get_bytes();
}
impl StorageExample for Contract {
#[storage(write)]
fn store_map() {
storage.storage_map.insert(12, true);
storage.storage_map.insert(59, false);
// try_insert() will only insert if a value does not already exist for a key.
let result = storage.storage_map.try_insert(103, true);
assert(result.is_ok());
}
#[storage(read)]
fn get_map() {
// Access directly
let stored_val1: bool = storage.storage_map.get(12).try_read().unwrap_or(false);
// First get the storage key and then access the value.
let storage_key2: StorageKey<bool> = storage.storage_map.get(59);
let stored_val2: bool = storage_key2.try_read().unwrap_or(false);
// Unsafely access the value.
let stored_val3: bool = storage.storage_map.get(103).read();
}
#[storage(write)]
fn store_vec() {
storage
.storage_vec
.push(0x1111111111111111111111111111111111111111111111111111111111111111);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000001);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000002);
// Set will overwrite the element stored at the given index.
storage.storage_vec.set(2, b256::zero());
}
#[storage(read, write)]
fn get_vec() {
// Method 1: Access the element directly
// Note: get() does not remove the element from the vec.
let stored_val1: b256 = storage.storage_vec.get(0).unwrap().try_read().unwrap_or(b256::zero());
// Method 2: First get the storage key and then access the value.
let storage_key2: StorageKey<b256> = storage.storage_vec.get(1).unwrap();
let stored_val2: b256 = storage_key2.try_read().unwrap_or(b256::zero());
// pop() will remove the last element from the vec.
let length: u64 = storage.storage_vec.len();
let stored_val3: b256 = storage.storage_vec.pop().unwrap();
assert(length != storage.storage_vec.len());
}
#[storage(write)]
fn store_string() {
let my_string = String::from_ascii_str("Fuel is blazingly fast");
storage.storage_string.write_slice(my_string);
}
#[storage(read)]
fn get_string() {
let stored_string: String = storage.storage_string.read_slice().unwrap();
}
#[storage(write)]
fn store_bytes() {
// Setup Bytes
let mut my_bytes = Bytes::new();
my_bytes.push(1u8);
my_bytes.push(2u8);
my_bytes.push(3u8);
// Write to storage
storage.storage_bytes.write_slice(my_bytes);
}
#[storage(read)]
fn get_bytes() {
let stored_bytes: Bytes = storage.storage_bytes.read_slice().unwrap();
}
}
To write to a storage map, call either the insert()
or try_insert()
functions as follows:
storage.storage_map.insert(12, true);
storage.storage_map.insert(59, false);
// try_insert() will only insert if a value does not already exist for a key.
let result = storage.storage_map.try_insert(103, true);
assert(result.is_ok());
The following demonstrates how to read from a storage map:
// Access directly
let stored_val1: bool = storage.storage_map.get(12).try_read().unwrap_or(false);
// First get the storage key and then access the value.
let storage_key2: StorageKey<bool> = storage.storage_map.get(59);
let stored_val2: bool = storage_key2.try_read().unwrap_or(false);
// Unsafely access the value.
let stored_val3: bool = storage.storage_map.get(103).read();
StorageVec<T>
Generic storage vectors are available in the standard library as StorageVec<T>
which have to be defined inside a storage
block and allow you to call push()
and pop()
to push and pop values from a vector respectively. Refer to Storage Vector for more information about StorageVec<T>
.
The following demonstrates how to import StorageVec<T>
:
use std::storage::storage_vec::*;
use std::storage::storage_bytes::*;
use std::storage::storage_string::*;
storage {
storage_map: StorageMap<u64, bool> = StorageMap {},
storage_vec: StorageVec<b256> = StorageVec {},
storage_string: StorageString = StorageString {},
storage_bytes: StorageBytes = StorageBytes {},
}
abi StorageExample {
#[storage(write)]
fn store_map();
#[storage(read)]
fn get_map();
#[storage(write)]
fn store_vec();
#[storage(read, write)]
fn get_vec();
#[storage(write)]
fn store_string();
#[storage(read)]
fn get_string();
#[storage(write)]
fn store_bytes();
#[storage(read)]
fn get_bytes();
}
impl StorageExample for Contract {
#[storage(write)]
fn store_map() {
storage.storage_map.insert(12, true);
storage.storage_map.insert(59, false);
// try_insert() will only insert if a value does not already exist for a key.
let result = storage.storage_map.try_insert(103, true);
assert(result.is_ok());
}
#[storage(read)]
fn get_map() {
// Access directly
let stored_val1: bool = storage.storage_map.get(12).try_read().unwrap_or(false);
// First get the storage key and then access the value.
let storage_key2: StorageKey<bool> = storage.storage_map.get(59);
let stored_val2: bool = storage_key2.try_read().unwrap_or(false);
// Unsafely access the value.
let stored_val3: bool = storage.storage_map.get(103).read();
}
#[storage(write)]
fn store_vec() {
storage
.storage_vec
.push(0x1111111111111111111111111111111111111111111111111111111111111111);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000001);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000002);
// Set will overwrite the element stored at the given index.
storage.storage_vec.set(2, b256::zero());
}
#[storage(read, write)]
fn get_vec() {
// Method 1: Access the element directly
// Note: get() does not remove the element from the vec.
let stored_val1: b256 = storage.storage_vec.get(0).unwrap().try_read().unwrap_or(b256::zero());
// Method 2: First get the storage key and then access the value.
let storage_key2: StorageKey<b256> = storage.storage_vec.get(1).unwrap();
let stored_val2: b256 = storage_key2.try_read().unwrap_or(b256::zero());
// pop() will remove the last element from the vec.
let length: u64 = storage.storage_vec.len();
let stored_val3: b256 = storage.storage_vec.pop().unwrap();
assert(length != storage.storage_vec.len());
}
#[storage(write)]
fn store_string() {
let my_string = String::from_ascii_str("Fuel is blazingly fast");
storage.storage_string.write_slice(my_string);
}
#[storage(read)]
fn get_string() {
let stored_string: String = storage.storage_string.read_slice().unwrap();
}
#[storage(write)]
fn store_bytes() {
// Setup Bytes
let mut my_bytes = Bytes::new();
my_bytes.push(1u8);
my_bytes.push(2u8);
my_bytes.push(3u8);
// Write to storage
storage.storage_bytes.write_slice(my_bytes);
}
#[storage(read)]
fn get_bytes() {
let stored_bytes: Bytes = storage.storage_bytes.read_slice().unwrap();
}
}
NOTE: When importing the
StorageVec<T>
, please be sure to use the glob operator:use std::storage::storage_vec::*
.
The following demonstrates how to write to a StorageVec<T>
:
storage
.storage_vec
.push(0x1111111111111111111111111111111111111111111111111111111111111111);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000001);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000002);
// Set will overwrite the element stored at the given index.
storage.storage_vec.set(2, b256::zero());
The following demonstrates how to read from a StorageVec<T>
:
// Method 1: Access the element directly
// Note: get() does not remove the element from the vec.
let stored_val1: b256 = storage.storage_vec.get(0).unwrap().try_read().unwrap_or(b256::zero());
// Method 2: First get the storage key and then access the value.
let storage_key2: StorageKey<b256> = storage.storage_vec.get(1).unwrap();
let stored_val2: b256 = storage_key2.try_read().unwrap_or(b256::zero());
// pop() will remove the last element from the vec.
let length: u64 = storage.storage_vec.len();
let stored_val3: b256 = storage.storage_vec.pop().unwrap();
assert(length != storage.storage_vec.len());
StorageBytes
Storage of Bytes
is available in the standard library as StorageBytes
which have to be defined inside a storage
block. StorageBytes
cannot be manipulated in the same way a StorageVec<T>
or StorageMap<K, V>
can but stores bytes more efficiently thus reducing gas. Only the entirety of a Bytes
may be read/written to storage. This means any changes would require loading the entire Bytes
to the heap, making changes, and then storing it once again. If frequent changes are needed, a StorageVec<u8>
is recommended.
The following demonstrates how to import StorageBytes
:
use std::storage::storage_bytes::*;
use std::storage::storage_string::*;
storage {
storage_map: StorageMap<u64, bool> = StorageMap {},
storage_vec: StorageVec<b256> = StorageVec {},
storage_string: StorageString = StorageString {},
storage_bytes: StorageBytes = StorageBytes {},
}
abi StorageExample {
#[storage(write)]
fn store_map();
#[storage(read)]
fn get_map();
#[storage(write)]
fn store_vec();
#[storage(read, write)]
fn get_vec();
#[storage(write)]
fn store_string();
#[storage(read)]
fn get_string();
#[storage(write)]
fn store_bytes();
#[storage(read)]
fn get_bytes();
}
impl StorageExample for Contract {
#[storage(write)]
fn store_map() {
storage.storage_map.insert(12, true);
storage.storage_map.insert(59, false);
// try_insert() will only insert if a value does not already exist for a key.
let result = storage.storage_map.try_insert(103, true);
assert(result.is_ok());
}
#[storage(read)]
fn get_map() {
// Access directly
let stored_val1: bool = storage.storage_map.get(12).try_read().unwrap_or(false);
// First get the storage key and then access the value.
let storage_key2: StorageKey<bool> = storage.storage_map.get(59);
let stored_val2: bool = storage_key2.try_read().unwrap_or(false);
// Unsafely access the value.
let stored_val3: bool = storage.storage_map.get(103).read();
}
#[storage(write)]
fn store_vec() {
storage
.storage_vec
.push(0x1111111111111111111111111111111111111111111111111111111111111111);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000001);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000002);
// Set will overwrite the element stored at the given index.
storage.storage_vec.set(2, b256::zero());
}
#[storage(read, write)]
fn get_vec() {
// Method 1: Access the element directly
// Note: get() does not remove the element from the vec.
let stored_val1: b256 = storage.storage_vec.get(0).unwrap().try_read().unwrap_or(b256::zero());
// Method 2: First get the storage key and then access the value.
let storage_key2: StorageKey<b256> = storage.storage_vec.get(1).unwrap();
let stored_val2: b256 = storage_key2.try_read().unwrap_or(b256::zero());
// pop() will remove the last element from the vec.
let length: u64 = storage.storage_vec.len();
let stored_val3: b256 = storage.storage_vec.pop().unwrap();
assert(length != storage.storage_vec.len());
}
#[storage(write)]
fn store_string() {
let my_string = String::from_ascii_str("Fuel is blazingly fast");
storage.storage_string.write_slice(my_string);
}
#[storage(read)]
fn get_string() {
let stored_string: String = storage.storage_string.read_slice().unwrap();
}
#[storage(write)]
fn store_bytes() {
// Setup Bytes
let mut my_bytes = Bytes::new();
my_bytes.push(1u8);
my_bytes.push(2u8);
my_bytes.push(3u8);
// Write to storage
storage.storage_bytes.write_slice(my_bytes);
}
#[storage(read)]
fn get_bytes() {
let stored_bytes: Bytes = storage.storage_bytes.read_slice().unwrap();
}
}
NOTE: When importing the
StorageBytes
, please be sure to use the glob operator:use std::storage::storage_bytes::*
.
The following demonstrates how to write to a StorageBytes
:
// Setup Bytes
let mut my_bytes = Bytes::new();
my_bytes.push(1u8);
my_bytes.push(2u8);
my_bytes.push(3u8);
// Write to storage
storage.storage_bytes.write_slice(my_bytes);
The following demonstrates how to read from a StorageBytes
:
let stored_bytes: Bytes = storage.storage_bytes.read_slice().unwrap();
StorageString
Storage of String
is available in the standard library as StorageString
which have to be defined inside a storage
block. StorageString
cannot be manipulated in the same way a StorageVec<T>
or StorageMap<K, V>
. Only the entirety of a String
may be read/written to storage.
The following demonstrates how to import StorageString
:
use std::storage::storage_string::*;
storage {
storage_map: StorageMap<u64, bool> = StorageMap {},
storage_vec: StorageVec<b256> = StorageVec {},
storage_string: StorageString = StorageString {},
storage_bytes: StorageBytes = StorageBytes {},
}
abi StorageExample {
#[storage(write)]
fn store_map();
#[storage(read)]
fn get_map();
#[storage(write)]
fn store_vec();
#[storage(read, write)]
fn get_vec();
#[storage(write)]
fn store_string();
#[storage(read)]
fn get_string();
#[storage(write)]
fn store_bytes();
#[storage(read)]
fn get_bytes();
}
impl StorageExample for Contract {
#[storage(write)]
fn store_map() {
storage.storage_map.insert(12, true);
storage.storage_map.insert(59, false);
// try_insert() will only insert if a value does not already exist for a key.
let result = storage.storage_map.try_insert(103, true);
assert(result.is_ok());
}
#[storage(read)]
fn get_map() {
// Access directly
let stored_val1: bool = storage.storage_map.get(12).try_read().unwrap_or(false);
// First get the storage key and then access the value.
let storage_key2: StorageKey<bool> = storage.storage_map.get(59);
let stored_val2: bool = storage_key2.try_read().unwrap_or(false);
// Unsafely access the value.
let stored_val3: bool = storage.storage_map.get(103).read();
}
#[storage(write)]
fn store_vec() {
storage
.storage_vec
.push(0x1111111111111111111111111111111111111111111111111111111111111111);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000001);
storage
.storage_vec
.push(0x0000000000000000000000000000000000000000000000000000000000000002);
// Set will overwrite the element stored at the given index.
storage.storage_vec.set(2, b256::zero());
}
#[storage(read, write)]
fn get_vec() {
// Method 1: Access the element directly
// Note: get() does not remove the element from the vec.
let stored_val1: b256 = storage.storage_vec.get(0).unwrap().try_read().unwrap_or(b256::zero());
// Method 2: First get the storage key and then access the value.
let storage_key2: StorageKey<b256> = storage.storage_vec.get(1).unwrap();
let stored_val2: b256 = storage_key2.try_read().unwrap_or(b256::zero());
// pop() will remove the last element from the vec.
let length: u64 = storage.storage_vec.len();
let stored_val3: b256 = storage.storage_vec.pop().unwrap();
assert(length != storage.storage_vec.len());
}
#[storage(write)]
fn store_string() {
let my_string = String::from_ascii_str("Fuel is blazingly fast");
storage.storage_string.write_slice(my_string);
}
#[storage(read)]
fn get_string() {
let stored_string: String = storage.storage_string.read_slice().unwrap();
}
#[storage(write)]
fn store_bytes() {
// Setup Bytes
let mut my_bytes = Bytes::new();
my_bytes.push(1u8);
my_bytes.push(2u8);
my_bytes.push(3u8);
// Write to storage
storage.storage_bytes.write_slice(my_bytes);
}
#[storage(read)]
fn get_bytes() {
let stored_bytes: Bytes = storage.storage_bytes.read_slice().unwrap();
}
}
NOTE: When importing the
StorageString
, please be sure to use the glob operator:use std::storage::storage_string::*
.
The following demonstrates how to write to a StorageString
:
let my_string = String::from_ascii_str("Fuel is blazingly fast");
storage.storage_string.write_slice(my_string);
The following demonstrates how to read from a StorageString
:
let stored_string: String = storage.storage_string.read_slice().unwrap();
Advanced Storage
For more advanced storage techniques please refer to the Advanced Storage page.
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 {
...
}
Note: the
#[storage(write)]
attribute also permits a function to read from storage. This is due to the fact that partially writing a storage slot requires first reading the slot.
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 Assets
The FuelVM has built-in support for working with multiple assets.
Key Differences Between EVM and FuelVM Assets
ERC-20 vs Native Asset
On the EVM, Ether is the native asset. As such, sending ETH to an address or contract is an operation built into the EVM, meaning it doesn't rely on the existence of a smart contract to update balances to track ownership as with ERC-20 tokens.
On the FuelVM, all assets are native and the process for sending any native asset is the same.
While you would still need a smart contract to handle the minting and burning of assets, the sending and receiving of these assets can be done independently of the asset contract.
Just like the EVM however, Fuel has a standard that describes a standard API for Native Assets using the Sway Language. The ERC-20 equivalent for the Sway Language is the SRC-20; Native Asset Standard.
NOTE It is important to note that Fuel does not have tokens.
ERC-721 vs Native Asset
On the EVM, an ERC-721 token or NFT is a contract that contains multiple tokens which are non-fungible with one another.
On the FuelVM, the ERC-721 equivalent is a Native Asset where each asset has a supply of one. This is defined in the SRC-20; Native Asset Standard under the Non-Fungible Asset Restrictions.
In practice, this means all NFTs are treated the same as any other Native Asset on Fuel. When writing Sway code, no additional cases for handling non-fungible and fungible assets are required.
No Token Approvals
An advantage Native Assets bring is that there is no need for token approvals; as with Ether on the EVM. With millions of dollars hacked every year due to misused token approvals, the FuelVM eliminates this attack vector.
Asset vs Coin vs Token
An "Asset" is a Native Asset on Fuel and has the associated AssetId
type. Assets are distinguishable from one another. A "Coin" represents a singular unit of an Asset. Coins of the same Asset are not distinguishable from one another.
Fuel does not use tokens like other ecosystems such as Ethereum and uses Native Assets with a UTXO design instead.
The AssetId
type
The AssetId
type represents any Native Asset on Fuel. An AssetId
is used for interacting with an asset on the network.
The AssetId
of any Native Asset on Fuel is calculated by taking the SHA256 hash digest of the originating ContractId
that minted the asset and a SubId
i.e. sha256((contract_id, sub_id))
.
Creating a New AssetId
There are 3 ways to instantiate a new AssetId
:
Default
When a contract will only ever mint a single asset, it is recommended to use the DEFAULT_ASSET_ID
sub id. This is referred to as the default asset of a contract.
To get the default asset from an internal contract call, call the default()
function:
let asset_id: AssetId = AssetId::default();
New
If a contract mints multiple assets or if the asset has been minted by an external contract, the new()
function will be needed. The new()
function takes the ContractId
of the contract which minted the token as well as a SubId
.
To create a new AssetId
using a ContractId
and SubId
, call the new()
function:
let my_contract_id: ContractId = ContractId::from(0x1000000000000000000000000000000000000000000000000000000000000000);
let my_sub_id: SubId = 0x2000000000000000000000000000000000000000000000000000000000000000;
let asset_id: AssetId = AssetId::new(my_contract_id, my_sub_id);
From
In the case where the b256
value of an asset is already known, you may call the from()
function with the b256
value.
let asset_id: AssetId = AssetId::from(0x0000000000000000000000000000000000000000000000000000000000000000);
The SubId
type
The SubId is used to differentiate between different assets that are created by the same contract. The SubId
is a b256
value.
When creating a single new asset on Fuel, we recommend using the DEFAULT_SUB_ID
or SubId::zero()
.
The Base Asset
On the Fuel Network, the base asset is Ether. This is the only asset on the Fuel Network that does not have a SubId
.
The Base Asset can be returned anytime by calling the base()
function of the AssetId
type.
let base_asset: AssetId = AssetId::base();
Basic Native Asset Functionality
Minting A Native Asset
To mint a new asset, the std::asset::mint()
function must be called internally within a contract. A SubId
and amount of coins must be provided. These newly minted coins will be owned by the contract which minted them. To mint another asset from the same contract, replace the DEFAULT_SUB_ID
with your desired SubId
.
mint(DEFAULT_SUB_ID, mint_amount);
You may also mint an asset to a specific entity with the std::asset::mint_to()
function. Be sure to provide a target Identity
that will own the newly minted coins.
mint_to(target_identity, DEFAULT_SUB_ID, mint_amount);
If you intend to allow external users to mint assets using your contract, the SRC-3; Mint and Burn Standard defines a standard API for minting assets. The Sway-Libs Asset Library also provides an additional library to support implementations of the SRC-3 Standard into your contract.
Burning a Native Asset
To burn an asset, the std::asset::burn()
function must be called internally from the contract which minted them. The SubId
used to mint the coins and amount must be provided. The burned coins must be owned by the contract. When an asset is burned it doesn't exist anymore.
burn(DEFAULT_SUB_ID, burn_amount);
If you intend to allow external users to burn assets using your contract, the SRC-3; Mint and Burn Standard defines a standard API for burning assets. The Sway-Libs Asset Library also provides an additional library to support implementations of the SRC-3 Standard into your contract.
Transfer a Native Asset
To internally transfer a Native Asset, the std::asset::transfer()
function must be called. A target Identity
or user must be provided as well as the AssetId
of the asset and an amount.
transfer(target, asset_id, coins);
Native Asset And Transactions
Getting The Transaction Asset
To query for the Native Asset sent in a transaction, you may call the std::call_frames::msg_asset_id()
function.
let amount = msg_asset_id();
Getting The Transaction Amount
To query for the amount of coins sent in a transaction, you may call the std::context::msg_amount()
function.
let amount = msg_amount();
Native Assets and Contracts
Checking A Contract's Balance
To internally check a contract's balance, call the std::context::this_balance()
function with the corresponding AssetId
.
this_balance(asset_id)
To check the balance of an external contract, call the std::context::balance_of()
function with the corresponding AssetId
.
balance_of(target_contract, asset_id)
NOTE Due to the FuelVM's UTXO design, balances of
Address
's cannot be returned in the Sway Language. This must be done off-chain using the SDK.
Receiving Native Assets In A Contract
By default, a contract may not receive a Native Asset in a contract call. To allow transferring of assets to the contract, add the #[payable]
attribute to the function.
#[payable]
fn deposit() {
assert(msg_amount() > 0);
}
Native Asset Standards
There are a number of standards developed to enable further functionality for Native Assets and help cross contract functionality. Information on standards can be found in the Sway Standards Repo.
We currently have the following standards for Native Assets:
- SRC-20; Native Asset Standard defines the implementation of a standard API for Native Assets using the Sway Language.
- SRC-3; Mint and Burn Standard is used to enable mint and burn functionality for Native Assets.
- SRC-7; Arbitrary Asset Metadata Standard is used to store metadata for Native Assets.
- SRC-6; Vault Standard defines the implementation of a standard API for asset vaults developed in Sway.
Native Asset Libraries
Additional Libraries have been developed to allow you to quickly create an deploy dApps that follow the Sway Standards.
- Asset Library provides functionality to implement the SRC-20; Native Asset Standard, SRC-3; Mint and Burn Standard, and SRC-7; Arbitrary Asset Metadata Standard standards.
Single Native Asset Example
In this fully fleshed out example, we show a native asset contract which mints a single asset. This is the equivalent to the ERC-20 Standard use in Ethereum. Note there are no token approval functions.
It implements the SRC-20; Native Asset, SRC-3; Mint and Burn, and SRC-5; Ownership standards. It does not use any external libraries.
// ERC20 equivalent in Sway.
contract;
use src3::SRC3;
use src5::{SRC5, State, AccessError};
use src20::SRC20;
use std::{
asset::{
burn,
mint_to,
},
call_frames::{
contract_id,
msg_asset_id,
},
constants::DEFAULT_SUB_ID,
context::msg_amount,
string::String,
};
configurable {
DECIMALS: u8 = 9u8,
NAME: str[7] = __to_str_array("MyAsset"),
SYMBOL: str[5] = __to_str_array("MYTKN"),
}
storage {
total_supply: u64 = 0,
owner: State = State::Uninitialized,
}
// Native Asset Standard
impl SRC20 for Contract {
#[storage(read)]
fn total_assets() -> u64 {
1
}
#[storage(read)]
fn total_supply(asset: AssetId) -> Option<u64> {
if asset == AssetId::default() {
Some(storage.total_supply.read())
} else {
None
}
}
#[storage(read)]
fn name(asset: AssetId) -> Option<String> {
if asset == AssetId::default() {
Some(String::from_ascii_str(from_str_array(NAME)))
} else {
None
}
}
#[storage(read)]
fn symbol(asset: AssetId) -> Option<String> {
if asset == AssetId::default() {
Some(String::from_ascii_str(from_str_array(SYMBOL)))
} else {
None
}
}
#[storage(read)]
fn decimals(asset: AssetId) -> Option<u8> {
if asset == AssetId::default() {
Some(DECIMALS)
} else {
None
}
}
}
// Ownership Standard
impl SRC5 for Contract {
#[storage(read)]
fn owner() -> State {
storage.owner.read()
}
}
// Mint and Burn Standard
impl SRC3 for Contract {
#[storage(read, write)]
fn mint(recipient: Identity, sub_id: SubId, amount: u64) {
require(sub_id == DEFAULT_SUB_ID, "incorrect-sub-id");
require_access_owner();
storage
.total_supply
.write(amount + storage.total_supply.read());
mint_to(recipient, DEFAULT_SUB_ID, amount);
}
#[storage(read, write)]
fn burn(sub_id: SubId, amount: u64) {
require(sub_id == DEFAULT_SUB_ID, "incorrect-sub-id");
require(msg_amount() >= amount, "incorrect-amount-provided");
require(
msg_asset_id() == AssetId::default(),
"incorrect-asset-provided",
);
require_access_owner();
storage
.total_supply
.write(storage.total_supply.read() - amount);
burn(DEFAULT_SUB_ID, amount);
}
}
abi SingleAsset {
#[storage(read, write)]
fn constructor(owner_: Identity);
}
impl SingleAsset for Contract {
#[storage(read, write)]
fn constructor(owner_: Identity) {
require(storage.owner.read() == State::Uninitialized, "owner-initialized");
storage.owner.write(State::Initialized(owner_));
}
}
#[storage(read)]
fn require_access_owner() {
require(
storage.owner.read() == State::Initialized(msg_sender().unwrap()),
AccessError::NotOwner,
);
}
Multi Native Asset Example
In this fully fleshed out example, we show a native asset contract which mints multiple assets. This is the equivalent to the ERC-1155 Standard use in Ethereum. Note there are no token approval functions.
It implements the SRC-20; Native Asset, SRC-3; Mint and Burn, and SRC-5; Ownership standards. It does not use any external libraries.
// ERC1155 equivalent in Sway.
contract;
use src5::{SRC5, State, AccessError};
use src20::SRC20;
use src3::SRC3;
use std::{
asset::{
burn,
mint_to,
},
call_frames::{
contract_id,
msg_asset_id,
},
hash::{
Hash,
},
context::this_balance,
storage::storage_string::*,
string::String
};
storage {
total_assets: u64 = 0,
total_supply: StorageMap<AssetId, u64> = StorageMap {},
name: StorageMap<AssetId, StorageString> = StorageMap {},
symbol: StorageMap<AssetId, StorageString> = StorageMap {},
decimals: StorageMap<AssetId, u8> = StorageMap {},
owner: State = State::Uninitialized,
}
// Native Asset Standard
impl SRC20 for Contract {
#[storage(read)]
fn total_assets() -> u64 {
storage.total_assets.read()
}
#[storage(read)]
fn total_supply(asset: AssetId) -> Option<u64> {
storage.total_supply.get(asset).try_read()
}
#[storage(read)]
fn name(asset: AssetId) -> Option<String> {
storage.name.get(asset).read_slice()
}
#[storage(read)]
fn symbol(asset: AssetId) -> Option<String> {
storage.symbol.get(asset).read_slice()
}
#[storage(read)]
fn decimals(asset: AssetId) -> Option<u8> {
storage.decimals.get(asset).try_read()
}
}
// Mint and Burn Standard
impl SRC3 for Contract {
#[storage(read, write)]
fn mint(recipient: Identity, sub_id: SubId, amount: u64) {
require_access_owner();
let asset_id = AssetId::new(contract_id(), sub_id);
let supply = storage.total_supply.get(asset_id).try_read();
if supply.is_none() {
storage.total_assets.write(storage.total_assets.try_read().unwrap_or(0) + 1);
}
let current_supply = supply.unwrap_or(0);
storage.total_supply.insert(asset_id, current_supply + amount);
mint_to(recipient, sub_id, amount);
}
#[storage(read, write)]
fn burn(sub_id: SubId, amount: u64) {
require_access_owner();
let asset_id = AssetId::new(contract_id(), sub_id);
require(this_balance(asset_id) >= amount, "not-enough-coins");
let supply = storage.total_supply.get(asset_id).try_read();
let current_supply = supply.unwrap_or(0);
storage.total_supply.insert(asset_id, current_supply - amount);
burn(sub_id, amount);
}
}
abi MultiAsset {
#[storage(read, write)]
fn constructor(owner_: Identity);
#[storage(read, write)]
fn set_name(asset: AssetId, name: String);
#[storage(read, write)]
fn set_symbol(asset: AssetId, symbol: String);
#[storage(read, write)]
fn set_decimals(asset: AssetId, decimals: u8);
}
impl MultiAsset for Contract {
#[storage(read, write)]
fn constructor(owner_: Identity) {
require(storage.owner.read() == State::Uninitialized, "owner-initialized");
storage.owner.write(State::Initialized(owner_));
}
#[storage(read, write)]
fn set_name(asset: AssetId, name: String) {
require_access_owner();
storage.name.insert(asset, StorageString {});
storage.name.get(asset).write_slice(name);
}
#[storage(read, write)]
fn set_symbol(asset: AssetId, symbol: String) {
require_access_owner();
storage.symbol.insert(asset, StorageString {});
storage.symbol.get(asset).write_slice(symbol);
}
#[storage(read, write)]
fn set_decimals(asset: AssetId, decimals: u8) {
require_access_owner();
storage.decimals.insert(asset, decimals);
}
}
#[storage(read)]
fn require_access_owner() {
require(
storage.owner.read() == State::Initialized(msg_sender().unwrap()),
AccessError::NotOwner,
);
}
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;
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 = msg_sender().unwrap();
if let Identity::Address(addr) = sender {
assert(addr == OWNER);
} else {
revert(0);
}
true
}
}
The msg_sender
function works as follows:
- If the caller is a contract, then
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
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
Err(AuthError)
is returned.
Contract Ownership
Many contracts require some form of ownership for access control. The SRC-5 Ownership Standard has been defined to provide an interoperable interface for ownership within contracts.
To accomplish this, use the Ownership Library 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 b256::zero()
or otherwise.
- The following is an example of how to properly lock a function such that only the owner may call a function:
#[storage(read)]
fn only_owner() {
storage.owner.only_owner();
// Do stuff here
}
Setting ownership can be done in one of two ways; During compile time or run time.
- The following is an example of how to properly set ownership of a contract during compile time:
storage {
owner: Ownership = Ownership::initialized(Identity::Address(Address::zero())),
}
- The following is an example of how to properly set ownership of a contract during run time:
#[storage(write)]
fn set_owner(identity: Identity) {
storage.owner.set_ownership(identity);
}
- The following is an example of how to properly revoke ownership of a contract:
#[storage(write)]
fn revoke_ownership() {
storage.owner.renounce_ownership();
}
- The following is an example of how to properly retrieve the state of ownership:
#[storage(read)]
fn owner() -> State {
storage.owner.owner()
}
Access Control Libraries
Sway-Libs provides the following libraries to enable further access control.
- Ownership Library; used to apply restrictions on functions such that only a single user may call them. This library provides helper functions for the SRC-5; Ownership Standard.
- Admin Library; used to apply restrictions on functions such that only a select few users may call them like a whitelist.
- Pausable Library; allows contracts to implement an emergency stop mechanism.
- Reentrancy Guard Library; used to detect and prevent reentrancy attacks.
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 (coins
) 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, coins: 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-libs
library. The guard will panic (revert) at run time if re-entrancy is detected.
contract;
use reentrancy::reentrancy_guard;
abi MyContract {
fn some_method();
}
impl ContractB for Contract {
fn some_method() {
reentrancy_guard();
// do something
}
}
CEI pattern violation static analysis
Another way of avoiding re-entrancy-related attacks is to follow the so-called CEI pattern. CEI stands for "Checks, Effects, Interactions", meaning that the contract code should first perform safety checks, also known as "pre-conditions", then perform effects, i.e. modify or read the contract storage and execute external contract calls (interaction) only at the very end of the function/method.
Please see this blog post for more detail on some vulnerabilities in case of storage modification after interaction and this blog post for more information on storage reads after interaction.
The Sway compiler implements a check that the CEI pattern is not violated in the user contract and issues warnings if that's the case.
For example, in the following contract the CEI pattern is violated, because an external contract call is executed before a storage write.
contract;
mod other_contract;
use other_contract::*;
use std::hash::*;
abi MyContract {
#[storage(read, write)]
fn withdraw(external_contract_id: ContractId);
}
storage {
balances: StorageMap<Identity, u64> = StorageMap::<Identity, u64> {},
}
impl MyContract for Contract {
#[storage(read, write)]
fn withdraw(external_contract_id: ContractId) {
let sender = msg_sender().unwrap();
let bal = storage.balances.get(sender).try_read().unwrap_or(0);
assert(bal > 0);
// External call
let caller = abi(OtherContract, external_contract_id.into());
caller.external_call {
coins: bal,
}();
// Storage update _after_ external call
storage.balances.insert(sender, 0);
}
}
Here, other_contract
is defined as follows:
library;
abi OtherContract {
#[payable]
fn external_call();
}
The CEI pattern analyzer issues a warning as follows, pointing to the interaction before a storage modification:
warning
--> /path/to/contract/main.sw:28:9
|
26 |
27 | let caller = abi(OtherContract, external_contract_id.into());
28 | caller.external_call { coins: bal }();
| _________-
29 | |
30 | | // Storage update _after_ external call
31 | | storage.balances.insert(sender, 0);
| |__________________________________________- Storage write after external contract interaction in function or method "withdraw". Consider making all storage writes before calling another contract
32 | }
33 | }
|
____
In case there is a storage read after an interaction, the CEI analyzer will issue a similar warning.
In addition to storage reads and writes after an interaction, the CEI analyzer reports analogous warnings about:
- balance tree updates, i.e. balance tree reads with subsequent writes, which may be produced by the
tr
andtro
ASM instructions or library functions using them under the hood; - balance trees reads with
bal
instruction; - changes to the output messages that can be produced by the
__smo
intrinsic function or thesmo
ASM instruction.
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.
Fallback
When a contract is compiled, a special section called "contract selection" is also generated. This section checks if the contract call method matches any of the available ABI methods. If this fails, one of two possible actions will happen:
1 - if no fallback function was specified, the contract will revert; 2 - otherwise, the fallback function will be called.
For all intents and purposes the fallback function is considered a contract method, which means that it has all the limitations that other contract methods have. As the fallback function signature, the function cannot have arguments, but they can return anything.
If for some reason the fallback function needs to returns different types, the intrinsic __contract_ret
can be used.
contract;
abi MyContract {
fn some_method();
}
impl ContractB for Contract {
fn some_method() {
}
}
#[fallback]
fn fallback() {
}
You may still access the method selector and arguments to the call in the fallback.
For instance, let's assume a function fn foobar(bool, u64) {}
gets called on a contract that doesn't have it with arguments true
and 42
.
It can execute the following fallback:
#[fallback]
fn fallback() {
// the method selector is the first four bytes of sha256("foobar(bool,u64)")
// per https://fuellabs.github.io/fuel-specs/master/protocol/abi#function-selector-encoding
let method_selector = std::call_frames::first_param::<u64>();
// the arguments tuple is (true, 42)
let arguments = std::call_frames::second_param::<(bool, u64)>();
}
External Code Execution
The std-lib
includes a function called run_external
that allows Sway contracts to execute arbitrary external Sway code.
This functionality enables features like upgradeable contracts and proxies.
Upgradeable Contracts
Upgradeable contracts are designed to allow the logic of a smart contract to be updated after deployment.
Consider this example proxy contract:
#[namespace(my_storage_namespace)]
storage {
target_contract: Option<ContractId> = None,
}
impl Proxy for Contract {
#[storage(write)]
fn set_target_contract(id: ContractId) {
storage.target_contract.write(Some(id));
}
#[storage(read)]
fn double_input(_value: u64) -> u64 {
let target = storage.target_contract.read().unwrap();
run_external(target)
}
}
The contract has two functions:
set_target_contract
updates thetarget_contract
variable in storage with theContractId
of an external contract.double_input
reads thetarget_contract
from storage and uses it to run external code. If thetarget_contract
has a function with the same name (double_input
), the code in the externaldouble_input
function will run. In this case, the function will return au64
.
Notice in the Proxy
example above, the storage block has a namespace
attribute. Using this attribute is considered a best practice for all proxy contracts in Sway, because it will prevent storage collisions with the implementation contract, as the implementation contract has access to both storage contexts.
Below is what an implementation contract could look like for this:
storage {
value: u64 = 0,
// to stay compatible, this has to stay the same in the next version
}
impl Implementation for Contract {
#[storage(write)]
fn double_input(value: u64) -> u64 {
let new_value = value * 2;
storage.value.write(new_value);
new_value
}
}
This contract has one function called double_input
, which calculates the input value times two, updates the value
variable in storage, and returns the new value.
How does this differ from calling a contract?
There are a couple of major differences between calling a contract directly and using the run_external
method.
First, to use run_external
, the ABI of the external contract is not required. The proxy contract has no knowledge of the external contract except for its ContractId
.
Upgradeable Contract Storage
Second, the storage context of the proxy contract is retained for the loaded code.
This means that in the examples above, the value
variable gets updated in the storage for the proxy contract.
For example, if you were to read the value
variable by directly calling the implementation contract, you would get a different result than if you read it through the proxy contract.
The proxy contract loads the code and executes it in its own context.
Fallback functions
If the function name doesn't exist in the target contract but a fallback
function does, the fallback
function will be triggered.
If there is no fallback function, the transaction will revert.
You can access function parameters for fallback functions using the call_frames
module in the std-lib
.
For example, to access the _foo
input parameter in the proxy function below, you can use the called_args
method in the fallback
function:
fn does_not_exist_in_the_target(_foo: u64) -> u64 {
run_external(TARGET)
}
#[fallback]
fn fallback() -> u64 {
use std::call_frames::*;
__log(3);
__log(called_method());
__log("double_value");
__log(called_method() == "double_value");
let foo = called_args::<u64>();
foo * 3
}
In this case, the does_not_exist_in_the_target
function will return _foo * 3
.
Limitations
Some limitations of run_external
function are:
- It can only be used with other contracts. Scripts, predicates, and library code cannot be run externally.
- If you change the implementation contract, you must maintain the same order of previous storage variables and types, as this is what has been stored in the proxy storage.
- You can't use the call stack in another call frame before you use
run_external
. You can only use the call stack within the call frame that containsrun_external
.
Advanced Concepts
Advanced concepts.
- Advanced Types
- Advanced Storage
- Generic Types
- Traits
- Associated Types
- Generics and Trait Constraints
- Assembly
- Never Type
Advanced Types
Creating Type Synonyms with Type Aliases
Sway provides the ability to declare a type alias to give an existing type another name. For this we use the type
keyword. For example, we can create the alias Kilometers
to u64
like so:
type Kilometers = u64;
Now, the alias Kilometers
is a synonym for u64
. Note that Kilometers
is not a separate new type. Values that have the type Kilometers
will be treated the same as values of type u64
:
let x: u64 = 5;
let y: Kilometers = 5;
assert(x + y == 10);
Because Kilometers
and u64
are the same type, we can add values of both types and we can pass Kilometers
values to functions that take u64
parameters. However, using this method, we don’t get the type checking benefits that we get from introducing a separate new type called Kilometers
. In other words, if we mix up Kilometers
and i32
values somewhere, the compiler will not give us an error.
The main use case for type synonyms is to reduce repetition. For example, we might have a lengthy array type like this:
[MyStruct<u64, b256>; 5]
Writing this lengthy type in function signatures and as type annotations all over the code can be tiresome and error prone. Imagine having a project full of code like this:
fn foo_long(array: [MyStruct<u64, b256>; 5]) -> [MyStruct<u64, b256>; 5] {
array
}
A type alias makes this code more manageable by reducing the repetition. Below, we’ve introduced an alias named MyArray
for the verbose type and can replace all uses of the type with the shorter alias MyArray
:
type MyArray = [MyStruct<u64, b256>; 5];
fn foo_shorter(array: MyArray) -> MyArray {
array
}
This code is much easier to read and write! Choosing a meaningful name for a type alias can help communicate your intent as well.
Advanced Storage
Nested Storage Collections
Through the use of StorageKey
s, you may have nested storage collections such as storing a StorageString
in a StorageMap<K, V>
.
For example, here we have a few common nested storage types declared in a storage
block:
storage {
nested_map_vec: StorageMap<u64, StorageVec<u8>> = StorageMap {},
nested_map_string: StorageMap<u64, StorageString> = StorageMap {},
nested_vec_bytes: StorageVec<StorageBytes> = StorageVec {},
}
Please note that storage initialization is needed to do this.
NOTE: When importing a storage type, please be sure to use the glob operator i.e.
use std::storage::storage_vec::*
.
Storing a StorageVec<T>
in a StorageMap<K, V>
The following demonstrates how to write to a StorageVec<T>
that is nested in a StorageMap<T, V>
:
// Setup and initialize storage for the StorageVec.
storage.nested_map_vec.try_insert(10, StorageVec {});
// Method 1: Push to the vec directly
storage.nested_map_vec.get(10).push(1u8);
storage.nested_map_vec.get(10).push(2u8);
storage.nested_map_vec.get(10).push(3u8);
// Method 2: First get the storage key and then push the values.
let storage_key_vec: StorageKey<StorageVec<u8>> = storage.nested_map_vec.get(10);
storage_key_vec.push(4u8);
storage_key_vec.push(5u8);
storage_key_vec.push(6u8);
The following demonstrates how to read from a StorageVec<T>
that is nested in a StorageMap<T, V>
:
// Method 1: Access the StorageVec directly.
let stored_val1: u8 = storage.nested_map_vec.get(10).pop().unwrap();
let stored_val2: u8 = storage.nested_map_vec.get(10).pop().unwrap();
let stored_val3: u8 = storage.nested_map_vec.get(10).pop().unwrap();
// Method 2: First get the storage key and then access the value.
let storage_key: StorageKey<StorageVec<u8>> = storage.nested_map_vec.get(10);
let stored_val4: u8 = storage_key.pop().unwrap();
let stored_val5: u8 = storage_key.pop().unwrap();
let stored_val6: u8 = storage_key.pop().unwrap();
Storing a StorageString
in a StorageMap<K, V>
The following demonstrates how to write to a StorageString
that is nested in a StorageMap<T, V>
:
// Setup and initialize storage for the StorageString.
storage.nested_map_string.try_insert(10, StorageString {});
// Method 1: Store the string directly.
let my_string = String::from_ascii_str("Fuel is blazingly fast");
storage.nested_map_string.get(10).write_slice(my_string);
// Method 2: First get the storage key and then write the value.
let my_string = String::from_ascii_str("Fuel is modular");
let storage_key: StorageKey<StorageString> = storage.nested_map_string.get(10);
storage_key.write_slice(my_string);
The following demonstrates how to read from a StorageString
that is nested in a StorageMap<T, V>
:
// Method 1: Access the string directly.
let stored_string: String = storage.nested_map_string.get(10).read_slice().unwrap();
// Method 2: First get the storage key and then access the value.
let storage_key: StorageKey<StorageString> = storage.nested_map_string.get(10);
let stored_string: String = storage_key.read_slice().unwrap();
Storing a StorageBytes
in a StorageVec<T>
The following demonstrates how to write to a StorageBytes
that is nested in a StorageVec<T>
:
// Setup and initialize storage for the StorageVec.
storage.nested_map_vec.try_insert(10, StorageVec {});
// Method 1: Push to the vec directly
storage.nested_map_vec.get(10).push(1u8);
storage.nested_map_vec.get(10).push(2u8);
storage.nested_map_vec.get(10).push(3u8);
// Method 2: First get the storage key and then push the values.
let storage_key_vec: StorageKey<StorageVec<u8>> = storage.nested_map_vec.get(10);
storage_key_vec.push(4u8);
storage_key_vec.push(5u8);
storage_key_vec.push(6u8);
The following demonstrates how to read from a StorageBytes
that is nested in a StorageVec<T>
:
// Method 1: Access the StorageVec directly.
let stored_val1: u8 = storage.nested_map_vec.get(10).pop().unwrap();
let stored_val2: u8 = storage.nested_map_vec.get(10).pop().unwrap();
let stored_val3: u8 = storage.nested_map_vec.get(10).pop().unwrap();
// Method 2: First get the storage key and then access the value.
let storage_key: StorageKey<StorageVec<u8>> = storage.nested_map_vec.get(10);
let stored_val4: u8 = storage_key.pop().unwrap();
let stored_val5: u8 = storage_key.pop().unwrap();
let stored_val6: u8 = storage_key.pop().unwrap();
Storage Namespace
If you want the values in storage to be positioned differently, for instance to avoid collisions with storage from another contract when loading code, you can use the namespace annotation to add a salt to the slot calculations.
#[namespace(example_namespace)]
storage {
Manual Storage Management
It is possible to leverage FuelVM storage operations directly using the std::storage::storage_api::write
and std::storage::storage_api::read
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::storage_api::{read, write};
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) {
write(STORAGE_KEY, 0, amount);
}
#[storage(read)]
fn get_something() -> u64 {
let value: Option<u64> = read::<u64>(STORAGE_KEY, 0);
value.unwrap_or(0)
}
}
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.
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
An 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> {
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> {
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
The example below implements a Compare
trait for u64
to check if two numbers are equal. 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, i.e., 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 +
.
ABI supertraits
ABIs can also have supertrait annotations:
contract;
struct Foo {}
impl ABIsupertrait for Foo {
fn foo() {}
}
trait ABIsupertrait {
fn foo();
}
abi MyAbi : ABIsupertrait {
fn bar();
} {
fn baz() {
Self::foo() // supertrait method usage
}
}
impl ABIsupertrait for Contract {
fn foo() {}
}
// The implementation of MyAbi for Contract must also implement ABIsupertrait
impl MyAbi for Contract {
fn bar() {
Self::foo() // supertrait method usage
}
}
The implementation of MyAbi
for Contract
must also implement the ABIsupertrait
trait. Methods in ABIsupertrait
are not available externally, i.e. they're not actually contract methods, but they can be used in the actual contract methods, as shown in the example above.
ABI supertraits are intended to make contract implementations compositional, allowing combining orthogonal contract features using, for instance, libraries.
SuperABIs
In addition to supertraits, ABIs can have superABI annotations:
contract;
abi MySuperAbi {
fn foo();
}
abi MyAbi : MySuperAbi {
fn bar();
}
impl MySuperAbi for Contract {
fn foo() {}
}
// The implementation of MyAbi for Contract must also implement MySuperAbi
impl MyAbi for Contract {
fn bar() {}
}
The implementation of MyAbi
for Contract
must also implement the MySuperAbi
superABI. Methods in MySuperAbi
will be part of the MyAbi
contract interface, i.e. will be available externally (and hence cannot be called from other MyAbi
contract methods).
SuperABIs are intended to make contract implementations compositional, allowing combining orthogonal contract features using, for instance, libraries.
Associated Items
Traits can declare different kinds of associated items in their interface surface:
Associated functions
Associated functions in traits consist of just function signatures. This indicates that each implementation of the trait for a given type must define all the trait functions.
trait Trait {
fn associated_fn(self, b: Self) -> bool;
}
Associated constants
Associated constants are constants associated with a type.
trait Trait {
const ID: u32 = 0;
}
The initializer expression of an associated constants in a trait definition may be omitted to indicate that each implementation of the trait
for a given type must specify an initializer:
trait Trait {
const ID: u32;
}
Check the associated consts
section on constants page.
Associated types
Associated types in Sway allow you to define placeholder types within a trait, which can be customized by concrete implementations of that trait. These associated types are used to specify the return types of trait methods or to define type relationships within the trait.
trait MyTrait {
type AssociatedType;
}
Check the associated types
section on associated types page.
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;
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 {
[ ... ]
}
Associated Types
Associated types in Sway allow you to define placeholder types within a trait, which can be customized by concrete implementations of that trait. These associated types are used to specify the return types of trait methods or to define type relationships within the trait.
Associated types are a powerful feature of Sway's trait system, enabling generic programming and abstraction over types. They help improve code clarity and maintainability by allowing you to define generic traits without committing to specific types.
Declaring Associated Types
Associated types are declared within a trait using the type keyword. Here's the syntax for declaring an associated type:
trait MyTrait {
type AssociatedType;
}
Implementing Associated Types
Concrete implementations of a trait with associated types must provide a specific type for each associated type defined in the trait. Here's an example of implementing a trait with an associated type:
struct MyStruct;
impl MyTrait for MyStruct {
type AssociatedType = u32; // Implementing the associated type with u32
}
In this example, MyStruct
implements MyTrait
and specifies that the associated type AssociatedType
is u32
.
Using Associated Types
Associated types are used within trait methods or where the trait is used as a bound for generic functions or structs. You can use the associated type like any other type. Here's an example:
trait MyTrait {
type AssociatedType;
fn get_value(self) -> Self::AssociatedType;
}
struct MyStruct;
impl MyTrait for MyStruct {
type AssociatedType = u32;
fn get_value(self) -> Self::AssociatedType {
42
}
}
In this example, get_value
is a trait method that returns an associated type AssociatedType
.
Use Cases
Associated types are particularly useful in scenarios where you want to define traits that work with different types of data structures or abstractions, allowing the implementer to specify the concrete types. Some common use cases include:
- Collections: Traits for generic collections that allow users to specify the type of elements.
- Iterator Patterns: Traits for implementing iterators with varying element types.
- Serialization and Deserialization: Traits for serializing and deserializing data with different data formats.
Generics and Trait Constraints
Generics as Constraints
At a high level, Sway allows you to define constraints, or restrictions, that allow you to strike a balance between writing abstract and reusable code and enforcing compile-time checks to determine if the abstract code that you've written is correct.
The "abstract and reusable" part largely comes from generic types and the "enforcing compile-time checks" part largely comes from trait constraints. Generic types can be used with functions, structs, and enums (as we have seen in this book), but they can also be used with traits.
Generic Traits
Combining generic types with traits allows you to write abstract and reusable traits that can be implemented for any number of data types.
For example, imagine that you want to write a trait for converting between
different types. This would be similar to Rust's Into
and From
traits. In
Sway your conversion trait would look something like:
trait Convert<T> {
fn from(t: T) -> Self;
}
The trait Convert
takes a generic type T
. Convert
has one method
from
, which takes one parameter of type T
and returns a Self
. This means
that when you implement Convert
for a data type, from
will return the type
of that data type but will take as input the type that you define as T
. Here
is an example:
struct Square {
width: u64,
}
struct Rectangle {
width: u64,
length: u64,
}
impl Convert<Square> for Rectangle {
fn from(t: Square) -> Self {
Self {
width: t.width,
length: t.width,
}
}
}
In this example, you have two different data types, Square
and Rectangle
.
You know that all squares are rectangles and thus Square
can convert into Rectangle
(but not vice
versa) and thus you can implement the conversion trait for those types.
If we want to call these methods we can do so by:
fn main() {
let s = Square { width: 5 };
let r = Rectangle::from(s);
}
Trait Constraints
Trait constraints allow you to use generic types and traits to place constraints on what abstract code you are willing to accept in your program as correct. These constraints take the form of compile-time checks for correctness.
If we wanted to use trait constraints with our Convert
trait from the previous
section we could do so like so:
fn into_rectangle<T>(t: T) -> Rectangle
where
Rectangle: Convert<T>,
{
Rectangle::from(t)
}
This function allows you to take any generic data type T
and convert it to the
type Rectangle
as long as Convert<T>
is implemented for Rectangle
.
Calling this function with a type T
for which Convert<T>
is not implemented
for Rectangle
will fail Sway's compile-time checks.
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 (e.g., 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 you 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.
Never Type
The Never type !
represents the type of computations which never resolve to any value at all.
Additional Information
break
, continue
and return
expressions also have type !
. For example we are allowed to
write:
let x: ! = {
return 123
};
Although the let
is pointless here, it illustrates the meaning of !
. Since x
is never
assigned a value (because return
returns from the entire function), x
can be given type
Never
. We could also replace return 123
with a revert()
or a never-ending loop
and this code
would still be valid.
A more realistic usage of Never
is in this code:
let num: u32 = match get_a_number() {
Some(num) => num,
None => break,
};
Both match arms must produce values of type [u32
], but since break
never produces a value
at all we know it can never produce a value which isn't a [u32
]. This illustrates another
behaviour of the !
type - expressions with type !
will coerce into any other type.
Note that !
type coerces into any other type, another example of this would be:
let x: u32 = {
return 123
};
Regardless of the type of x
, the return block of type Never
will always coerce into x
type.
Examples
fn foo() {
let num: u64 = match Option::None::<u64> {
Some(num) => num,
None => return,
};
}
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 StorageVec
is similar to a vector on the heap but uses persistent storage.
A StorageMap
allows you to associate a value with a particular key.
We’ll discuss how to create and update a vector, StorageVec
, and StorageMap
, 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 {
Some(third) => log(third),
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::storage_vec::*;
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 StorageVec
To create a new empty StorageVec
, 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 StorageVec
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 StorageVec
To add elements to a StorageVec
, 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 StorageVec
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 {
Some(third) => log(third.read()),
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<StorageKey<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().read());
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
followed by a call to read()
to actually read the stored value. 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 StorageVec
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 StorageVec
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.
Nested Storage Vectors
It is possible to nest storage vectors as follows:
nested_vec: StorageVec<StorageVec<u64>> = StorageVec {},
The nested vector can then be accessed as follows:
#[storage(read, write)]
fn access_nested_vec() {
storage.nested_vec.push(StorageVec {});
storage.nested_vec.push(StorageVec {});
let mut inner_vec0 = storage.nested_vec.get(0).unwrap();
let mut inner_vec1 = storage.nested_vec.get(1).unwrap();
inner_vec0.push(0);
inner_vec0.push(1);
inner_vec1.push(2);
inner_vec1.push(3);
inner_vec1.push(4);
assert(inner_vec0.len() == 2);
assert(inner_vec0.get(0).unwrap().read() == 0);
assert(inner_vec0.get(1).unwrap().read() == 1);
assert(inner_vec0.get(2).is_none());
assert(inner_vec1.len() == 3);
assert(inner_vec1.get(0).unwrap().read() == 2);
assert(inner_vec1.get(1).unwrap().read() == 3);
assert(inner_vec1.get(2).unwrap().read() == 4);
assert(inner_vec1.get(3).is_none());
}
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::<Address, u64> {},
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.
For example:
#[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.
For example:
#[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).try_read().unwrap_or(0);
}
Here, value1
will have the value that's associated with the first address, and the result will be 42
. The get
method returns an Option<V>
; if there’s no value for that key in the storage map, get
will return None
. This program handles the Option
by calling unwrap_or
to set value1
to zero if map
doesn't have an entry for the key.
Storage Maps with Multiple Keys
Maps with multiple keys can be implemented using tuples as keys. For example:
map_two_keys: StorageMap<(b256, bool), b256> = StorageMap::<(b256, bool), b256> {},
Nested Storage Maps
It is possible to nest storage maps as follows:
nested_map: StorageMap<u64, StorageMap<u64, u64>> = StorageMap::<u64, StorageMap<u64, u64>> {},
The nested map can then be accessed as follows:
#[storage(read, write)]
fn access_nested_map() {
storage.nested_map.get(0).insert(1, 42);
storage.nested_map.get(2).insert(3, 24);
assert(storage.nested_map.get(0).get(1).read() == 42);
assert(storage.nested_map.get(0).get(0).try_read().is_none()); // Nothing inserted here
assert(storage.nested_map.get(2).get(3).read() == 24);
assert(storage.nested_map.get(2).get(2).try_read().is_none()); // Nothing inserted here
}
Testing
Sway aims to provide facilities for both unit testing and integration testing.
Unit testing refers to "in-language" test functions annotated with #[test]
.
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.
Unit Testing
Forc provides built-in support for building and executing tests for a package.
Tests are written as free functions with the #[test]
attribute.
For example:
#[test]
fn test_meaning_of_life() {
assert(6 * 7 == 42);
}
Each test function is ran as if it were the entry point for a script. Tests "pass" if they return successfully, and "fail" if they revert or vice versa while testing failure.
If the project has failing tests forc test
will exit with exit status 101
.
Building and Running Tests
We can build and execute all tests within a package with the following:
forc test
The output should look similar to this:
Compiled library "core".
Compiled library "std".
Compiled library "lib_single_test".
Bytecode size is 92 bytes.
Running 1 tests
test test_meaning_of_life ... ok (170.652µs)
Result: OK. 1 passed. 0 failed. Finished in 1.564996ms.
Visit the forc test
command reference to find
the options available for forc test
.
Testing Failure
Forc supports testing failing cases for test functions declared with #[test(should_revert)]
.
For example:
#[test(should_revert)]
fn test_meaning_of_life() {
assert(6 * 6 == 42);
}
It is also possible to specify an expected revert code, like the following example.
#[test(should_revert = "18446744073709486084")]
fn test_meaning_of_life() {
assert(6 * 6 == 42);
}
Tests with #[test(should_revert)]
are considered to be passing if they are reverting.
Calling Contracts
Unit tests can call contract functions an example for such calls can be seen below.
contract;
abi MyContract {
fn test_function() -> bool;
}
impl MyContract for Contract {
fn test_function() -> bool {
true
}
}
To test the test_function()
, a unit test like the following can be written.
#[test]
fn test_success() {
let caller = abi(MyContract, CONTRACT_ID);
let result = caller.test_function {}();
assert(result == true)
}
It is also possible to test failure with contract calls as well.
#[test(should_revert)]
fn test_fail() {
let caller = abi(MyContract, CONTRACT_ID);
let result = caller.test_function {}();
assert(result == false)
}
Note: When running
forc test
, your contract will be built twice: first without unit tests in order to determine the contract's ID, then a second time with unit tests with theCONTRACT_ID
provided to their namespace. ThisCONTRACT_ID
can be used with theabi
cast to enable contract calls within unit tests.
Unit tests can call methods of external contracts if those contracts are added as contract dependencies, i.e. in the contract-dependencies
section of the manifest file. An example of such calls is shown below:
contract;
abi CallerContract {
fn test_false() -> bool;
}
impl CallerContract for Contract {
fn test_false() -> bool {
false
}
}
abi CalleeContract {
fn test_true() -> bool;
}
#[test]
fn test_multi_contract_calls() {
let caller = abi(CallerContract, CONTRACT_ID);
let callee = abi(CalleeContract, callee::CONTRACT_ID);
let should_be_false = caller.test_false();
let should_be_true = callee.test_true();
assert(!should_be_false);
assert(should_be_true);
}
Example Forc.toml
for contract above:
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "main.sw"
license = "Apache-2.0"
name = "caller"
[dependencies]
core = { path = "../../../sway-lib-core" }
std = { path = "../../../sway-lib-std/" }
[contract-dependencies]
callee = { path = "../callee" }
Running Tests in Parallel or Serially
By default, all unit tests in your project are run in parallel. Note that this does not lead to any data races in storage because each unit test has its own storage space that is not shared by any other unit test.
By default, forc test
will use all the available threads in your system. To request that a specific number of threads be used, the flag --test-threads <val>
can be provided to forc test
.
forc test --test-threads 1
Logs Inside Tests
Forc has some capacity to help decode logs returned from the unit tests. You can use this feature to decode raw logs into a human readable format.
script;
fn main() {}
#[test]
fn test_fn() {
let a = 10;
log(a);
let b = 30;
log(b);
assert_eq(a, 10)
assert_eq(b, 30)
}
The example shown above is logging two different variables, a
and b
and their values are 10
and 30
, respectively. Without log decoding printed log for this test with forc test --logs
(--logs
flag is required to see the logs for this example since the test is passing. Logs are silenced by default in passing tests, and can be enabled using the --logs
flag.):
Finished debug [unoptimized + fuel] target(s) in 5.23s
Bytecode hash: 0x1cb1edc031691c5c08b50fd0f07b02431848ab81b325b72eb3fd233c67d6b548
Running 1 test, filtered 0 tests
test test_fn ... ok (38.875µs, 232 gas)
[{"LogData":{"data":"000000000000000a","digest":"8d85f8467240628a94819b26bee26e3a9b2804334c63482deacec8d64ab4e1e7","id":"0000000000000000000000000000000000000000000000000000000000000000","is":10368,"len":8,"pc":11032,"ptr":67107840,"ra":0,"rb":0}},{"LogData":{"data":"000000000000001e","digest":"48a97e421546f8d4cae1cf88c51a459a8c10a88442eed63643dd263cef880c1c","id":"0000000000000000000000000000000000000000000000000000000000000000","is":10368,"len":8,"pc":11516,"ptr":67106816,"ra":0,"rb":1}}]
This is not very easy to understand, it is possible to decode these logs with --decode
flag, executing forc test --logs --decode
:
Finished debug [unoptimized + fuel] target(s) in 5.23s
Bytecode hash: 0x1cb1edc031691c5c08b50fd0f07b02431848ab81b325b72eb3fd233c67d6b548
Running 1 test, filtered 0 tests
test test_fn ... ok (38.875µs, 232 gas)
Decoded log value: 10, log rb: 0
Decoded log value: 30, log rb: 1
As it can be seen, the values are human readable and easier to understand which makes debugging much more easier.
Note: This is an experimental feature and we are actively working on reporting variable names next to their values.
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 easier for Sway developers 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 the cargo-generate 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.
_Note:
templates/sway-test-rs
can be replaced withtemplates/sway-script-test-rs
ortemplates/sway-predicate-test-rs
to generate a test harness for scripts and predicates respectively.
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.write(value);
value
}
#[storage(read, write)]
fn increment_counter(amount: u64) -> u64 {
let incremented = storage.counter.read() + amount;
storage.counter.write(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::*, types::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::load_from(
"./out/debug/my-fuel-project.bin",
LoadConfiguration::default().set_storage_configuration(
StorageConfiguration::load_from(
"./out/debug/my-fuel-project-storage_slots.json",
)
.unwrap(),
),
)
.unwrap()
.deploy(&wallet, TxParameters::default())
.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!
Debugging
Forc provides tools for debugging both live transactions as well as Sway unit tests. Debugging can be done via CLI or using the VSCode IDE.
Unit testing refers to "in-language" test functions annotated with #[test]
. Line-by-line
debugging is available within the VSCode IDE.
Live transaction refers to the testing sending a transaction to a running Fuel Client
node to exercise your Sway code. Instruction-by-instruction debugging is available in the forc debug
CLI.
Debugging with CLI
The forc debug
CLI enables debugging a live transaction on a running Fuel Client node.
An example project
First, we need a project to debug, so create a new project using
forc new --script dbg_example && cd dbg_example
And then add some content to src/main.sw
, for example:
script;
use std::logging::log;
fn factorial(n: u64) -> u64 {
let mut result = 1;
let mut counter = 0;
while counter < n {
counter = counter + 1;
result = result * counter;
}
return result;
}
fn main() {
log::<u64>(factorial(5)); // 120
}
Building and bytecode output
Now we are ready to build the project.
forc build
After this the resulting binary should be located at out/debug/dbg_example.bin
. Because we are interested in the resulting bytecode, we can read that with:
forc parse-bytecode out/debug/dbg_example.bin
Which should give us something like
half-word byte op raw notes
0 0 JI { imm: 4 } 90 00 00 04 jump to byte 16
1 4 NOOP 47 00 00 00
2 8 InvalidOpcode 00 00 00 00 data section offset lo (0)
3 12 InvalidOpcode 00 00 00 44 data section offset hi (68)
4 16 LW { ra: 63, rb: 12, imm: 1 } 5d fc c0 01
5 20 ADD { ra: 63, rb: 63, rc: 12 } 10 ff f3 00
6 24 MOVE { ra: 18, rb: 1 } 1a 48 10 00
7 28 MOVE { ra: 17, rb: 0 } 1a 44 00 00
8 32 LW { ra: 16, rb: 63, imm: 0 } 5d 43 f0 00
9 36 LT { ra: 16, rb: 17, rc: 16 } 16 41 14 00
10 40 JNZI { ra: 16, imm: 13 } 73 40 00 0d conditionally jump to byte 52
11 44 LOG { ra: 18, rb: 0, rc: 0, rd: 0 } 33 48 00 00
12 48 RET { ra: 0 } 24 00 00 00
13 52 ADD { ra: 17, rb: 17, rc: 1 } 10 45 10 40
14 56 MUL { ra: 18, rb: 18, rc: 17 } 1b 49 24 40
15 60 JI { imm: 8 } 90 00 00 08 jump to byte 32
16 64 NOOP 47 00 00 00
17 68 InvalidOpcode 00 00 00 00
18 72 InvalidOpcode 00 00 00 05
We can recognize the while
loop by the conditional jumps JNZI
. The condition just before the first jump can be identified by LT
instruction (for <
). Some notable instructions that are generated only once in our code include MUL
for multiplication and LOG {.., 0, 0, 0}
from the log
function.
Setting up the debugging
We can start up the debug infrastructure. On a new terminal session run fuel-core run --db-type in-memory --debug
; we need to have that running because it actually executes the program. Now we can fire up the debugger itself: forc-debug
. Now
if everything is set up correctly, you should see the debugger prompt (>>
). You can use help
command to list available commands.
Now we would like to inspect the program while it's running. To do this, we first need to send the script to the executor, i.e. fuel-core
. To do so, we need a transaction specification, tx.json
. It looks something like this:
{
"Script": {
"script_gas_limit": 1000000,
"script": [],
"script_data": [],
"policies": {
"bits": "GasPrice",
"values": [0,0,0,0]
},
"inputs": [
{
"CoinSigned": {
"utxo_id": {
"tx_id": "c49d65de61cf04588a764b557d25cc6c6b4bc0d7429227e2a21e61c213b3a3e2",
"output_index": 18
},
"owner": "f1e92c42b90934aa6372e30bc568a326f6e66a1a0288595e6e3fbd392a4f3e6e",
"amount": 10599410012256088338,
"asset_id": "2cafad611543e0265d89f1c2b60d9ebf5d56ad7e23d9827d6b522fd4d6e44bc3",
"tx_pointer": {
"block_height": 0,
"tx_index": 0
},
"witness_index": 0,
"maturity": 0,
"predicate_gas_used": null,
"predicate": null,
"predicate_data": null
}
}
],
"outputs": [],
"witnesses": [
{
"data": [
156,254,34,102,65,96,133,170,254,105,147,35,196,199,179,133,132,240,208,149,11,46,30,96,44,91,121,195,145,184,159,235,117,82,135,41,84,154,102,61,61,16,99,123,58,173,75,226,219,139,62,33,41,176,16,18,132,178,8,125,130,169,32,108
]
}
],
"receipts_root": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
However, the key script
should contain the actual bytecode to execute, i.e. the contents of out/debug/dbg_example.bin
as a JSON array. The following command can be used to generate it:
python3 -c 'print(list(open("out/debug/dbg_example.bin", "rb").read()))'
So now we replace the script array with the result, and save it as tx.json
.
Using the debugger
Now we can actually execute the script:
>> start_tx tx.json
Receipt: Log { id: 0000000000000000000000000000000000000000000000000000000000000000, ra: 120, rb: 0, rc: 0, rd: 0, pc: 10380, is: 10336 }
Receipt: Return { id: 0000000000000000000000000000000000000000000000000000000000000000, val: 0, pc: 10384, is: 10336 }
Receipt: ScriptResult { result: Success, gas_used: 60 }
Terminated
Looking at the first output line, we can see that it logged ra: 120
which is the correct return value for factorial(5)
. It also tells us that the execution terminated without hitting any breakpoints. That's unsurprising, because we haven't set up any. We can do so with breakpoint
command:
>> breakpoint 0
>> start_tx tx.json
Receipt: ScriptResult { result: Success, gas_used: 0 }
Stopped on breakpoint at address 0 of contract 0x0000000000000000000000000000000000000000000000000000000000000000
Now we have stopped execution at the breakpoint on entry (address 0
). We can now inspect the initial state of the VM.
>> register ggas
reg[0x9] = 1000000 # ggas
>> memory 0x10 0x8
000010: e9 5c 58 86 c8 87 26 dd
However, that's not too interesting either, so let's just execute until the end, and then reset the VM to remove the breakpoints.
>> continue
Receipt: Log { id: 0000000000000000000000000000000000000000000000000000000000000000, ra: 120, rb: 0, rc: 0, rd: 0, pc: 10380, is: 10336 }
Receipt: Return { id: 0000000000000000000000000000000000000000000000000000000000000000, val: 0, pc: 10384, is: 10336 }
Terminated
>> reset
Next, we will setup a breakpoint to check the state on each iteration of the while
loop. For instance, if we'd like to see what numbers get multiplied together, we could set up a breakpoint before the operation. The bytecode has only a single MUL
instruction:
half-word byte op raw notes
14 56 MUL { ra: 18, rb: 18, rc: 17 } 1b 49 24 40
We can set a breakpoint on its address, at halfword-offset 14
.
>>> breakpoint 14
>> start_tx tx.json
Receipt: ScriptResult { result: Success, gas_used: 9 }
Stopped on breakpoint at address 56 of contract 0x0000000000000000000000000000000000000000000000000000000000000000
Now we can inspect the inputs to multiply. Looking at the specification tells us that the instruction MUL { ra: 18, rb: 18, rc: 17 }
means reg[18] = reg[18] * reg[17]
. So inspecting the inputs tells us that
>> r 18 17
reg[0x12] = 1 # reg18
reg[0x11] = 1 # reg17
So on the first round the numbers are 1
and 1
, so we can continue to the next iteration:
>> c
Stopped on breakpoint at address 56 of contract 0x0000000000000000000000000000000000000000000000000000000000000000
>> r 18 17
reg[0x12] = 1 # reg18
reg[0x11] = 2 # reg17
And the next one:
>> c
Stopped on breakpoint at address 56 of contract 0x0000000000000000000000000000000000000000000000000000000000000000
>> r 18 17
reg[0x12] = 2 # reg18
reg[0x11] = 3 # reg17
And fourth one:
>> c
Stopped on breakpoint at address 56 of contract 0x0000000000000000000000000000000000000000000000000000000000000000
>> r 18 17
reg[0x12] = 6 # reg18
reg[0x11] = 4 # reg17
And round 5:
>> c
Stopped on breakpoint at address 56 of contract 0x0000000000000000000000000000000000000000000000000000000000000000
>> r 18 17
reg[0x12] = 24 # reg18
reg[0x11] = 5 # reg17
At this point we can look at the values
17 | 18 |
---|---|
1 | 1 |
2 | 1 |
3 | 2 |
4 | 6 |
5 | 24 |
From this we can clearly see that the left side, register 17
is the counter
variable, and register 18
is result
. Now the counter equals the given factorial function argument 5
, and the loop terminates. So when we continue, the program finishes without encountering any more breakpoints:
>> c
Receipt: Log { id: 0000000000000000000000000000000000000000000000000000000000000000, ra: 120, rb: 0, rc: 0, rd: 0, pc: 10380, is: 10336 }
Receipt: Return { id: 0000000000000000000000000000000000000000000000000000000000000000, val: 0, pc: 10384, is: 10336 }
Terminated
Debugging with IDE
The forc debug
plugin also enables line-by-line debugging of Sway unit tests in VSCode.
Installation
- Install the Sway VSCode extension from the marketplace.
- Ensure you have the forc-debug binary installed.
which forc-debug
. It can be installed withfuelup component add forc-debug
. - Create a
.vscode/launch.json
file with the following contents:
{
"version": "0.2.0",
"configurations": [
{
"type": "sway",
"request": "launch",
"name": "Debug Sway",
"program": "${file}"
}]
}
An example project
Given this example contract:
contract;
abi CallerContract {
fn test_false() -> bool;
}
impl CallerContract for Contract {
fn test_false() -> bool {
false
}
}
abi CalleeContract {
fn test_true() -> bool;
}
#[test]
fn test_multi_contract_calls() {
let caller = abi(CallerContract, CONTRACT_ID);
let callee = abi(CalleeContract, callee::CONTRACT_ID);
let should_be_false = caller.test_false();
let should_be_true = callee.test_true();
assert(!should_be_false);
assert(should_be_true);
}
Within the sway file open in VSCode, you can set breakpoints on lines within the test or functions that it calls, and click Run -> Start Debugging to begin debugging the unit test.
This will build the sway project and run it in debug mode. The debugger will stop the VM execution when a breakpoint is hit.
The debug panel will show VM registers under the Variables tab, as well as the current VM opcode where execution is suspended. You can continue execution, or use the Step Over function to step forward, instruction by instruction.
Sway LSP
Welcome to the documentation for Sway LSP, the language server designed specifically for the Sway programming language. This documentation serves as a comprehensive guide to help you understand and utilize the powerful features provided by Sway LSP.
Sway LSP is built on the Language Server Protocol (LSP), a standardized protocol for enabling rich programming language support in editor and IDE environments. It acts as a bridge between your favorite code editor or integrated development environment and the Sway programming language, offering advanced semantic analysis and a wide range of features to enhance your development experience.
With Sway LSP, you can expect a seamless and efficient coding experience while working with the Sway programming language. It provides intelligent code completion, precise symbol navigation, type information, and other smart features that empower you to write clean and error-free code. By leveraging the power of Sway LSP, you can increase productivity, reduce debugging time, and write high-quality code with confidence.
In this documentation, you will find detailed information about how to set up Sway LSP in your preferred code editor or IDE, configure its settings to match your coding style, and take advantage of its various features. We will guide you through the installation process, provide examples of typical configuration setups, and walk you through the usage of each feature supported by Sway LSP.
Whether you are a beginner or an experienced Sway developer, this documentation aims to be your go-to resource for understanding and maximizing the capabilities of Sway LSP. So let's dive in and unlock the full potential of the Sway programming language with Sway LSP!
Installation
The Sway language server is contained in the forc-lsp
binary, which is installed as part of the Fuel toolchain. Once installed, it can be used with a variety of IDEs. It must be installed for any of the IDE plugins to work.
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.
VSCode
This is the best supported editor at the moment.
You can install the latest release of the plugin from the marketplace.
Note that we only support the most recent version of VS Code.
Code OSS (VSCode on Linux)
- Install code-marketplace to get access to all of the extensions in the VSCode marketplace.
- Install the Sway extension.
vim / neovim
Follow the documentation for sway.vim to install.
helix
Install helix and Sway LSP will work out of the box.
Sway support is built into helix using tree-sitter-sway.
Emacs
Coming soon! Feel free to contribute.
Features
Code Actions
Source: code_actions
Quickly generate boilerplate code and code comments for functions, structs, and ABIs.
Completion
Source: completion.rs
Suggests code to follow partially written statements for functions and variables.
Diagnostics
Source: diagnostic.rs
Displays compiler warnings and errors inline.
Syntax Highlighting
Source: highlight.rs
Highlights code based on type and context.
Hover
Source: hover
Provides documentation, compiler diagnostics, and reference links when hovering over functions and variables.
Inlay Hints
Source: inlay_hints.rs
Displays the implied type of a variable next to the variable name. Configurable in Settings.
Rename
Source: rename.rs
Renames a symbol everywhere in the workspace.
Run
Source: runnable.rs
Shows a button above a runnable function or test.
Troubleshooting
First, confirm you are running the most recent version:
fuelup toolchain install latest
fuelup update
forc-lsp --version
Second, confirm that your $PATH
resolves to the forc-lsp
binary in $HOME/.fuelup/bin
.
which forc-lsp
Slow Performance
If you are experiencing slow performance, you can try the following:
Follow the steps above to ensure you are running the most recent version.
Then, make sure you only have the most recent version of the LSP server running.
pkill forc-lsp
Large projects
Sway projects with ten or more Sway files are likely to have slower LSP performance. We are working on better support for large projects.
In the meantime, if it's too slow, you can disable the LSP server entirely with the sway-lsp.diagnostic.disableLsp
setting. The extension will still provide basic syntax highlighting, command palettes, as well as the Sway debugger, but all other language features will be disabled.
Server Logs
You can you enable verbose logging of the LSP server.
In VSCode, this is under the setting:
"sway-lsp.trace.server": "verbose"
Once enabled, you can find this in the output window under Sway Language Server.
For other editors, see Installation for links to documentation.
Sway Reference
- Compiler Intrinsics
- Attributes
- Style Guide
- Known Issues and Workarounds
- Differences from Rust
- Differences from Solidity
- Contributing to Sway
- Keywords
Sway Libraries
The purpose of Sway Libraries is to contain libraries which users can import and use that are not part of the standard library.
These libraries contain helper functions and other tools valuable to blockchain development.
For more information on how to use a Sway-Libs library, please refer to the Sway-Libs Book.
Assets Libraries
Asset Libraries are any libraries that use Native Assets on the Fuel Network.
- Asset Library; provides helper functions for the SRC-20, SRC-3, and SRC-7 standards.
Access Control and Security Libraries
Access Control and Security Libraries are any libraries that are built and intended to provide additional safety when developing smart contracts.
- Ownership Library; used to apply restrictions on functions such that only a single user may call them. This library provides helper functions for the SRC-5; Ownership Standard.
- Admin Library; used to apply restrictions on functions such that only a select few users may call them like a whitelist.
- Pausable Library; allows contracts to implement an emergency stop mechanism.
- Reentrancy Guard Library; used to detect and prevent reentrancy attacks.
Cryptography Libraries
Cryptography Libraries are any libraries that provided cryptographic functionality beyond what the std-lib provides.
- Bytecode Library; used for on-chain verification and computation of bytecode roots for contracts and predicates.
- Merkle Proof Library; used to verify Binary Merkle Trees computed off-chain.
Math Libraries
Math Libraries are libraries which provide mathematic functions or number types that are outside of the std-lib's scope.
- Fixed Point Number Library; an interface to implement fixed-point numbers.
- Signed Integers Library; an interface to implement signed integers.
Data Structures Libraries
Data Structure Libraries are libraries which provide complex data structures which unlock additional functionality for Smart Contracts.
- Queue Library; a linear data structure that provides First-In-First-Out (FIFO) operations.
Compiler Intrinsics
The Sway compiler supports a list of intrinsics that perform various low level operations that are useful for building libraries. Compiler intrinsics should rarely be used but are preferred over asm
blocks because they are type-checked and are safer overall. Below is a list of all available compiler intrinsics:
__size_of_val<T>(val: T) -> u64
Description: Return the size of type T
in bytes.
Constraints: None.
__size_of<T>() -> u64
Description: Return the size of type T
in bytes.
Constraints: None.
__size_of_str_array<T>() -> u64
Description: Return the size of type T
in bytes. This intrinsic differs from __size_of
in the case of "string arrays" where the actual length in bytes of the string is returned without padding the byte size to the next word alignment. When T
is not a "string array" 0
is returned.
Constraints: None.
__assert_is_str_array<T>()
Description: Throws a compile error if type T
is not a "string array".
Constraints: None.
__to_str_array(s: str) -> str[N]
Description: Converts a "string slice" to "string array" at compile time. Parameter "s" must be a string literal.
Constraints: None.
__is_reference_type<T>() -> bool
Description: Returns true
if T
is a reference type and false
otherwise.
Constraints: None.
__is_str_array<T>() -> bool
Description: Returns true
if T
is a string array and false
otherwise.
Constraints: None.
__eq<T>(lhs: T, rhs: T) -> bool
Description: Returns whether lhs
and rhs
are equal.
Constraints: T
is bool
, u8
, u16
, u32
, u64
, u256
, b256
or raw_ptr
.
__gt<T>(lhs: T, rhs: T) -> bool
Description: Returns whether lhs
is greater than rhs
.
Constraints: T
is u8
, u16
, u32
, u64
, u256
, b256
.
__lt<T>(lhs: T, rhs: T) -> bool
Description: Returns whether lhs
is less than rhs
.
Constraints: T
is u8
, u16
, u32
, u64
, u256
, b256
.
__gtf<T>(index: u64, tx_field_id: u64) -> T
Description: Returns transaction field with ID tx_field_id
at index index
, if applicable. This is a wrapper around FuelVM's gtf
instruction. The resulting field is cast to T
.
Constraints: None.
__addr_of<T>(val: T) -> raw_ptr
Description: Returns the address in memory where val
is stored.
Constraints: T
is a reference type.
__state_load_word(key: b256) -> u64
Description: Reads and returns a single word from storage at key key
.
Constraints: None.
__state_load_quad(key: b256, ptr: raw_ptr, slots: u64) -> bool
Description: Reads slots
number of slots (b256
each) from storage starting at key key
and stores them in memory starting at address ptr
. Returns a Boolean describing whether all the storage slots were previously set.
Constraints: None.
__state_store_word(key: b256, val: u64) -> bool
Description: Stores a single word val
into storage at key key
. Returns a Boolean describing whether the store slot was previously set.
Constraints: None.
__state_store_quad(key: b256, ptr: raw_ptr, slots: u64) -> bool
Description: Stores slots
number of slots (b256
each) starting at address ptr
in memory into storage starting at key key
. Returns a Boolean describing whether the first storage slot was previously set.
Constraints: None.
__log<T>(val: T)
Description: Logs value val
.
Constraints: None.
__add<T>(lhs: T, rhs: T) -> T
Description: Adds lhs
and rhs
and returns the result.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
.
__sub<T>(lhs: T, rhs: T) -> T
Description: Subtracts rhs
from lhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
.
__mul<T>(lhs: T, rhs: T) -> T
Description: Multiplies lhs
by rhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
.
__div<T>(lhs: T, rhs: T) -> T
Description: Divides lhs
by rhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
.
__and<T>(lhs: T, rhs: T) -> T
Description: Bitwise AND lhs
and rhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
, b256
.
__or<T>(lhs: T, rhs: T) -> T
Description: Bitwise OR lhs
and rhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
, b256
.
__xor<T>(lhs: T, rhs: T) -> T
Description: Bitwise XOR lhs
and rhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
, b256
.
__mod<T>(lhs: T, rhs: T) -> T
Description: Modulo of lhs
by rhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
.
__rsh<T>(lhs: T, rhs: u64) -> T
Description: Logical right shift of lhs
by rhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
, b256
.
__lsh<T>(lhs: T, rhs: u64) -> T
Description: Logical left shift of lhs
by rhs
.
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
, b256
.
__revert(code: u64)
Description: Reverts with error code code
.
Constraints: None.
__ptr_add(ptr: raw_ptr, offset: u64)
Description: Adds offset
to the raw value of pointer ptr
.
Constraints: None.
__ptr_sub(ptr: raw_ptr, offset: u64)
Description: Subtracts offset
to the raw value of pointer ptr
.
Constraints: None.
__smo<T>(recipient: b256, data: T, coins: u64)
Description: Sends a message data
of arbitrary type T
and coins
amount of the base asset to address recipient
.
Constraints: None.
__not(op: T) -> T
Description: Bitwise NOT of op
Constraints: T
is an integer type, i.e. u8
, u16
, u32
, u64
, u256
, b256
.
__jmp_mem()
Description: Jumps to MEM[$hp]
.
Constraints: None.
Attributes
The Sway compiler supports a list of attributes that perform various operations that are useful for building, testing and documenting Sway programs. Below is a list of all available attributes:
Allow
The #[allow(...)]
attribute overrides checks so that violations will go unreported. The following checks can be disabled:
#[allow(dead_code)]
disable checks for dead code;#[allow(deprecated)]
disables checks for usage of deprecated structs, functions and other items.
Doc
The #[doc(..)]
attribute specifies documentation.
Line doc comments beginning with exactly three slashes ///
, are interpreted as a special syntax for doc attributes. That is, they are equivalent to writing #[doc("...")]
around the body of the comment, i.e., /// Foo
turns into #[doc("Foo")]
Line comments beginning with //!
are doc comments that apply to the module of the source file they are in. That is, they are equivalent to writing #![doc("...")]
around the body of the comment. //!
module level doc comments should be at the top of Sway files.
Documentation can be generated from doc attributes using forc doc
.
Inline
The inline attribute suggests that a copy of the attributed function should be placed in the caller, rather than generating code to call the function where it is defined.
Note: The Sway compiler automatically inlines functions based on internal heuristics. Incorrectly inlining functions can make the program slower, so this attribute should be used with care.
The #[inline(never)]
attribute suggests that an inline expansion should never be performed.
The #[inline(always)]
attribute suggests that an inline expansion should always be performed.
Note:
#[inline(..)]
in every form is a hint, with no requirements on the language to place a copy of the attributed function in the caller.
Payable
The lack of #[payable]
implies the method is non-payable. When calling an ABI method that is non-payable, the compiler emits an error if the amount of coins forwarded with the call is not guaranteed to be zero. Note that this is strictly a compile-time check and does not incur any runtime cost.
Storage
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.
The #[storage(read)]
attribute indicates that a function requires read access to the storage.
The #[storage(write)]
attribute indicates that a function requires write access to the storage.
More details in Purity.
Test
The #[test]
attribute marks a function to be executed as a test.
The #[test(should_revert)]
attribute marks a function to be executed as a test that should revert.
More details in Unit Testing.
Deprecated
The #[deprecated]
attribute marks an item as deprecated and makes the compiler emit a warning for every usage of the deprecated item. This warning can be disabled using #[allow(deprecated)]
.
It is possible to improve the warning message with #[deprecated(note = "your message")]
Fallback
The #[fallback]
attribute makes the compiler use the marked function as the contract call fallback function, which means that, when a contract is called, and the contract selection fails, the fallback function will be called instead.
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.
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, all primitive integers smaller and including u64
are stored in registers; u256
, being bigger than the registers, and hashes (the b256
type) are not stored in registers but rather in memory. They are therefore pointers to a 32-byte memory region containing their data.
Unsigned Integers Only
Only unsigned integers are provided as primitives: u8
, u16
, u32
, u64
, and u256
. 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.
Account Types
Account types in the FuelVM have type-safe wrappers around primitive b256
hashes to clearly distinguish their respective types. The wrapper Address
mirrors that of an EOA (Externally Owned Account) and has the ability to hold UTXOs in the context of the EVM. The other wrapper, ContractId
, reflects that of a deployed contract in the EVM but cannot hold UTXOs.
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, but with a different syntax. You can see the above enum but with Sway syntax below:
// 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 Discourse.
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 bug fixes 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 to 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
Keywords
The following list contains keywords that are reserved for current or future use by the Sway language. As such, they cannot be used as identifiers. Identifiers are names of functions, variables, parameters, modules, constants, attributes, types or traits, etc.
Keywords Currently in Use
The following is a list of keywords currently in use, with their functionality described.
as
- rename items inuse
statements, e.g.,use type::a as alias_name
abi
- defines a smart contract ABI in a syntactically similar way to traitsbreak
- exit a loop immediatelyconst
- define constant itemscontinue
- continue to the next loop iterationelse
- used in conjunction withif
conditions for control flow constructsenum
- define an enumerationfalse
- Boolean false literalfn
- define a function or the function pointer typeif
- branch based on the result of a conditional expressionimpl
- implement inherent or trait functionalitylet
- bind a variablematch
- exhaustively match a value to patternsmod
- define a modulemut
- denote mutability in references, or pattern bindingspub
- denote public visibility of Sway data structures, traits, or modulesref
- bind by referencereturn
- return early from a functionSelf
- a type alias for the type we are defining or implementingself
- method subjectstruct
- define a structuretrait
- define a traittrue
- Boolean true literaltype
- define a type alias or associated typeuse
- bring symbols into scopewhere
- specifies traits for generic typeswhile
- loop conditionally based on the result of an expression
Keywords Reserved for Possible Future Use
abstract
async
await
become
box
do
dyn
extern
for
in
loop
macro
move
override
priv
static
super
try
typeof
unsafe
unsized
virtual
yield
Special Keywords
Program Keywords
Keywords associated with defining the type of Sway program to compile
contract
- analogous to a deployed API with some database statelibrary
- Sway code that defines new common behaviorpredicate
- programs that return a Boolean value and which represent ownership of some resource upon execution to truescript
- a runnable bytecode on the chain, which executes once to perform a task
Attribute Keywords
Keywords associated with defining the functionality of attributes
allow
- overrides checks that would otherwise result in errors or warningsdoc
- specifies documentationinline
- suggests that a copy of the attributed function should be placed in the caller, rather than generating code to call the function where it is definedpayable
- implies method is payable for compile timestorage
- declaration that contains a list of stored variablestest
- marks a function to be executed as a testdeprecated
- marks an item as deprecated
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 recommended 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-profile]
- 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-profile.*]
section
The [build-profile]
tables provide a way to customize compiler settings such as debug options.
The following fields can be provided for a build-profile:
print-ast
- Whether to print out the generated AST or not, defaults to false.print-dca-graph
- Whether to print out the computed Dead Code Analysis (DCA) graph (in GraphViz DOT format), defaults to false.print-dca-graph-url-format
- The URL format to be used in the generated DOT file, an example for VS Code would be:vscode://file/{path}:{line}:{col}
.print-ir
- Whether to print out the generated Sway IR (Intermediate Representation) or not, defaults to false.print-asm
- Whether to print out the generated ASM (assembler), defaults to false.terse
- Terse mode. Limited warning and error output, defaults to false.time_phases
- Whether to output the time elapsed over each part of the compilation process, defaults to false.include_tests
- Whether or not to include test functions in parsing, type-checking, and code generation. This is set to true by invocations likeforc test
, but defaults to false.json_abi_with_callpaths
- Whether to generate a JSON ABI withcallpaths
instead of names for structs and enums, defaults to false. This option can help prevent conflicting struct or enum definitions by using the full path instead of the name.error_on_warnings
- Whether to treat errors as warnings, defaults to false.
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-profile.debug]
print-asm = { virtual = false, allocated = false, final = true }
print-ir = { initial = false, final = true, modified = false, passes = []}
terse = false
[build-profile.release]
print-asm = { virtual = true, allocated = false, final = true }
print-ir = { initial = true, final = false, modified = true, passes = ["dce", "sroa"]}
terse = true
Since release
and debug
are 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 --asm
) will override the selected build profile. For example if you pass both --release
and --asm all
, release
build profile is overridden and resulting build profile would have a structure like the following:
print-ast = false
print-ir = { initial = false, final = false, modified = false, passes = []}
print-asm = { virtual = true, allocated = true, final = true }
terse = false
time-phases = false
include-tests = false
json-abi-with-callpaths = false
error-on-warnings = false
experimental-private-modules = 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
with the test
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-dependencies]
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;
}
Because the ID of a contract is computed deterministically, rebuilding the same contract would always result in the same contract ID. Since two contracts with the same contract ID cannot be deployed on the blockchain, a "salt" factor is needed to modify the contract ID. For each contract dependency declared under [contract-dependencies]
, salt
can be specified. An example is shown below:
[contract-dependencies]
foo = { path = "../foo", salt = "0x1000000000000000000000000000000000000000000000000000000000000000" }
For contract dependencies that do not specify any value for salt
, a default of all zeros for salt
is implicitly applied.
Workspaces
A workspace is a collection of one or more packages, namely workspace members, that are managed together.
The key points for workspaces are:
- Common
forc
commands available for a single package can also be used for a workspace, likeforc build
orforc deploy
. - All packages share a common
Forc.lock
file which resides in the root directory of the workspace.
Workspace manifests are declared within Forc.toml
files and support the following fields:
An empty workspace can be created with forc new --workspace
or forc init --workspace
.
The members
field
The members
field defines which packages are members of the workspace:
[workspace]
members = ["member1", "path/to/member2"]
The members
field accepts entries to be given in relative path with respect to the workspace root.
Packages that are located within a workspace directory but are not contained within the members
set are ignored.
The [patch]
section
The [patch]
section can be used to override any dependency in the workspace dependency graph. The usage is the same with package level [patch]
section and details can be seen here.
It is not allowed to declare patch table in member of a workspace if the workspace manifest file contains a patch table.
Example:
[workspace]
members = ["member1", "path/to/member2"]
[patch.'https://github.com/fuellabs/sway']
std = { git = "https://github.com/fuellabs/sway", branch = "test" }
In the above example each occurrence of std
as a dependency in the workspace will be changed with std
from test
branch of sway repo.
Some forc
commands that support workspaces
forc build
- Builds an entire workspace.forc deploy
- Builds and deploys all deployable members (i.e, contracts) of the workspace in the correct order.forc run
- Builds and runs all scripts of the workspace.forc check
- Checks all members of the workspace.forc update
- Checks and updates workspace levelForc.lock
file that is shared between workspace members.forc clean
- Cleans all output artifacts for each member of the workspace.forc fmt
- Formats all members of a workspace.
Dependencies
Forc has a dependency management system which can pull packages using git and ipfs
. 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 git
, ipfs
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" }
For ipfs
sources, forc
will fetch the specified cid
using either a local ipfs
node or a public gateway. forc
automatically tries to connect to local ipfs
node. If it fails, it defaults to using https://ipfs.io/
as a gateway.
The following example adds a dependency with an ipfs
source.
[dependencies]
custom_lib = { ipfs = "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" }
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
and ipfs
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 contract-id
forc init
forc new
forc parse-bytecode
forc plugins
forc predicate-root
forc test
forc update
forc template
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 create a wallet you can use forc wallet new
. 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 created a wallet, you can derive a new account by running forc wallet account new
. It will ask your password to decrypt the wallet before deriving 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, 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
.
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 --account <account_index> tx-id <transaction_id>
. 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
accounts
command.
forc wallet accounts
- 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 want to sign the transaction generated by
forc-deploy
orforc-run
with an account funded by default once you start your local node, you can pass--default-signer
to them. Please note that this will only work against your local node.forc-deploy --default-signer
forc-run --default-signer
By default --default-signer
flag would sign your transactions with the following private-key:
0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c
Interacting with the testnet
To interact with the latest testnet, use the --testnet
flag. When this flag is passed, transactions created by forc-deploy
will be sent to the beta-4
testnet.
forc-deploy --testnet
It is also possible to pass the exact node URL while using forc-deploy
or forc-run
which can be done using --node-url
flag.
forc-deploy --node-url https://beta-3.fuel.network
Another alternative is the --target
option, which provides useful aliases to all targets. For example if you want to deploy to beta-3
you can use:
forc-deploy --target beta-3
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.
Deployment Artifacts
forc-deploy saves the details of each deployment in the out/deployments
folder within the project's root directory. Below is an example of a deployment artifact:
{
"transaction_id": "0xec27bb7a4c8a3b8af98070666cf4e6ea22ca4b9950a0862334a1830520012f5d",
"salt": "0x9e35d1d5ef5724f29e649a3465033f5397d3ebb973c40a1d76bb35c253f0dec7",
"network_endpoint": "http://127.0.0.1:4000",
"chain_id": 0,
"contract_id": "0x767eeaa7af2621e637f9785552620e175d4422b17d4cf0d76335c38808608a7b",
"deployment_size": 68,
"deployed_block_id": "0x915c6f372252be6bc54bd70df6362dae9bf750ba652bf5582d9b31c7023ca6cf"
}