On the last post, we booted up our GitPod environment and now are ready to tackle the first Soroban Quest: Hello World!
Understanding README.md
Let's begin questing by opening the quests
folder in our Explorer. We can see multiple quests existing here. Since we will tackle the first quest right now, let's open the 1-hello-world
folder by clicking on it. Then, select README.md
for more information about what to do on this quest.
We can see that we have to use our Stellar Quest account to deploy and invoke the 1 Hello World contract on the Stellar Futurenet. What does any of these words mean? Let's check them out.
Setting Up Our Stellar Quest Account
If we missed it, or are using another workspace, we should use sq login
to login to our Stellar Quest account. This way, our progress can be saved to our account, and we can get sweet NFTs to our connected wallet. If the 1-hello-world
folder is missing, we can use sq pull
to get the newest copy of the repository from GitHub.
Pull? From where?
sq pull
usesgit
underneath to "pull" the newest changes from the soroban quest repository to our local workspace. git is a version control program with excellent remote and local repository synchronization support, so that's what is happening here.sq pull
accomplishes it in three steps:
- Uses
git stash
to save our changes into a stash, so that collisions between the remote repository and our repository do not occur.- Uses
git pull
to pull the changes from the soroban quest repository.- Uses
git stash pop
to reapply the saved changes to our workspace.It is pretty useful for saving our work on the workspace, while having the ability to add new stuff!
After setting up our account and everything, we can use sq play 1
to start questing! We should accept the funding by the prompt, as we will need some Futurenet Lumens for transacting. If you accidentally pressed no, you can use sq fund [account]
, with account being the public key given to you for the quest. Don't forget to remove the brackets!
Examining the Contract
On the src
folder, we can see two files: lib.rs
and test.rs
. lib.rs
contains the source code of the Soroban contract given on this quest. Let's begin checking out the code.
// We don't include the standard library to minimize compiled
// size. We also import a few macros and types we need from the
// `soroban_sdk`.
#![no_std]
use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec};
pub struct HelloContract;
// Our `HelloContract` implementation contains only one function,
// `hello()`. This function will receive a `to` argument, and
// return a Vec made up of "Hello" and the supplied `to` value.
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol!("Hello"), to]
}
}
// This `mod` declaration inserts the contents of `test.rs` into
// this file.
mod test;
The Soroban contracts are written in Rust, which is the hottest programming language at the moment. If you are not familiar with Rust, no worries! The contracts are extremely well commented, and it is quite understandable if you are experienced with another programming language. Let's dive deeper into the contract's code.
What is Rust and Why is it Everywhere?
Rust is a systems programming language, which is known by it's memory safety. In addition to being memory safe, it is also blazingly fast, which makes it a popular programming language amongst programmers. The memory safety aspect is important, since 70% of the CVEs are caused by memory safety issues. So, to write a fast and secure code, a different approach than C and C++, must be employed. That's why Rust is everywhere!
#![no_std]
use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec};
pub struct HelloContract;
The #![no_std]
line tells the Rust compiler to not include the standard library, as it is quite large and is not fully supported by the Soroban environment. The useful components are already implemented in the soroban_sdk
, so we don't need to include the standard library! The second line tells the compiler to use the implementations of contractimpl
, symbol
, vec
, Env
, Symbol
, and Vec
from the soroban_sdk
library. We will use these lines almost every time when we are developing a Soroban contract. The next line tells the Soroban Runtime that our contract's name is HelloContract
. After these definitions, we can check the implementation of the contract.
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol!("Hello"), to]
}
}
The implementations of Soroban contracts are marked with #[contractimpl]
to tell the compiler to do the right thing. In our contract implementation of the HelloContract
, we can see that we have a public function, denoted with pub fn
. Public functions are functions on a contract which are invokable by everyone using the Soroban network. The hello
function has two arguments, and returns a Vector
of Symbol
s. A Symbol
is a string-like value, which consists of at most 10 characters. A Vector
can be defined as an array with the capacity to grow indefinitely.
The first input argument is env
of type Env
. This parameter can be found on every functiion that deals with the state of the Soroban network. It is generally used in each function, as it supplies some additional functionality. The env
argument is not supplied by the invoker of the function, and is added automatically on function invocation.
The second input argument is to
of type Symbol
. Symbols can be assumed as strings of a maximum length of 10. They provide cost and space efficiency, and are widely used for small data.
The function returns a Vector
, using the macro vec!
. It returns a vector with three elements, which are &env
, the symbol
"Hello", and the symbol
to
, which is supplied by the invoker of the function. The &env
parameter is the reference of the Soroban environment, and is not explicitly returned to the user.
mod test;
The last line inserts the code content of the test.rs
file to the lib.rs
file.
Examining the Test
Now, let's examine the test.rs
file. Examining the tests are important to see how the contract operates. A well tested contract is more secure than a non-tested contract, and security is one of the most important concerns when dealing with contracts on a blockchain.
#![cfg(test)]
use super::*;
use soroban_sdk::{symbol, vec, Env};
// The purpose of this file is to run automated tests on the
// contract code we've written in `lib.rs`. Writing tests can be
// quite a big topic, and we'll dive in further in a future quest.
// Just you wait!
#[test]
fn test() {
// we register the contract in a Soroban environment, and
// build a client we can use to invoke the contract
let env = Env::default();
let contract_id = env.register_contract(None, HelloContract);
let client = HelloContractClient::new(&env, &contract_id);
// Next, we call `client.hello()`, supplying "Dev" as our `to`
// argument.
let words = client.hello(&symbol!("Dev"));
// We assert the contract must return a Vec that matches what
// we would expect to receive from our contract:
// [Symbol("Hello"), Symbol("Dev")]
assert_eq!(words, vec![&env, symbol!("Hello"), symbol!("Dev"),]);
}
The test is also written in Rust, and is used to test the functionality of the HelloContract
on a local environment.
#![cfg(test)]
use super::*;
use soroban_sdk::{symbol, vec, Env};
The first line configures the file as a test environment. The second line, use super::*;
, imports everything from the parent module, which is the HelloContract
file here. As before, the third line imports some implementations from the soroban_sdk
.
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register_contract(None, HelloContract);
let client = HelloContractClient::new(&env, &contract_id);
let words = client.hello(&symbol!("Dev"));
assert_eq!(words, vec![&env, symbol!("Hello"), symbol!("Dev"),]);
}
The test implementations are marked with #[test]
, and allows the Rust compiler to understand that these are testing functions. We start with creating a constant, env
, and initializing with the default value. Next, we are registering our HelloContract
to the environment. Now, we have a local Soroban sandbox with only HelloContract
existing. We then create a client to interact with the contract of ours for testing.
We invoke the function hello
on our HelloContract
by using the hello
method of our client using the "Dev" argument. See that we are not supplying the env
parameter, as it is implicitly included already. We then set the reply of the function to the words
constant.
After doing our invocation, we assert the contract must return a Vec
, which consists of the elements &env
, "Hello", and our input argument, "Dev". If that is the case, the test ends without an error, which means that our implementation is correct.
Testing Tips
When testing software, we use the AAA (Arrange, Act, and Assert) principle. First, we "Arrange" the environment, which we did here using the first three lines. Then, we "Act" on our source code, which corresponds to invoking our
hello
function on our contract. Finally, we "Assert" that everything is correct by comparing the actual result to the expected result. This way, testing software can be done systematically.
Compiling, Testing, Deploying
We can compile our code using cargo build
to compile our rust codes into .wasm
files. If we want to build only a specific quest's contract, the usage is
cargo build --package [package-name] --target wasm32-unknown-unknown --release
where the package name of a quest can be found in the Cargo.toml
file on the quest's folder. For example, the Hello World quest's package name is soroban-hello-world-contract
, so we can build it using
cargo build --package soroban-hello-world-contract --target wasm32-unknown-unknown --release
The created object can be found in target/wasm32-unknown-unknown/release/soroban_hello_world_contract.wasm
file. We can also use cargo test
, to see if our implementations contains any error.
Everything looks great! We now can deploy our Soroban contract to the Futurenet using soroban deploy
command. Please be careful that you are using the CLI - Futurenet: bash
terminal to successfully accomplish this task. The full command is
soroban deploy --wasm target/wasm32-unknown-unknown/release/soroban_hello_world_contract.wasm
We should see a success message with the contract's ID. If you are getting errors when deploying, wait for the Soroban client to synchronize to the Futurenet, which can take some time. If you are still getting errors, try using the handy command sq rpc -c
to change the default RPC endpoint to an official one.
Great! We will need to supply this ID to interact with our deployed contract. We can use a different contract ID to interact with another contract. The possibilities are endless!
We can now invoke the hello
function using soroban invoke
to get a reply from our contract. We should again use the Futurenet CLI for this purpose.
soroban invoke --id [your contract id] --fn hello --arg [any string argument shorter than 10 characters]
The invocation should return a success and the Vector
that contains the "Hello" word followed by the argument we supplied.
Everything works perfectly, and we have completed the first quest! Well done! Use sq check 1
to claim your reward 🎉.
How Long Will My Contract Live?
Forever! That's the beauty of the blockchains. The immutability and the censorship resistance are what makes blockchains great. This power is a double edged sword though, if we make any mistakes, or have any vulnerabilities in our contract, there's nothing we can do! That's why testing and auditing smart contracts are an important business.
Note that this is the Futurenet, which is a test server for Soroban contracts. It often gets reset, so your Soroban contract will cease to exist. This will not happen when Soroban hits the mainnet!
Top comments (3)
Hope you enjoyed this post!
If you want to support my work, you can send me some XLM at
GB2M73QXDA7NTXEAQ4T2EGBE7IYMZVUX4GZEOFQKRWJEMC2AVHRYL55V
Thanks for all your support!
This is a really great write up. Well done! 👏
(I did notice a little miss on a link at the top though)
[git](https://git-scm.com/)
Thanks!
Also thanks for the heads up on the error, I fixed it!