Move 101
New to Move? Start here with the basics!
Learn the fundamentals of the Move programming language with simple, step-by-step examples. This section covers key concepts like modules, resources, and transactions, helping you build a solid foundation for smart contract development.
For the latest RPC URL, please refer to the Supra Network Information page.
Getting Started with Move
Move code is organized into modules that are uploaded to the Supra blockchain. Users can interact with the modules by calling functions from these modules via transactions. Modules are grouped into packages. When uploading code to the blockchain, an entire package of multiple modules will be uploaded.
Each Move module has a unique identifier comprising the address it's deployed at and the module name. The fully qualified name is of the format <address>::<module_name>
We'll create a DinosaurNest contract with the following features:
Keep track of all the Dinosaurs it has spawned. Birth new Dinosaurs via a function We'll generate a special Gendna code every time a Dinosaur is spawned to create unique genetics for each Dinosaur. The special abilities genetic code has digits and looks like below: 1234567890
This special Gendna code represents each Dinosaur's unique attributes, with every 2 digits representing one, such as shirts, hats, and glasses. Later, you can have more fun and add new attributes by adding more digits to the special abilities code.
Let's start by adding a number to track the number of digits allowed in a Gendna code. This number will be stored on the blockchain (global storage). This data can be accessed and changed by the module's code as well as read by web UI.
On Supra, all data written to the blockchain needs to be declared in a struct that has the key attribute. For now, take a look at the simple example below:
In the example above, we're storing a simple value (Gendna_digits
) in global storage. This value can be read in functions later. One common confusion is between global storage and local variables. Unless explicitly declared in struct
the key
attribute, all variables declared and modified in a function are local variables and thus not stored on the blockchain (in global storage). Also, note that the struct name is capitalized by convention (DinosaurGendna
instead of Dinosaur_Gendna
).
Unsigned Integers: u8, u32, u64, u128, u256
Move supports multiple different types of unsigned integers. The values of these integers are always zero or larger. The different types of integers have different maximum values they can store. For example, u8
can store values up to 255, while u256
can store values up to 2^256 - 1. The most commonly used integer type is u64
. Smaller types such as u8
and u32
are usually only used to save on gas (less storage space used). Larger sizes such as u128
and u256
are only used for when you need to store very large numbers.
Now that we have defined the DinosaurGendna
struct, we need to set its initial value. We can’t set the initial value directly when defining a struct
, so we'll need to create a function to do that. Recall that structs can be stored directly in global storage on Supra. Structs stored in global storage are called Resources. Each Resource needs to be stored at a specific address. This can be the same address where the module is deployed or any other user address. This is different from other blockchains - code and data can coexist at the same address on Supra. For our Dinosaur_nest module
, we'll keep it simple and store the DinosaurGendna
resource at the same address as the module. Let’s initialize it by writing the init_module
function, where we get access to the signer of the address we're deploying to. init_module
is called when the module is deployed to the blockchain and is the perfect place for initializing data. Don't worry about what a signer is for now - we'll cover that in later lessons. You just need to know that the signer is required to create and store a new resource at an address the first time.
Moving on with our code:
In the example above, we're creating a new DinosaurGendna
resource with a default value (Gendna_digits
) of 10 (our Gendna Value as shared earlier) and storing it at 0xcafe (same address as the module) with move_to
.
move_to is
a special function that requires a signer that can be called to store a resource at a specific address.
Math in Move
Doing math in Move is easy and very similar to other programming languages:
Add:
x + y
Subtract:
x - y
Multiply:
x * y
Divide:
x / y
Mod:
x % y
Exponential:
x ^ y
Note: This is integer math, which means results are rounded down.
For example, 5 / 2 = 2
To make sure our Dinosaur's Gendna is only 10 digits, let's make another u64
value equal to 10^10. That way we can later use the modulus operator %
to create valid Gendna codes from any randomly generated numbers.
Create another integer value named Gendna_modulus
in the DinosaurGendna struct, and set it equal to 10 to the power of Gendna_digits
So far we've seen different types of integers: u8
, u32
, u64
, u128
, u256
. Although math can be done easily among integers of the same type, it's not possible to do math directly between integers of different types,
Example Below:
Let’s cast Gendna_modulus to u256 with (Gendna_modulus as u256). Remember that the parentheses () are required when typecasting:
The actual contract would look like this:
Vectors
When you want a list of values, use vectors. A vector
in Move is dynamic by default and has no fixed size. It can get larger or smaller as needed. Vector in Supra and Aptos are available to import and use at std::vector
.
You just need to do “use std::vector
” at the top of your module to be able to access it.
You can also store structs in vectors, Note that for structs that are stored in a resource struct, you need to add the store attribute to the struct:
When creating an empty vector you can use the following syntax: let empty_vector = vector[];
We need to track all the Dinosaurs created from Dinosaur_nest, We can do this by declaring two new structs:
Dinosaur
Struct having Key and a new resource struct named DinosaurSwarm
which has a vector of Dinosaur structs, this resource needs to be stored at 0xcafe
in init_module
with an empty vector of Dinosaurs to start with.
Code Follows Like:
We've only been using the init_module
function, which is called when the module is deployed to initialize default values. we'll create one more function that will later be called by the user to create a new Dinosaur.
Let’s create a new function named spawn_Dinosaur
that takes one argument of type u64
named Gendna
and returns a Dinosaur struct with that Gendna:
Note: This function is public, which means it can be called from any other Move module. we'll keep all functions public, except for init_module. init_module has to be private because it's called only once when the module is deployed.
borrow_global
: read an existing resource struct
borrow_global
: read an existing resource structWe just need to call borrow_global
with the right resource type and address. The address can be passed into the function as an argument or referred to specifically with @ such as @0xcafe
. A signer is not required here. If the resource is not found at the address, the function will error when the code is run on the blockchain. The function that calls borrow_global
also needs to declare that it does so by adding “acquires ResourceName
” at the end.
Let’s write a new function named get_Gendna_digits that returns the Gendna_digits field of the DinosaurGendna resource stored at 0xcafe:
pass-by-value & pass-by-reference
In Move, when you pass a simple value as u64 to a function, you might be making a copy of it. This is called pass-by-value, for example:
So how do we modify the original value?
We need to pass a mut reference (&mut
) to the value instead of the value itself. This is called pass-by-reference. This is similar to how you pass a pointer to a value in C/C++ or Rust.
There are two types of references in Move: references
(&) and mutable references
(&mut). The immutable reference (&) is often used to pass a value such as a vector to a function that only intends to read data instead of writing it. A function that takes a reference needs to explicitly declare:
You can also pass a reference to a struct and modify it:
Now, Write a function in Dinosaur_nest
that returns the Gendna
of the first Dinosaur in the DinosaurSwarm
. Don’t forget the “acquires
” declaration!
vector::push_back
: add all the things we've learned
vector::push_back
: add all the things we've learnedWe can add all the things we've learned so far to create a more complex module. One more thing you can do now that you know references: You can add an element to a vector using vector::push_back
and pass a mutable reference to the vector.
Let’s Modify spawn_Dinosaur
to add the new Dinosaur to the DinosaurSwarm's vector of Dinosaurs instead of returning it.
Event::Emit
Events are a way for your contract to communicate that something happened on the blockchain to your app front-end, which can be 'listening' for certain events and taking action when they happen.
In order to emit an event, you need to do three things:
Define the event struct.
Use
event::emit
to emit it
Note: Event structs need to be declared with the #[event]
annotation.
Finally, Let's add an event named SpawnDinosaurEvent
that contains the new Gendna code & Emit SpawnDinosaurEvent
when a Dinosaur is created.
Wrapping Up
Till now you must have gotten a good idea of how things get Built and How Logic works in MOVE. You can find the Move.toml and Source File in the repo, fort it, and run on your side to get hands-on and learn with building your version of the Move Module as well.
Contribution
Feel free to contribute to this project by submitting pull requests or opening issues, all contributions that enhance the functionality or user experience of this project are welcome.
Last updated