The Sway Reference
This is the technical reference for the Sway programming language. For a prose explanation and introduction to the language, please refer to the Sway Book.
Installation
The Sway toolchain
is required to compile Sway
programs.
There are three ways to install the Sway toolchain
:
The supported operating systems include Linux and macOS; however, Windows is unsupported
.
Fuelup
Fuelup
is the recommended tool for installation and management of the toolchain.
Cargo
Cargo
may be used instead of Fuelup
; however, the user needs to manage the toolchain themselves.
The advantage of using Cargo
is the installation of plugins
that have not been added into Fuelup
.
The disadvantage occurs when Fuelup
and Cargo
are used in tandem because the latest plugins
may not be recognized.
Source
The latest features may be accessed when installing from source
; however, the features may not be ready for release and lead to unstable behavior.
Fuelup
Fuelup
is a tool used to manage the Sway toolchain. It allows the user to download compiled binaries and switch between different versions of Sway.
The installation instructions can be found at the start of the Fuelup Book
.
After installing fuelup
run the following command to check the version:
fuelup --version
The output may look similar to:
fuelup 0.13.0
Cargo
Cargo can be used to install the Sway toolchain with various plugins
.
Dependencies
A prerequisite for installing and using Sway is the Rust toolchain
running on the stable
channel.
After installing the Rust toolchain
run the following command to check default channel:
rustup toolchain list
The output may look similar to:
stable-x86_64-unknown-linux-gnu (default)
Installation & Updating
The Sway toolchain
can be installed/updated with:
cargo install forc
From Source
The Sway toolchain
can be built directly from the Sway repository
.
Installation & Updating
In the root of the repository /sway/<here>
build forc
with the following command:
cargo build
The executable binary can be found in /sway/target/debug/forc
.
Using the Toolchain
After installing run the following command to check the version:
/sway/target/debug/forc --version
The output may look similar to:
forc 0.31.2
Fuel Core
The Fuel toolchain
is an extension of the Sway toolchain
.
It consists of a full node known as Fuel Core
and it enables deployment and testing via the Rust SDK
.
Installation & Updating
Fuel Core
can be installed/updated with:
cargo install fuel-core
There may be additional system dependencies
required for installation.
Sway Program Types
A Sway program is a file ending with the extension .sw
, e.g. main.sw
, and the first line of the file is a declaration of the type of program.
A Sway program can be one of four types:
- contract
- Primarily used for protocols or systems that operate within a fixed set of rules e.g. staking contracts, decentralized exchanges, etc.
- library
- Reusable code for handling common operations
- script
- Used for complex, multi-step, on-chain interactions that won't persist, such as using a decentralized exchange to create a leveraged position (borrow, swap, re-collateralize, borrow)
- predicate
- A set of preconditions to the construction of a transaction, the result of which must be a Boolean value of
true
in order for the transaction to be considered valid
- A set of preconditions to the construction of a transaction, the result of which must be a Boolean value of
Sway Project Types
A project type in Sway refers to which program type is in the main file of the project.
This means that there are four types of projects:
- contracts
- libraries
- scripts
- predicates
All four projects can contain multiple library files in the src
directory.
There is a caveat when it comes to contracts, scripts and predicates and it's as follows:
- A project can at most contain any one of a contract, script or predicate.
This means that a project cannot contain more than one contract, one script, one predicate and it cannot mix them together.
Entry Points
An entry point is the starting point of execution for a program.
Since a library is not directly deployable to the blockchain it does not have an entry point and instead its code is exported for use within other programs.
Unlike libraries; contracts, scripts and predicates all have an entry point. Contracts expose an Application Binary Interface (ABI)
while scripts and predicates expose a main()
function for entry.
Smart Contracts
A smart contract is a piece of bytecode that can be deployed to a blockchain via a transaction.
It can be called in the same way that an API may be called to perform computation and store and retrieve data from a database.
A smart contract consists of two parts:
Application Binary Interface (ABI
)
The ABI
is a structure which defines the endpoints that a contract exposes for calls. That is to say that functions defined in the ABI
are considered to be external
and thus a contract cannot call its own functions.
The following example demonstrates an interface for a wallet which is able to receive and send funds.
The structure begins by using the keyword abi
followed by the name of the contract.
Inside the declaration are function signatures, annotations denoting the interaction with storage and documentation comments outlining the functionality.
library;
abi Wallet {
/// When the BASE_ASSET is sent to this function the internal contract balance is incremented
#[storage(read, write)]
fn receive_funds();
/// Sends `amount_to_send` of the BASE_ASSET to `recipient`
///
/// # Arguments
///
/// - `amount_to_send`: amount of BASE_ASSET to send
/// - `recipient`: user to send the BASE_ASSET to
///
/// # Reverts
///
/// * When the caller is not the owner of the wallet
/// * When the amount being sent is greater than the amount in the contract
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient: Identity);
}
Implementation the ABI
Similar to traits in Rust implementing the ABI
is done with the syntax impl <name-of-abi> for Contract
.
All functions defined in the ABI
must be declared in the implementation.
Since the interface is defined outside of the contract we must import it using the use
syntax before we can use it.
contract;
use interface::Wallet;
impl Wallet for Contract {
#[storage(read, write)]
fn receive_funds() {
// function implementation
}
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient: Identity) {
// function implementation
}
}
Library
A library is used to contain code that performs common operations in order to prevent code duplication.
Definition
Libraries are defined using the library
keyword at the beginning of a file.
library;
Accessibility
Code defined inside a library, but more generally anywhere inside a Sway project, is considered to be private
which means that it is inaccessible to other files unless explicitly exposed.
Code can be exposed through a two step process:
- Add a
pub
keyword at the start of some code - Specify the library in the
Forc.toml
file as a dependency and then import thepub
declaration
library;
// Cannot import because the `pub` keyword is missing
fn foo() {}
// Can import everything below because they are using the `pub` keyword
pub const ONE = __to_str_array("1");
pub struct MyStruct {}
impl MyStruct {
pub fn my_function() {}
}
pub enum MyEnum {
Variant: (),
}
pub fn bar() {}
pub trait MyTrait {
fn my_function();
}
The following structures can be marked as pub
:
- Globally defined constants
- Structs
- Enums
- Functions
- Traits
Deployment
Libraries cannot be directly deployed to a blockchain, but they can be deployed as part of a contract.
Internal Libraries
A library is internal to a project if it is in the same source src
directory as the other program files.
$ tree
.
├── Cargo.toml
├── Forc.toml
└── src
├── lib.sw
└── my_library.sw
To be able to use our library my_library.sw
in lib.sw
there are two steps to take:
- Bring our library into scope by using the
mod
keyword followed by the library name - Use the
use
keyword to selectively import various items from the library
library;
mod my_library;
use my_library::bar;
// `bar` from `my_library` is now available throughout the file
External Libraries
An external library is a library that is outside of the src
directory (most likely in an entirely different project).
$ tree
.
├── my_library
│ ├── Cargo.toml
│ ├── Forc.toml
│ └─── src
│ └── lib.sw
│
└── my_other_library
├── Cargo.toml
├── Forc.toml
└─── src
└── lib.sw
Libraries
my_other_library
my_other_library
has a function quix()
which can be imported into my_library
because it uses the pub
keyword.
library;
pub fn quix() {}
my_library
To be able to use quix()
inside my_library
there are two steps to take.
Add to Dependencies
Add my_other_library
as a dependency under the [dependencies]
section of the my_library
Forc.toml
file.
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "lib.sw"
license = "Apache-2.0"
name = "my_library"
[dependencies]
my_other_library = { path = "../my_other_library" }
std = { path = "../../../../../../../../../sway-lib-std" }
Import
Use the use
keyword to selectively import our code from my_other_library
library;
use my_other_library::quix;
// `quix` from `my_other_library` is now available throughout the file
Scripts
A script is an executable that does not need to be deployed because it only exists during a transaction.
It can be used to replicate the functionality of contracts, such as routers, without the cost of deployment or increase of the blockchain size.
Some properties of a script include:
- It cannot be called by a contract
- It is stateless but can interact with storage through a contract
- Can call multiple contracts
Example
The following example demonstrates a script which takes one argument and returns the Boolean value of true
.
script;
// All scripts require a main function. The return type is optional.
fn main(amount: u64) -> bool {
true
}
Predicates
A predicate is an executable that represents a UTXO spending condition, such as a multisig predicate, which has restrictions on the VM instructions that can be used .
It does not need to be deployed to a blockchain because it only exists during a transaction. That being said, the predicate root is on chain as the owner of one or more UTXOs.
Predicates can neither read from nor write to any contract state. Moreover, they cannot use any contract instructions.
Transfer Coins to a Predicate
In Fuel, coins can be sent to an address uniquely representing a particular predicate's bytecode (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
.
Spending Conditions
Predicates may introspect the transaction spending their coins (inputs, outputs, script bytecode, etc.) and may take runtime arguments (the predicateData
), either or both of which may affect the evaluation of the predicate.
Example
Similar to a script, a predicate consists of a single main()
function which can take any number of arguments but must return a Boolean. In order for the predicate to be valid, the returned Boolean value must be true
.
predicate;
// All predicates require a main function which return a Boolean value.
fn main(amount: u64) -> bool {
true
}
Built-in Types
Sway is a statically typed language therefore every value must be known at compile time. This means that each value must have a type and the compiler can usually infer the type without the user being required to specify it.
Sway provides a number of out-of-the-box (primitive) types which can be used to construct more complex data structures and programs.
Primitive Types
Sway has the following primitive types:
- Numerics
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)hexadecimal
,binary
&base-10
syntax
- Boolean
bool
(true or false)
- Strings
str
(string slice)str[n]
(fixed-length string of size n)
- Bytes
b256
(256 bits / 32 bytes, i.e. a hash)
The default numeric type is u64
. The FuelVM's word size is 64 bits, and the cases where using a smaller numeric type to save space are minimal.
All other types in Sway are built up of these primitive types, or references to these primitive types.
Compound Types
Compound types are types that group multiple values into one type.
Sway has the following compound types:
Numeric Types
Broadly speaking there are two types of integers:
Signed Integers
A signed integer is a whole number which can take the value of zero and both negative and positive values. This means that a signed integer can take values such as:
- -42
- 0
- 42
In order to achieve this one bit must be kept for tracking the sign (+ or -) of the value and thus the range of available values is smaller than an unsigned integer.
For those inclined, the range for an n-bit signed integer is -2n-1 to 2n-1-1.
Sway does not natively support signed integers however there is nothing stopping a library from using primitives to create types that act like signed types.
Unsigned Integers
An unsigned integer is a whole number which can take the value of zero or any positive number, but cannot be negative. This allows for one more bit of values to be used for the positive numbers and thus the positive range is significantly larger than for signed integers.
An example of available values is:
- 0
- 42
For those inclined, the range for an n-bit unsigned integer is 0 to 2n-1.
Alternative Syntax
All of the unsigned integer types are numeric types, and the byte
type can also be viewed as an 8-bit unsigned integer.
Numbers can be declared with binary syntax, hexadecimal syntax, base-10 syntax, and underscores for delineation.
let hexadecimal = 0xffffff;
let binary = 0b10101010;
let base_10 = 10;
let underscore_delineated_base_10 = 100_000;
let underscore_delineated_binary = 0x1111_0000;
let underscore_delineated_hexadecimal = 0xfff_aaa;
Boolean Type
A Boolean is a type that is represented by either a value of one or a value of zero. To make it easier to use the values have been given names: true
& false
.
Boolean values are typically used for conditional logic or validation, for example in if expressions, and thus expressions are said to be evaluated to true
or false
.
Using the unary operator !
the Boolean value can be changed:
- From
true
tofalse
- From
false
totrue
Example
The following example creates two Boolean variables, performs a comparison using the unary operator !
and implicitly returns the result.
fn returns_true() -> bool {
let is_true = true;
let is_false = false;
// implicitly returns the Boolean value of `true`
is_true == !is_false
}
String Type
A string is a collection of characters (letters, numbers etc.).
Sway has one string type and it's a fixed length string which has the following implications:
- A string cannot be grown or shrunk during execution
- The content of the string must meet its length
- This could be via a legitimate value that takes up the entire length or through padding
The reason for this is that the compiler must know the size of the type and the length is a part of the type.
A string can be created through the use of double-quotation marks "
around the text. The length of the string is permanently set at that point and cannot be changed even if the variable is marked as mutable.
// The variable `fuel` is a string slice with length equals 4
let fuel = "fuel";
let crypto = __to_str_array("crypto");
Strings default to UTF-8 in Sway.
Bytes
Sway has a single "bytes" type which is the b256
.
As the name suggests it contains 256 bits
/ 32 bytes
of information. Unlike some other programming languages this type is treated as a single, whole, type unlike an array of bytes which is iterated over.
let zero = 0x0000000000000000000000000000000000000000000000000000000000000000;
Tuples
A tuple is a general-purpose static-length aggregation of types, in other words, it's 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.
Declare
To declare a tuple we wrap the values in ()
.
// Define a tuple containing 2 u64 types
let mut balances = (42, 1337);
Retrieve by Index
Values can be retrieved individually from the tuple by specifying the index.
// first = 42, second = 1337
let first = balances.0;
let second = balances.1;
Mutate
A value can be mutated in a tuple as long as the tuple is declared to be mutable and the new value has the same type as the previous value.
// 12 has the same type as 42 (u64) therefore this is valid
balances.0 = 12;
// true is a Boolean and the tuple expects a u64 therefore this is invalid
// balances.0 = true;
The entire tuple can be overwritten when it is mutable and the type for each value is the same.
// 3 is the same type as 42 (u64) and so is 4 and 1337 therefore this is valid
balances = (3, 4);
Destructure
Elements can be destructured from a tuple into individual variables.
// first = 42, second = 1337
let (first, second) = balances;
We can also ignore elements when destructuring.
// 42 is ignored and cannot be used
let (_, second) = balances;
Arrays
An array is similar to a tuple, but an array's values must all be of the same type. It's defined using square brackets []
and separates its values using commas.
Unlike a tuple, an array can be iterated over through indexing.
fn syntax() {
let array = [1, 2, 3, 4, 5];
let mut counter = 0;
let mut total = 0;
while counter < 5 {
total += array[counter];
counter += 1;
}
}
Arrays are allocated on the stack and thus the size of an array is considered to be static
. What this means is that once an array is declared to have a size of n
it cannot be changed to contain more, or fewer, elements than n
.
Structs
A struct in Sway is a product type
which is a data structure that allows grouping of various types under a name that can be referenced, unlike a tuple. The types contained in the struct are named and thus they can be referenced by their names as well.
Declaration
The following syntax demonstrates the declaration of a struct named Foo
containing two fields - public field bar
, a u64
, and a private field baz
, a bool
.
struct Foo {
pub bar: u64,
baz: bool,
}
Public fields are accessible in all the modules in which the struct is accessible. Private fields are accessible only within the module in which the struct is declared.
Instantiation
To instantiate a struct the name of the struct must be used followed by {}
where the fields from the declaration must be specified inside the brackets. Instantiation requires all fields to be initialized, both private and public.
fn hardcoded_instantiation() {
// Instantiate the variable `foo` as `Foo`
let mut foo = Foo {
bar: 42,
baz: false,
};
// Access and write to "baz"
foo.baz = true;
}
fn variable_instantiation() {
// Declare variables and pass them into `Foo`
let number = 42;
let boolean = false;
let mut foo = Foo {
bar: number,
baz: boolean,
};
// Access and write to "baz"
foo.baz = true;
}
fn shorthand_instantiation() {
// 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;
}
Structs with private fields can be instantiated only within the module in which the struct is declared.
Destructuring
The fields of a struct can be accessed through destructuring.
fn destructuring() {
let foo = Foo {
bar: 42,
baz: false,
};
// bar and baz are now accessible as variables
let Foo { bar, baz } = foo;
if baz {
let quix = bar * 2;
}
// You may use `..` to omit the remaining fields if the types match
// The compiler will fill them in for you
let Foo { bar, .. } = foo;
}
When destructuring structs with private fields outside of a module in which the struct is defined, the private fields must be omitted by using the ..
.
Enums
An enum, also known as a sum type
, is a type that consists of several variants where each variant is named and has a type.
Let's take a look at an example where we define an enum called Color
with a few color variations.
enum Color {
Blue: (),
Green: (),
Red: (),
Silver: (),
Grey: (),
}
We begin by using the enum
keyword followed by the name for our enumeration. The variants are contained inside {}
and they are ordered sequentially from top to bottom. Each variant has a name, such as the first Blue
variant, and a type, which in this case is the unit type ()
for all variants.
The unit type is a type that does not contain any data however any type can be used.
// To instantiate an enum with a variant of the unit type the syntax is
let blue = Color::Blue;
let silver = Color::Silver;
Enums of Structs
In order to demonstrate more complex data types we can define a struct and assign that struct as a data type for any of an enum's variants.
Here we have a struct Item
and an enum MyEnum
. The enum has one variant by the name Product
and its type is declared to the right of :
which in this case is our struct Item
.
struct Item {
amount: u64,
id: u64,
price: u64,
}
enum MyEnum {
Product: Item,
}
fn main() {
let my_enum = MyEnum::Product(Item {
amount: 2,
id: 42,
price: 5,
});
}
Enums of Enums
Similar to structs we can use other enums as types for our variants.
enum UserError {
InsufficientPermissions: (),
Unauthorized: (),
}
enum Error {
UserError: UserError,
}
fn main() {
let my_enum = Error::UserError(UserError::Unauthorized);
}
Variables
A variable is a way to reference some information by a specific name and it can take the form of a variety of data structures.
In Sway there are two states that a variable can take:
- Immutable
- Can be read but cannot be changed after it has been declared
- Mutable
- Can be read and can have its value changed if and only if the new value is the same type
By default all variables in Sway are immutable unless declared as mutable through the use of the mut
keyword. This is one of the ways in which Sway encourages safe programming, and many modern languages have the same default.
In the following sections, we'll take a look at two keywords that are used to instantiate information (let & const) and a way to temporarily reuse a variable name without affecting the original instantiation through variable shadowing.
let
The let
keyword is used to assign a value to a variable during run-time. It can only be used inside of a function and its value can be changed when declared as mutable.
Immutable
We can declare a variable that cannot have its value changed in the following way.
let foo = 5;
By default foo
is an immutable u64
with the value of 5
. This means that we can pass foo
around and its value can be read, but it cannot have its value changed from 5
to any other value.
Mutable
We can declare a variable that can have its value changed through the use of the mut
keyword.
let mut foo = 5;
foo = 6;
Constants
Constants are similar to immutable let 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.
Declaration
To define a constant the const
keyword is used followed by a name and an assignment of a value.
const FOO = 5;
The example above hardcodes the value of 5
however function calls may also be used alongside built-in types.
impl self
Constants
Constants can also be declared inside impl
blocks. In this case, the constant is referred to as an associated constant.
struct Point {
x: u64,
y: u64,
}
impl Point {
const ZERO: Point = Point { x: 0, y: 0 };
}
fn main() -> u64 {
Point::ZERO.x
}
Shadowing
When looking at the let variable we've seen that the value can be changed through the use of the mut
keyword. We can take this a couple steps further through reassignment and variable shadowing. Note that shadowing applies only to variables. Constants cannot be shadowed.
Reassignment
We can redefine the type and value of a variable by instantiating a new version after the first declaration.
// Set `foo` to take the value of `5` and the default `u64` type
let foo = 5;
// Reassign `foo` to be a `str` with the value of `Fuel`
let foo = "Fuel";
Variable Shadowing
If we do not want to alter the original variable but we'd like to temporarily reuse the variable name then we can use block scope to constrain the variable.
let foo = 5;
{
let foo = 42;
}
assert(foo == 5);
foo
is defined inside the curly brackets { }
and only exist inside the { .. }
scope; therefore, the original foo
variable with the value of 5
maintains its value.
Comments
There are two kinds of comments in Sway.
- Regular Comments are used for conveying information to the reader of the source code
- Documentation Comments are used for documenting functionality for external use
Regular Comments
Regular comments are broken down into two forms of syntax:
// comment
/* comment */
The first form starts after the two forward slashes and continues to the end of the line.
Comments can be placed on multiple lines by starting each line with //
and they can be placed at the end of some code.
// imagine that this line is twice as long
// and it needed to be split onto multiple lines
let baz = 8; // Eight is a good number
Similarly, the second form continues to the end of the line and it can also be placed at the end of some code.
/*
imagine that this line is twice as long
and it needed to be split onto multiple lines
*/
Documentation Comments
Documentation comments start with three forward slashes ///
and are placed on top of functions or above fields e.g. in a struct.
Documentation comments are typically used by tools for automatic documentation generation.
/// Data structure containing metadata about product XYZ
struct Product {
/// Some information about field 1
field1: u64,
/// Some information about field 2
field2: bool,
}
/// Creates a new instance of a Product
///
/// # Arguments
///
/// - `field1`: description of field1
/// - `field2`: description of field2
///
/// # Returns
///
/// A struct containing metadata about a Product
fn create_product(field1: u64, field2: bool) -> Product {
Product { field1, field2 }
}
Functions, methods, and associated functions
Functions, and by extension methods and associated functions, are a way to group functionality together in a way that allows for code reuse without having to re-write the code in each place that it is used.
The distinction between a function, method and associated function is as follows:
- A function is a grouping of code that is independent of any object
- A method is a function that is associated with an object and it uses
self
as the first parameter - An associated function is a method but without the
self
parameter
Function Declaration
A function declaration consists of a few components
- The
fn
keyword - A unique name for the function
- Comma separated optional parameters, and their types, inside
()
- An optional return type
Here is a template that applies to the aforementioned functions.
fn my_function(my_parameter: u64 /* ... */ ) -> u64 {
// function code
42
}
Functions
In this section we will define a function that takes two numerical inputs and returns a Boolean value indicating whether they are equal. We will also take a look at how to use the function.
Declaration
The following function is called equals
and it takes two parameters of type u64
(64-bit unsigned integers). It performs a comparison and implicitly returns the result of that comparison.
fn equals(first_parameter: u64, second_parameter: u64) -> bool {
first_parameter == second_parameter
}
Usage
The following is a way to use the function defined above.
let result_one = equals(5, 5); // evaluates to `true`
let result_two = equals(5, 6); // evaluates to `false`
Methods
Methods are defined within the context of a struct (or enum) and either refer to the type or mutate it.
The first parameter of a method is always self
, which represents the instance of the type the method is being called on.
Declaration
In this example we will take a look at a struct however an enum will work in the same way.
struct Foo {
bar: u64,
}
We start by using the impl
(implementation) keyword, followed by the name of our struct, to define a function that belongs to our object i.e. a method.
impl Foo {
// refer to `bar`
fn add_number(self, number: u64) -> u64 {
self.bar + number
}
// mutate `bar`
fn increment(ref mut self, number: u64) {
self.bar += number;
}
}
Usage
To call a method use the dot syntax: <variable name>.<method name>()
.
let mut foo = Foo { bar: 42 };
let result = foo.add_number(5); // evaluates to `47`
foo.increment(5); // `bar` inside `foo` has been changed from 42 to 47
Associated Functions
Associated functions are similar to methods in that they are also defined in the context of a struct or enum, but they do not 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
A distinguished family of associated functions of a specific type are type constructors. Constructors are associated functions that construct, or in other words instantiate, new instances of a type. Their return type always includes the type itself, and is often just the type itself.
Public structs that have private fields must provide a public constructor, or otherwise cannot be instantiated outside of the module in which they are declared.
Declaration
In this example we will take a look at a struct; however, an enum will work in the same way.
struct Foo {
bar: u64,
}
We start by using the impl
(implementation) keyword, followed by the name of our struct, to define a function that belongs to our object i.e. a method.
impl Foo {
// this is an associated function because it does not take `self` as a parameter
// it is also a constructor because it instantiates
// and returns a new instance of `Foo`
fn new(number: u64) -> Self {
Self { bar: number }
}
}
Usage
To call an associated function on a type we use the following syntax.
let foo = Foo::new(42);
Returning from functions
In the previous sections we have seen how functions return values without going into detail. In this section we will take a closer look at how we can return data from a function.
There are two ways to return:
When returning data from a function the return types must match up with the return types declared in the function signature. This means that if the first return type is a u64
then the type of the first value being returned must also be a u64
.
Explicit Return
To return from a function explicitly we use the return
keyword followed by the arguments and a semi-colon.
fn main() -> bool {
return true;
}
A return expression is typically used at the end of a function; however, it can be used earlier as a mechanism to exit a function early if some condition is met.
fn return_data(parameter_one: u64, parameter_two: bool) -> (bool, u64) {
if parameter_two {
return (!parameter_two, parameter_one + 42);
}
return (parameter_two, 42);
}
Implicit Return
To return from a function implicitly we do not use the return
keyword and we omit the semi-colon at the end of the line.
fn main() -> bool {
true
}
An implicit return is a special case of the explicit return. It can only be used at the end of a function.
fn return_data(parameter_one: u64, parameter_two: bool) -> (bool, u64) {
if parameter_two {
(!parameter_two, parameter_one + 42)
} else {
(parameter_two, 42)
}
}
Control Flow
A control flow in a program is the order in which instructions are executed.
For example, a function may take an input u64
and if the value is greater than 5
then it calls one function otherwise it calls a different function.
Controlling the order of instructions can be done through the use of conditional expressions such as if and match and through looping.
if expressions
Sway supports if, else, and else if expressions which provide control over which instructions should be executed depending on the conditions.
Conditional Branching
In the following example we have a hardcoded variable number
set to the value of 5
which is put through some conditional checks.
let number = 5;
if number % 3 == 0 {
// call function 1
} else if number % 4 == 0 {
// call function 2
} else {
// call function 3
}
// more code here
The conditional checks are performed in the order that they are defined therefore the first check is to see if the number
is divisible by 3
.
If the condition evaluates to the Boolean value of true
then we call function 1
and we move on to the end where the comment more code here
is written. We do not evaluate the remaining conditions.
On the other hand if the condition evaluates to false
then we check the next condition, in this case if the number
is divisible by 4
. We can have as many else if
checks as we like as long as they evaluate to a Boolean.
At the end there is a special case which is known as a catch all
case i.e. the else
. What this means is that we have gone through all of our conditional checks above and none of them have been met. In this scenario we may want to have some special logic to handle a generic case which encompases all the other conditions which we do not care about or can be treated in the same way.
Using if & let together
In Conditional Branching we have opted to call some functions depending on which condition is met however that is not the only thing that we can do. Since if
's are expressions in Sway we can use them to match on a pattern.
if let
In the following examples we combine if
and let
into if let
followed by some comparison which must evaluate to a Boolean.
enum Foo {
One: (),
Two: (),
}
Example 1
Here we check to see if the hardcoded variable one
is the same as the first variant of Foo
.
let one = Foo::One;
let mut result = 0;
if let Foo::One = one {
result = 1;
}
Example 2
Alternatively, we can take the outcome of the comparison and assign it directly to a variable.
let one = Foo::One;
let result = if let Foo::One = one {
1
} else {
2
};
The syntax above can be altered to include an else if
.
match
If expressions can be used to check a large number of conditions however, there is an alternative syntax which allows us to perform advanced pattern matching.
A match
expression matches on a variable and checks each case, also known as an arm
, to see which branch of logic should be performed.
The cases are checked sequentially in the order they are declared, i.e. from top to bottom, and the last arm must ensure that all cases in the pattern are covered otherwise the compiler will not know how to handle an unspecified pattern and will throw an error.
In the following sections we'll look at:
- A primitive case where a single line of code is used in a case
- Expand the first example to use code blocks in the multi line case
- Look at complex pattern matching to demonstrate their utility
Single Line Arm
The following example demonstrates how a type can be matched on and its output is assigned to a variable. The assignment to a variable is optional.
let number = 5;
let result = match number {
0 => 10,
1 => 20,
5 => 50,
6 | 7 => 60,
catch_all => 0,
};
The left side of the arrow =>
is the pattern that we are matching on and the right side of the arrow =>
is the logic that we want to perform, in this case we are returning a different multiple of 10
depending on which arm is matched.
We check each arm starting from 0
and make our way down until we either find a match on our pattern or we reach the catch_all
case.
The |
operator can be used to produce a pattern that is a disjuction of other patterns.
The catch_all
case is equivalent to an else
in if expressions and it does not have to be called catch_all
. Any pattern declared after a catch_all
case will not be matched because once the compiler sees the first catch_all
it stop performing further checks.
Multi Line Arm
The arm of a match
expression can contain multiple lines of code by wrapping the right side of the arrow =>
in brackets {}
.
let number = 5;
let result = match number {
0 => {
// Multiple lines of code here then return 10
10
},
1 => 20,
5 => 50,
catch_all => 0,
};
Complex Patterns
Match expressions are meant to cover advanced patterns so the following sections demonstrate some examples:
- enum variants
- structs and their fields
- constant variables
- nested match expressions
- matching on multiple values
Enums
An enum can be matched on by specifying the name of the enum and the variant.
enum Color {
Red: (),
Green: (),
Blue: (),
}
fn enum_match(input: Color) {
let result = match input {
Color::Red => 0,
Color::Green => 1,
Color::Blue => 2,
};
}
Structs
We can match on specific arguments inside a struct while ignoring the rest by using ..
.
struct Point {
x: u64,
y: u64
}
fn struct_matching() {
let point = Point {
x: 1u64,
y: 2u64,
};
let result = match point {
Point { x: 5, y } => y + 1,
Point { x, .. } => x,
Point { y, .. } => y,
_ => 42,
};
}
If the struct is imported from another module and has private fields, the private fields must always be ignored by using ..
.
Constants
Variables can be matched on but only if they are constants.
const NUMBER_1: u64 = 7;
const NUMBER_2: u64 = 14;
fn constant_match() {
let number = 5;
let result = match number {
NUMBER_1 => 1,
NUMBER_2 => 42,
other => other,
};
}
Nested Expressions
We can nest match
expressions by placing them inside code blocks.
enum TopLevel {
One: (),
Two: SecondLevel,
}
enum SecondLevel {
Value1: u64,
Value2: (),
}
fn nested_match(input: TopLevel) -> u64 {
match input {
TopLevel::One => 1,
TopLevel::Two(second) => {
match second {
SecondLevel::Value1(2) => 2,
SecondLevel::Value1(_) => 3,
SecondLevel::Value2 => 42,
}
},
}
}
Multiple Values
We can match
on multiple values by wrapping them in a tuple and then specifying each variant in the same structure (tuple) that they have been defined.
use core::ops::Eq;
enum Binary {
True: (),
False: (),
}
impl Eq for Binary {
fn eq(self, other: Self) -> bool {
match (self, other) {
(Binary::True, Binary::True) => true,
(Binary::False, Binary::False) => true,
_ => false,
}
}
}
Looping
A loop is a type of operation which allows us to perform computation a certain number of times. For example, given a collection of items we could call a method on the first item and iterate until the method has been called on each item.
Usually, a loop has a condition which prevents it from continuing indefinitely however it is possible to create a loop that never stops i.e. an infinite loop.
Programming languages have various forms of syntax for declaring a loop which may slightly alter how the iteration takes place.
Sway has the following loops:
while
A while
loop uses the while
keyword followed by a condition which evaluates to a Boolean.
let mut counter = 0;
let mut condition = true;
while counter < 10 && condition {
counter += 1;
if 5 < counter {
condition = false;
}
}
In the example above we use two conditions.
- If the
counter
is less than10
then continue to iterate - If the
condition
variable istrue
then continue to iterate
As long as both those conditions are true
then the loop will iterate. In this case the loop will finish iterating once counter
reaches the value of 6
because condition
will be set to false
.
Nested loops
Sway also allows nested while
loops.
while true {
// computation here
while true {
// more computation here
}
}
break
break
is a keyword available for use inside of a while
loop and it is used to exit out of the loop before the looping condition is met.
let mut counter = 0;
while counter < 10 {
counter += 1;
if 5 < counter {
break;
}
}
In the example above the while
loop is set to iterate until counter
reaches the value of 10
however the if expression will break out of the loop once counter
reaches the value of 6
.
continue
continue
is a keyword available for use inside of a while
loop and it is used to skip to the next iteration without executing the code after continue
.
let mut counter = 0;
while counter < 10 {
counter += 1;
if counter % 2 == 0 {
continue;
}
// "other code"
}
In the example above the while
loop is set to iterate until counter
reaches the value of 10
however the if expression will skip (not execute) the "other code" when counter
is an even value. For example, this could be used to add all the odd numbers from 0
to 10
.
Annotations
Types
Sway is a compiled language and as such each data structure has a definition i.e. a type
which has some size
that must be allocated on the stack.
The compiler can usually infer the type
based on its usage however there may be occasions where the compiler cannot make the inference or the developer may deem it more useful to explicitly annotate a variable in order to make the code easier to read.
Annotating a variable is done by placing the annotation after the variable name but before the assignment (the =
sign).
let bar: str = "sway";
let baz: bool = true;
The compiler will disallow incorrect type
annotations therefore replacing the bool
annotation on the variable baz
with a u64
will result in a compilation error.
Attributes
An attribute is a metadatum which provides some additional functionality.
Storage
A storage attribute indicates the purity of a function i.e. whether it:
- reads from storage
- writes to storage
- reads from and writes to storage
- does not read or write (is pure)
When a function is pure the annotation is omitted otherwise the correct annotation must be placed above the function signature.
More information about storage can be found in the common storage operations section.
Reading from Storage
When we read from storage we use the read
keyword.
#[storage(read)]
Writing to Storage
When we write to storage we use the write
keyword.
#[storage(write)]
Reading & Writing
When we read from and write to storage we use the read
& write
keywords.
#[storage(read, write)]
Payable
The payable annotation is used to allow a contract function to accept an asset forwarded via a call.
Usage
To allow a contract to accept assets we use the payable
keyword.
#[payable]
fn deposit();
Test
Sway provides the #[test]
attribute which enables unit tests to be written in Sway.
Success case
The #[test]
attribute indicates that a test has passed if it did not revert.
#[test]
fn equal() {
assert_eq(1 + 1, 2);
}
Revert Case
To test a case where code should revert we can use the #[test(should_revert)]
annotation. If the test reverts then it will be reported as a passing test.
#[test(should_revert)]
fn unequal() {
assert_eq(1 + 1, 3);
}
We may specify a code to specifically test against.
#[test(should_revert = "18446744073709486084")]
fn assert_revert_code() {
assert(1 + 1 == 3);
}
#[test(should_revert = "42")]
fn custom_revert_code() {
revert(42);
}
Allow
Dead code
The #[allow(dead_code)]
annotation disables warnings which are emitted by the compiler for code that is unused.
#[allow(dead_code)]
fn unused_function() {}
Deprecated
The #[allow(deprecated)]
annotation disables warnings which are emitted by the compiler for usage of deprecated items.
#[deprecated(note = "this is deprecated")]
struct DeprecatedStruct {}
#[allow(deprecated)]
fn using_deprecated_struct() {
let _ = DeprecatedStruct {};
}
Inline
When making a call the compiler may generate code to call a function where it is defined or it may copy the function code (inline) to prevent additional code generation.
The Sway compiler automatically inlines functions based on internal heuristics; however, the inline
attribute may be used to suggest, but not require, code generation or code copying.
Generate code
To suggest code generation use the never
keyword.
#[inline(never)]
fn foo() {}
Copy code
To suggest code copy use the always
keyword.
#[inline(always)]
fn bar() {}
Deprecated
This annotation marks an item as deprecated, which makes the compiler to emit a warning for each usage of the item. This warning can be disabled using #[allow(deprecated)]
.
It is also possible to customize the warning message using the argument note
.
#[deprecated(note = "this is deprecated")]
struct DeprecatedStruct {}
#[allow(deprecated)]
fn using_deprecated_struct() {
let _ = DeprecatedStruct {};
}
Traits
A trait describes an abstract interface that types can implement. This interface consists of an interface surface
of associated items, along with methods
.
trait Trait {
fn fn_sig(self, b: Self) -> bool;
} {
fn method(self, b: Self) -> bool {
true
}
}
Associated items come in two varieties:
All traits define an implicit type parameter Self
that refers to "the type that is implementing this interface".
Traits may also contain additional type parameters. These type parameters, including Self
, may be constrained by
other traits and so forth as usual.
Traits are implemented for specific types through separate implementations.
Associated functions
Trait functions consist of just a function signature. This indicates that the implementation must define the function.
Associated constants
Associated constants are constants associated with a type.
An associated constant declaration declares a signature for associated constant definitions.
It is written as const
, then an identifier, then :
, then a type, finished by a ;
.
The identifier is the name of the constant used in the path. The type is the type that the definition has to implement.
An associated constant definition defines a constant associated with a type.
Associated constants examples
script;
trait T {
const C: bool;
}
struct S {}
impl T for S {
const C: bool = true;
}
fn main() -> bool {
let s = S {};
S::C
}
Associated constants may omit the equals sign and expression to indicate implementations must define the constant value.
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 examples
script;
trait TypeTrait {
type T;
fn method(self, s1: Self::T) -> Self::T;
}
struct Struct {}
struct Struct2 {}
impl TypeTrait for Struct2 {
type T = Struct;
fn method(self, s1: Self::T) -> Self::T {
s1
}
}
fn main() -> u32 {
Struct2{}.method(Struct{});
1
}
Generics
Style Guide
Programming languages have different ways of styling code i.e. how variables, functions, structures etc. are written.
The following snippets present the style guide for writing Sway
.
TODO: overview of content
Naming Convention
A naming convention is a set of rules used to standardize how code is written.
CapitalCase
Structs, traits, and enums are CapitalCase
which means each word has a capitalized first letter. The fields inside a struct should be snake_case and CapitalCase
inside an enum.
struct MultiSignatureWallet {
owner_count: u64,
}
trait MetaData {
// code
}
enum DepositError {
IncorrectAmount: (),
IncorrectAsset: (),
}
snake_case
Modules, variables, and functions are snake_case
which means that each word is lowercase and separated by an underscore.
Module name:
library;
Function and variable:
fn authorize_user(user: Identity) {
let blacklist_user = false;
// code
}
SCREAMING_SNAKE_CASE
Constants are SCREAMING_SNAKE_CASE
which means that each word in capitalized and separated by an underscore.
const MAXIMUM_DEPOSIT = 10;
Type Annotations
When declaring a variable it is possible to annotate it with a type; however, the compiler can usually infer that information automatically.
The general approach is to omit a type if the compiler does not throw an error; however, if it is deemed clearer by the developer to indicate the type then that is also encouraged.
fn execute() {
// Avoid unless it's more helpful to annotate
let executed: bool = false;
// Generally encouraged
let executed = false;
}
Struct Shorthand
A struct has a shorthand notation for initializing its fields. The shorthand works by passing a variable into a struct with the exact same name and type.
The following struct has a field amount
with type u64
.
struct Structure {
amount: u64,
}
Using the shorthand notation we can initialize the struct in the following way.
fn call(amount: u64) {
let structure = Structure { amount };
}
The shorthand is encouraged because it is a cleaner alternative to the following.
fn action(value: u64) {
let amount = value;
let structure = Structure { amount: value };
let structure = Structure { amount: amount };
}
Enums
An enum
may contain many types including other enums.
pub enum Error {
StateError: StateError,
UserError: UserError,
}
pub enum StateError {
Void: (),
Pending: (),
Completed: (),
}
pub enum UserError {
InsufficientPermissions: (),
Unauthorized: (),
}
Encouraged
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:
let error1 = StateError::Void;
let error2 = UserError::Unauthorized;
Discouraged
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:
let error1 = Error::StateError(StateError::Void);
let error2 = Error::UserError(UserError::Unauthorized);
Key points to note:
- You must import all of the enums i.e.
Error
,StateError
&UserError
- The lines may get unnecessarily long (depending on the names)
- The syntax is unergonomic
Returning
In returning from functions we outline two styles of returning:
- Explicitly which uses the
return
keyword - Implicitly which does not use the
return
keyword
In general the preferred style is to follow the implicit return however both are perfectly acceptable.
Pattern Matching
The following examples present pattern matching using the match
keyword for the catch-all case.
Encouraged
The _
is used for the catch-all to indicate the important cases have been defined above and the last case is not important enough to warrant a name.
fn unnamed_case(shape: Shape) {
let value = match shape {
Shape::Triangle => 3,
Shape::Quadrilateral => 4,
Shape::Pentagon => 5,
_ => 0,
};
}
Alternative
We may apply an appropriate name to provide context to the reader; however, unless it provides additional information the preferred usage is defined in the encouraged
case.
fn named_case(shape: Shape) {
let value = match shape {
Shape::Triangle => 3,
Shape::Quadrilateral => 4,
Shape::Pentagon => 5,
_invalid_shape => 0,
};
}
Comments
In regular comments we outline two forms:
// comment
/* comment */
The first form is generally encouraged however there may be instances where a comment needs to be placed in the middle of some code in which case the second form is encouraged.
For example, in function declaration the second form is used to indicate additional parameters.
Getter Functions
Functions that return values typically follow one of two styles:
- Prepending
get_
to the start of the name - Omitting
get_
Encouraged
In Sway the encouraged usage is to omit the get_
prefix.
fn maximum_deposit() -> u64 {
100
}
Discouraged
That is to say the following is discouraged.
fn get_maximum_deposit() -> u64 {
100
}
Unused Variables
A good practice is naming variables appropriately; however, variables may be unused at times such as the timestamp
from the call()
.
fn unused_variable() -> u64 {
let (timestamp, deposit_amount) = call();
deposit_amount
}
Named
We may preserve the name to provide context to the reader by prepending the variable with _
.
fn named_unused_variable() -> u64 {
let (_timestamp, deposit_amount) = call();
deposit_amount
}
Nameless
We may discard the context and the value by assigning it to _
.
fn nameless_variable() -> u64 {
let (_, deposit_amount) = call();
deposit_amount
}
Intermediate Variables
An intermediate variable, or a temporary variable, is a variable that is typically used once. In most cases we avoid creating intermediate variables; however, there are cases where they may enrich the code.
Contextual Assignment
It may be beneficial to use an intermediate variable to provide context to the reader about the value.
fn contextual_assignment() {
let remaining_amount = update_state();
// code that uses `remaining_amount` instead of directly calling `update_state()`
}
Shortened Name
In the cases of multiple levels of indentation or overly verbose names it may be beneficial to create an intermediate variable with a shorter name.
fn shortened_name() {
let remaining_amount = update_state_of_vault_v3_storage_contract();
// code that uses `remaining_amount` instead of directly calling `update_state_of_vault_v3_storage_contract()`
}
Storage
A smart contract is able to perform computation and store & manipulate data over time.
In the following sections we'll take a look at how Sway handles storage
through:
- Storage Initialization: How to declare a
storage
block - Reading & Writing: How to read from and write to storage
- Libraries: Additional functionality provided by the storage library
Initialization
Storage is declared through the use of the storage
keyword.
Inside the storage
block each variable is named, associated with a type and a default value.
storage {
// variable_name1: variable_type1 = default_value1,
// variable_name2: variable_type2 = default_value2,
// ...
}
Example
In the following example we will take a look at two ways of storing a struct.
- Explicitly declaring the values in the
storage
block - Encapsulating the values in an associated function
We'll begin by defining the Owner
& Role
data structures and implement a default
constructor on the Owner
.
struct Owner {
maximum_owners: u64,
role: Role,
}
impl Owner {
// a constructor that can be evaluated to a constant `Owner` during compilation
fn default() -> Self {
Self {
maximum_owners: 10,
role: Role::FullAccess,
}
}
}
enum Role {
FullAccess: (),
PartialAcess: (),
NoAccess: (),
}
Now that we have our data structures we'll keep track of how many current_owners
we have and declare the owner in the two aformentioned styles.
storage {
current_owners: u64 = 0,
explicit_declaration: Owner = Owner {
maximum_owners: 10,
role: Role::FullAccess,
},
encapsulated_declaration: Owner = Owner::default(),
}
An explicit declaration is likely to be sufficient for most types. However, it may be preferable to encapsulate the initialization of complex types within a constructor in order to keep the code concise.
Note that the constructors used in storage
blocks must evaluate to a constant during compilation.
Reading & Writing
When dealing with storage we have two options, we can either read from or write to storage. In both cases we must use a storage annotation to indicate the purity of the function.
When referencing a variable in storage we must explicitly indicate that the variable comes from storage and not a local scope.
This is done via the syntax storage.variable_name
e.g. storage.counter
.
storage {
counter: u64 = 0,
}
Reading from Storage
When dealing with a built-in type we can retrieve the variable without the use of any special methods.
#[storage(read)]
fn read() {
let counter = storage.counter.read();
}
Writing to Storage
When dealing with a built-in type we can update the variable without the use of any special methods.
#[storage(write)]
fn write() {
storage.counter.write(storage.counter.read() + 1);
}
Read & Write
We can read and write to storage by using both keywords in the attribute.
#[storage(read, write)]
fn read_write() {
let counter = storage.counter.read();
storage.counter.write(counter + 1);
}
Libraries
The standard library provides additional utility for handling storage.
- Mapping: Tracking key-value pairs
- Vectors: Using a persistent (not heap-allocated) vector
- Store & Get: Manually manipulating storage slots
StorageMap
A StorageMap
, a.k.a. a hash table, is a structure which associates a value v
with a key k
. The key is used to find the position in the table (memory) where the value is stored.
The benefit of a hash table is that no matter where the value is in the table the computation required to find the location of that value is constant i.e. it has an order of 1 O(1)
.
Sway provides a flexible StorageMap
because it uses generics for both k
& v
with the caveat that k
and v
have to be a single value. The value can be a struct, tuple, array etc. therefore if you'd like to have a complex k
or v
then the data needs to be wrapped into a single type.
Declaration
The StorageMap
type is included in the prelude therefore we do not need to import it. We'll be using msg_sender()
in the subsequent section so we'll import that here.
After the import we initialize our StorageMap
as described in the initialization section.
storage {
// k = Identity, v = u64
balance: StorageMap<Identity, u64> = StorageMap::<Identity, u64> {},
// k = (Identity, u64), v = bool
user: StorageMap<(Identity, u64), bool> = StorageMap::<(Identity, u64), bool> {},
}
There are two storage
variables: balance
& user
. balance
takes a single value as the key while user
wraps two values into a tuple and uses that as a key.
Reading from Storage
Retrieving data from a storage variable is done through the .get(key)
method. That is to say that we state which storage variable we would like to read from and append .get()
to the end while providing the key for the data that we want to retrieve. The method get
returns an Option
; if there is no value for key
in the map, get
will return None
.
In this example we wrap the Identity
of the caller with their provided id
into a tuple and use that as the key.
#[storage(read)]
fn reading_from_storage(id: u64) {
let user = storage.user.get((msg_sender().unwrap(), id)).read();
}
This contract method handles the returned Option
by calling unwrap_or
to set user
to zero if the map user
doesn't have an entry for the key.
Writing to Storage
Writing to storage is similar to reading. The difference is that we use a different method .insert(key, value)
.
In this example we retrieve the balance of the caller and then increment their balance by 1.
#[storage(read, write)]
fn writing_to_storage() {
let balance = storage.balance.get(msg_sender().unwrap()).read();
storage.balance.insert(msg_sender().unwrap(), balance + 1);
}
StorageVec
A StorageVec
is a vector that permanently stores its data in storage
. It replicates the functionality of a regular vector however its data is not stored contigiously because it utilizes hashing and generics to find a location to store the value T
.
There is a number of methods in the standard library however we will take a look at pushing and retrieving data.
Declaration
To use a StorageVec
we need to import it from the standard library and while we're at it we'll import the msg_sender()
so that we can use it in the following section.
After the import we initialize our StorageVec
as described in the initialization section.
use std::storage::storage_vec::*;
storage {
// T = u64
balance: StorageVec<u64> = StorageVec {},
// T = (Identity, u64)
user: StorageVec<(Identity, u64)> = StorageVec {},
}
There are two storage
variables: balance
& user
. balance
takes a single value while user
wraps two values into a tuple.
Reading from Storage
Retrieving data from a storage variable is done through the .get(index)
method. That is to say that we state which index by specifying it inside .get()
and appending that to the end of the storage variable.
In this example we look at how we can retrieve a single value balance
and how we can unpack multiple values from user
.
#[storage(read)]
fn reading_from_storage(id: u64) {
let balance = storage.balance.get(id).unwrap();
let (user, value) = storage.user.get(id).unwrap().read();
}
Writing to Storage
Writing to storage is similar to reading. The difference is that we use a different method .push(value)
and we use the read
keyword because the implementation reads the length of the vector to determine where to store the value.
In this example we insert a tuple containing an the Identity
of the caller and some id
into the vector.
#[storage(read, write)]
fn writing_to_storage(id: u64) {
storage.user.push((msg_sender().unwrap(), id));
}
Store & Get
Storage can be manipulated directly through the use of store()
& get()
functions. They utilize generics to store and retrieve values.
Declaration
To use store()
& get()
we must import them however we are not required to declare a storage
block.
use std::storage::storage_api::{read, write};
Store
To store a generic value T
we must provide a key of type b256
.
In this example we store some number of type u64
.
#[storage(write)]
fn store(key: b256, value: u64) {
// write(key, SLOT, T) where T = generic type
write(key, 0, value);
}
Get
To retrieve a generic value T
at the position of key
we must specify the type that we are retrieving.
In this example we retrieve some u64
at the position of key
.
#[storage(read)]
fn get(key: b256) {
// read::<T>(key, SLOT) where T = generic type
let value = read::<u64>(key, 0);
}
The function get
returns an Option
; if the storage slots requested have not been set before, get
will return None
.
Assertions
An assertion is a condition which must evaluate to the Boolean value of true
and its purpose is to prevent undesirable computation when the condition is evaluated to false
.
For example, a function may only work if the condition argument < 5
is true
. We can use an assertion to enforce this condition by:
- Forcing a revert in the program when
5 <= argument
- Handling the exception with additional code
Handling exceptions may be done through if expressions therefore the following sections will take a look at how we can make the virtual machine revert (safely crash).
assert
: Checks if acondition
istrue
otherwise revertsrequire
: Checks if acondition
istrue
otherwise logs avalue
and revertsrevert
: Reverts the virtual machine with the provided exit codeassert_eq
: Checks ifa
andb
are equal otherwise revertsassert_ne
: Checks ifa
andb
are not equal otherwise reverts
assert
The assert
function is automatically imported into every program from the prelude and it takes an expression which must evaluate to a Boolean. If the Boolean is true
then nothing will happen and the code will continue to run otherwise the virtual machine will revert.
Example
Here we have a function which takes two u64
arguments and subtracts them. A u64
cannot be negative therefore the assertion enforces that b
must be less than or equal to a
.
If the condition is not met, then the virtual machine will revert.
fn subtract(a: u64, b: u64) -> u64 {
assert(b <= a);
a - b
}
require
The require
function is automatically imported into every program from the prelude and it takes an expression which must evaluate to a Boolean. If the Boolean is true
then nothing will happen and the rest of the code will continue to run otherwise a log will be emitted and the virtual machine will revert.
Example
Here we have a function which takes two u64
arguments and subtracts them. A u64
cannot be negative therefore the assertion enforces that b
must be less than or equal to a
.
If the condition is not met then the message b is too large
will be logged and the virtual machine will revert.
The message is generic therefore it can be any type, in this example it's a string.
fn subtract(a: u64, b: u64) -> u64 {
require(b <= a, "b is too large");
a - b
}
revert
The revert
function is automatically imported into every program from the prelude and it takes a u64
as an exit code.
The function will behave differently depending on the context in which it is used:
- When used inside a predicate the function will panic and crash the program
- Otherwise it will revert the virtual machine
Example
To manually force a revert we need to provide an exit code. To be able to distinguish one revert from another different exit codes can be used in different places.
revert(42);
assert_eq
The assert_eq
function is automatically imported into every program from the prelude. It takes two expressions which are compared and the result is a Boolean. If the value is false
then the virtual machine will revert.
Example
Here is a function which asserts that a
and b
must be equal.
fn compare_eq(a: u64, b: u64) {
assert_eq(a, b);
// code
}
assert_ne
The assert_ne
function is automatically imported into every program from the prelude. It takes two expressions which are compared and the result is a Boolean. If the value is false
then the virtual machine will revert.
Example
Here is a function which asserts that a
and b
must not be equal.
fn compare_ne(a: u64, b: u64) {
assert_ne(a, b);
// code
}
Address Namespace
Sway utilizies namespaces to distinguish between address types.
Having multiple address types enforces type-safety and expands the range of values that an address can take because the same value can be used across multiple types.
The main types are:
Address
: Used to identify the UTXOContractId
: Used to identify a contract
For ease of use there is an enum wrapper Identity
which contains both types.
Address
In the UTXO model each output has an address.
The Address
type is a struct containing a value of a b256
type.
pub struct Address {
bits: b256,
}
The value of an Address
is a hash of either:
- A public key
- Predicate
The Address
type is completely separate from a ContractId
and thus it should not be used when dealing with an address of a deployed contract.
Casting between an Address
and b256
can be done in the following way:
let variable1 = 0x000000000000000000000000000000000000000000000000000000000000002A;
let my_address = Address::from(variable1);
let variable2: b256 = my_address.into();
// variable1 == variable2
ContractId
A contract's ID is a unique, deterministic identifier analogous to a contract's address in the EVM. Contracts cannot own UTXOs but they can own assets.
The ContractId
type is a struct containing a value of a b256
type.
pub struct ContractId {
bits: b256,
}
Casting between an ContractId
and b256
can be done in the following way:
let variable1 = 0x000000000000000000000000000000000000000000000000000000000000002A;
let my_contract_id = ContractId::from(variable1);
let variable2: b256 = my_contract_id.into();
// variable1 == variable2
Identity
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 address = 0xddec0e7e6a9a4a4e3e57d08d080d71a299c628a46bc609aab4627695679421ca;
let my_address_identity = Identity::Address(Address::from(address));
let my_contract_identity = Identity::ContractId(ContractId::from(address));
Call Data
The term call-data
refers to the metadata that is available to the recipient of a call.
In the following sections we'll cover the following call-data
:
Message Sender
: who is making the callAsset Sent
: which asset has been sent into the contractAmount of Asset Sent
: how much of an asset has been sent
Message Sender
The standard prelude imports a function msg_sender()
automatically, which retrieves the Identity of the caller.
The identity can be used for a variety of reasons however a common application is access control i.e. restricting functionality for non-privileged users (non-admins).
Example
We can implement access control by specifying that only the owner can call a function.
In the following snippet we accomplish this by comparing the caller msg_sender()
to the OWNER
. If a regular user calls the function then it will revert otherwise it will continue to run when called by the OWNER
.
const OWNER = Identity::Address(Address::from(0x0000000000000000000000000000000000000000000000000000000000000000));
fn update() {
require(msg_sender().unwrap() == OWNER, "Owner Only");
// code
}
Asset Sent
The standard library provides a function msg_asset_id()
which retrieves the ContractId of the asset being sent.
This can be used to determine which asset has been sent into the contract.
Example
To use msg_asset_id()
we must import it from the standard library. We'll also import the base asset for comparison.
use std::{call_frames::msg_asset_id, constants::BASE_ASSET_ID};
We can check which asset has been sent and perform different computation based on the type.
fn deposit() {
if msg_asset_id() == BASE_ASSET_ID {
// code
} else {
// code
}
}
Amount of Asset Sent
The standard library provides a function msg_amount()
which retrieves the amount of asset sent without any concern for which asset is sent.
This can be used to set a price or manually track the amount sent by each user.
Example
To use msg_amount()
we must import it from the standard library.
use std::context::msg_amount;
We can check how much of any asset has been sent and if an incorrect amount has been sent then we may revert.
fn purchase() {
require(msg_amount() == 100_000_000, "Incorrect amount sent");
// code
}
Logging
Logging is a way to record data as the program runs.
The standard library provides a logging
module which contains a generic log
function that is used to log a variable of any type.
Each call to log
appends 1 of 2 types of a receipt
to the list of receipts
Log
- Generated for non-reference types:
bool
,u8
,u16
,u32
, andu64
- Generated for non-reference types:
LogData
- Generated for reference types and
u256
- Generated for reference types and
The Rust & Typescript SDKs may be used to decode the data.
Example
To use the log
function we must import it from the standard library and pass in any generic type T
that we want to log.
fn log_data(number: u64) {
// generic T = `number` of type `u64`
log(number);
}
In the example above a u64
is used however we can pass in any generic type such as a struct, enum, string etc.
Call a Contract
A common blockchain operation is communication between smart contracts.
Example
To perform a call there are three steps that we must take:
- Provide an interface to call
- Create a type that allows us to make a call
- Call a function on our interface
Defining the Interface
Let's take the example of a Vault
to demonstrate how a call can be performed.
library;
abi Vault {
#[payable]
fn deposit();
fn withdraw(amount: u64, asset: ContractId);
}
Creating a Callable Type
To call a function on our Vault
we must create a type that can perform calls. The syntax for creating a callable type is: abi(<interface-name>, <b256-address>)
.
Calling a Contract
The following snippet uses a script
to call our Vault
contract.
script;
use contract_interface::Vault;
fn main(amount: u64, asset_id: ContractId, vault_id: b256) -> bool {
let caller = abi(Vault, vault_id);
// Optional arguments are wrapped in `{}`
caller.deposit {
// `u64` that represents the gas being forwarded to the contract
gas: 10000,
// `u64` that represents how many coins are being forwarded
coins: amount,
// `b256` that represents the asset ID of the forwarded coins
asset_id: asset_id.into(),
}();
caller.withdraw(amount, asset_id);
true
}
The deposit()
function uses pre-defined optional arguments provided by the Sway
language.
Re-entrancy
Re-entrancy occurs when a contract makes a call back into the contract that called it, e.g. Contract A
calls Contract B
but then Contract B
makes a call back into Contract A
.
To mitigate security concerns there are two approaches that are commonly used:
- Implement a guard: detect when a re-entrancy occurs
- Defensive programming: perform calls after all state changes have been made
Re-entrancy Guard
Sway provides a stateless re-entrancy guard, which reverts at run-time when re-entrancy is detected.
To use the guard we must import it.
use reentrancy::reentrancy_guard;
Then call it in a contract function.
fn deposit() {
reentrancy_guard();
// code
}
Checks-Effects-Interactions Pattern
The pattern states that all state (storage) changes should be made before a call is made.
fn withdraw() {
// Step 1. Perform any state changes to update balance
// Step 2. After all state changes make a call
}
Asset Operations
A common application of a smart contract is the creation of an asset / token i.e. a cryptocurrency.
Managing a cryptocurrency is typically done via the following models:
- Account based e.g. Ethereum
- Unspent Transaction Output (UTXO) e.g. Bitcoin
Sway operates on the UTXO model therefore assets can be transferred out of the contract that created them. What this means is that keeping track of assets that have been transferred out of the contract may be more difficult because the information is not centralized in one place.
With that regard in mind, the account based approach can be partially replicated while utilizing certain asset operations that are build into the FuelVM.
The following sections will take a look at how an asset can be:
Minted
(created)Burned
(destroyed)Transferred
(sent)
While also taking a look at:
Minting
Minting an asset means to create a new asset with an id of the contract that created it.
The standard library contains a module
that can be used to mint an asset.
There are four functions that can be used to mint:
Mint
To use the function we must import it.
use std::asset::mint;
To mint some amount of an asset we specify the amount
that we would like to mint and pass it into the mint()
function.
let amount = 10;
mint(ZERO_B256, amount);
Mint to Address
We can mint
and transfer
the asset to an Address
.
To use the function we must import it.
use std::asset::mint_to_address;
To mint some amount of an asset we specify the amount
that we would like to mint and the Address
to send it to.
let amount = 10;
let address = 0x0000000000000000000000000000000000000000000000000000000000000001;
let user = Address::from(address);
mint_to_address(user, ZERO_B256, amount);
Mint to Contract
We can mint
and transfer
the asset to an Contract
.
To use the function we must import it.
use std::asset::mint_to_contract;
To mint some amount of an asset we specify the amount
that we would like to mint and the ContractId
to send it to.
let amount = 10;
let address = 0x0000000000000000000000000000000000000000000000000000000000000001;
let pool = ContractId::from(address);
mint_to_contract(pool, ZERO_B256, amount);
Mint to Address or Contract
We can mint
and transfers
to an Address
or a Contract
.
To use the function we must import it.
use std::asset::mint_to;
To mint some amount of an asset we specify the amount
that we would like to mint and the Identity
to send it to.
let amount = 10;
let address = 0x0000000000000000000000000000000000000000000000000000000000000001;
let user = Identity::Address(Address::from(address));
let pool = Identity::ContractId(ContractId::from(address));
mint_to(user, ZERO_B256, amount);
mint_to(pool, ZERO_B256, amount);
Burning
Burning an asset means to destroy an asset that a contract has minted
.
The standard library contains a module
that can be used to burn an asset.
There is one function used to burn:
To use the function we must import it.
use std::asset::burn;
burn
To burn some amount of an asset we specify the amount
that we would like to burn and pass it into the burn()
function.
let amount = 10;
burn(ZERO_B256, amount);
Transfer
The standard library contains a module
that can be used to transfer (send) an asset from one owner to another.
There are three functions that can be used to transfer an asset:
To Address
To use the function we must import it.
use std::asset::transfer;
To transfer some amount of an asset we specify the amount
that we would like to transfer, the asset
and the Address
to send it to.
let amount = 10;
let address = 0x0000000000000000000000000000000000000000000000000000000000000001;
let asset = BASE_ASSET_ID;
let user = Address::from(address);
transfer_to_address(user, asset, amount);
To Contract
To use the function we must import it.
use std::asset::force_transfer_to_contract;
To transfer some amount of an asset we specify the amount
that we would like to transfer, the asset
and the ContractId
to send it to.
let amount = 10;
let address = 0x0000000000000000000000000000000000000000000000000000000000000001;
let asset = BASE_ASSET_ID;
let pool = ContractId::from(address);
force_transfer_to_contract(pool, asset, amount);
To Address or Contract
To use the function we must import it.
use std::asset::transfer;
To transfer some amount of an asset we specify the amount
that we would like to transfer, the asset
and the Identity
to send it to.
let amount = 10;
let address = 0x0000000000000000000000000000000000000000000000000000000000000001;
let asset = BASE_ASSET_ID;
let user = Identity::Address(Address::from(address));
let pool = Identity::ContractId(ContractId::from(address));
transfer(user, asset, amount);
transfer(pool, asset, amount);
Contract Balance
Hashing
The hash module
contains the following functions:
They take one generic
argument T
and return a b256
(hash of T
).
To hash multiple values the values must be wrapped into one type such as a tuple
, array
, struct
& enum
.
sha256
To use the sha256
function we must import it.
To hash multiple values we wrap them into a tuple
however other compound types
may be used.
fn sha256_hashing(age: u64, name: str, status: bool) -> b256 {
let mut hasher = Hasher::new();
age.hash(hasher);
hasher.write_str(name);
status.hash(hasher);
hasher.sha256()
}
keccak256
To use the keccak256
function we must import it.
To hash multiple values we wrap them into a tuple
however other compound types
may be used.
fn keccak256_hashing(age: u64, name: str, status: bool) -> b256 {
let mut hasher = Hasher::new();
age.hash(hasher);
hasher.write_str(name);
status.hash(hasher);
hasher.keccak256()
}
Signature Recovery
Fuel Address
Ethereum Address
Counter
The following example implements a counter which is able to:
- Increment the count by 1
- Decrement the count by 1
- Retrieve the value of the counter
ABI
To create a counter we must define an ABI
which exposes methods that manipulate the count and retrieve its value. Since we are handling storage
we must provide storage annotations
on the functions.
abi Counter {
#[storage(read, write)]
fn increment();
#[storage(read, write)]
fn decrement();
#[storage(read)]
fn count() -> u64;
}
Implementation
We initialize a count in storage
with the value of zero and implement methods to increment & decrement the count by one and return the value.
storage {
counter: u64 = 0,
}
impl Counter for Contract {
#[storage(read, write)]
fn increment() {
storage.counter.write(storage.counter.read() + 1);
}
#[storage(read, write)]
fn decrement() {
storage.counter.write(storage.counter.read() - 1);
}
#[storage(read)]
fn count() -> u64 {
storage.counter.read()
}
}
Fizzbuzz
The following example implements the fizzbuzz game.
The rules are:
- A number divisible by
3
returnsFizz
- A number divisible by
5
returnsBuzz
- A number which is divisible by
3
&5
returnsFizzbuzz
- Any other number entered is returned back to the user
State
Let's define an enum
which contains the state of the game.
enum State {
Fizz: (),
Buzz: (),
FizzBuzz: (),
Other: u64,
}
Implementation
We can write a function
which takes an input
and checks its divisibility. Depending on the result a different State
will be returned.
fn fizzbuzz(input: u64) -> State {
if input % 15 == 0 {
State::FizzBuzz
} else if input % 3 == 0 {
State::Fizz
} else if input % 5 == 0 {
State::Buzz
} else {
State::Other(input)
}
}
Contract Ownership
The following example implements access control to restrict functionality to a privileged user.
ABI
The interface
contains a function to set the owner and a function that only the owner can use.
abi Ownership {
#[storage(read, write)]
fn set_owner(owner: Option<Identity>);
#[storage(read)]
fn action();
}
Identity
We must keep track of the owner in storage and compare them against the caller via msg_sender()
.
Initially there is no owner so we'll set them to None
.
storage {
owner: Option<Identity> = None,
}
Implementation
To set the owner one of two conditions must be met:
- There is no owner
- The current owner is calling the function
To call our action()
function the caller must be the owner of the contract.
impl Ownership for Contract {
#[storage(read, write)]
fn set_owner(owner: Option<Identity>) {
assert(storage.owner.read().is_none() || storage.owner.read().unwrap() == msg_sender().unwrap());
storage.owner.write(owner);
}
#[storage(read)]
fn action() {
assert(storage.owner.read().unwrap() == msg_sender().unwrap());
// code
}
}
Wallet
The following example implements a wallet that utilizes the base asset.
ABI
The interface
contains a function which tracks the amount of the base asset received and a function to transfer the funds.
abi Wallet {
#[storage(read, write)]
fn receive();
#[storage(read, write)]
fn send(amount: u64, recipient: Identity);
}
Implementation
When receiving funds we assert that the wallet accepts the base asset and we track the amount sent. When transferring funds out of the wallet we assert that only the owner can perform the transfer.
use std::{
call_frames::msg_asset_id,
constants::BASE_ASSET_ID,
context::msg_amount,
asset::transfer,
};
storage {
balance: u64 = 0,
}
const OWNER = Address::from(0x8900c5bec4ca97d4febf9ceb4754a60d782abbf3cd815836c1872116f203f861);
impl Wallet for Contract {
#[storage(read, write)]
fn receive() {
assert(msg_asset_id() == BASE_ASSET_ID);
storage.balance.write(storage.balance.read() + msg_amount());
}
#[storage(read, write)]
fn send(amount: u64, recipient: Identity) {
assert(msg_sender().unwrap() == Identity::Address(OWNER));
storage.balance.write(storage.balance.read() - amount);
transfer(recipient, BASE_ASSET_ID, amount);
}
}
Liquidity Pool
Standard Library Prelude
The prelude is a list of commonly used features from the standard library which is automatically imported into every Sway program.
The prelude contains the following:
Address
: A struct containing ab256
value which represents the wallet addressContractId
A struct containing ab256
value which represents the ID of a contractIdentity
: An enum containingAddress
&ContractID
structsVec
: A growable, heap-allocated vectorStorageMap
: A key-value mapping in contract storageOption
: An enum containing either some generic value<T>
or an absence of that value, we also expose the variants directly:Some
None
Result
: An enum used to represent either a success or failure of an operation, we also expose the variants directly:Ok
Err
assert
: A module containingassert
: A function that reverts the VM if the condition provided to it is falseassert_eq
: A function that reverts the VM and logs its two inputs v1 and v2 if the condition v1 == v2 is falseassert_ne
: A function that reverts the VM and logs its two inputs v1 and v2 if the condition v1 != v2 is false
revert
: A module containingrequire
: A function that reverts and logs a given value if the condition isfalse
revert
: A function that reverts
log
: A function that logs arbitrary stack typesmsg_sender
: A function that gets the Identity from which a call was made
Language Comparison
Rust
Solidity
Software Development Kits
Rust SDK
TypeScript SDK
Advanced Concepts
Struct Memory Layout
Structs have zero memory overhead, meaning that each field is laid out sequentially in memory. No metadata regarding the struct's name or other properties is preserved at runtime.
In other words, structs are compile-time constructs similar to Rust, but different in other languages with runtimes like Java.
Enum Memory Layout
Enums 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. To calculate the size of an enum in memory, add 8 bytes to the size of the largest variant.
Examples
The following examples consist of enums with two variants.
The largest variant for Example One
is the u64
and b256
for Example Two
.
Example One
The size of enum T
is 16 bytes
, 8 bytes
for the tag and 8 bytes
for the u64
.
pub enum T {
a: u64,
b: (),
}
Instantiating the u64
type will take up 16 bytes
.
let a = T::a(42);
Instantiating the unit
type will take up 16 bytes
.
let b = T::b;
Example Two
The size of enum K
is 40 bytes
, 8 bytes
for the tag and 32 bytes
for the b256
.
pub enum K {
a: b256,
b: u64,
}
Instantiating the b256
type will take up 40 bytes
.
let a = K::a(0x0000000000000000000000000000000000000000000000000000000000000000);
Instantiating the u64
type will take up 40 bytes
.
let b = K::b(42);
Compiler Intrinsics
Known Issues and Workarounds
General
TODO: need help filling this in, might remove this page and move content into individual sections
- Issue: #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.
- All
Missing Features
Importing
In external libraries we have looked at how a library can be imported into a project so that code can be reused.
When it comes to importing only external libraries can be imported through the Forc.toml
file; any other type of program will result in an error.
This means that the following projects cannot be imported:
While contracts cannot be imported, a workaround is to move the contract's abi
declaration into an external library and import that library anywhere the ABI is needed.
TODO: move the next comment into a page where it makes sense to keep it
Furthermore, using contract dependencies it is possible to import the contract ID automatically as a public constant.
Strings
Sway strings are declared using double-quotes "
. Single quotes '
cannot be used. Attempting to define a string with single-quotes will result in an error.
// Will error if uncommented
// let fuel = 'fuel';
Strings are UTF-8 encoded therefore they cannot be indexed.
let fuel = "fuel";
// Will error if uncommented
// let f = fuel[0];
Predicates
A predicate does not have any side effects because it is pure and thus it cannot create receipts.
Since there are no receipts they cannot use logging nor create a stack backtrace for debugging. This means that there is no 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
.
Pattern Matching
Nested Match Expressions
In nested match expressions we nest a match
expression by embedding it inside the {}
brackets on the right side of the arrow =>
.
Match expressions cannot be used as a pattern, the left side of the arrow =>
.
Constants
When matching on constants we specify that a constant must be used in order to match on a variable. Dynamic values, such as an argument to a function, cannot be matched upon because it will be treated as a catch_all
case and thus any subsequent patterns will not be checked.