Skip to main content

1. Hello World

Once you've setup your development environment, you're ready to create your first Soroban contract.

Create New Project

Create a new Rust library using the cargo new command.

cargo new --lib hello-soroban

Open the Cargo.toml, it should look something like this:

Cargo.toml
[package]
name = "hello-soroban"
version = "0.1.0"
edition = "2021"

Configure the Library Type

Add the crate-type configuration, required for building contracts.

[lib]
crate-type = ["cdylib"]

Import soroban-sdk and Features

Add the following sections to the Cargo.toml to import the soroban-sdk, and configure a set of features explained below.

[dependencies]
soroban-sdk = "20.0.0"

[dev_dependencies]
soroban-sdk = { version = "20.0.0", features = ["testutils"] }

[features]
testutils = ["soroban-sdk/testutils"]

The features list includes a testutils feature, which will cause additional test utilities to be generated for calling the contract in tests.

info

The testutils are automatically enabled inside Rust unit tests inside the same crate as your contract. If you write tests from another crate, you'll need to require the testutils feature for those tests and enable the testutils feature when running your tests with cargo test --features testutils to be able to use those test utilities.

Configure the release Profile

Configuring the release profile to optimize the contract build is critical. Soroban contracts have a maximum size of 64KB. Rust programs, even small ones, without these configurations almost always exceed this size.

Add the following to your Cargo.toml and use the release profile when building.

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

Configure the release-with-logs Profile

Configuring a release-with-logs profile can be useful if you need to build a .wasm file that has logs enabled for printing debug logs when using the soroban-cli. Note that this is not necessary to access debug logs in tests or to use a step-through-debugger.

Add the following to your Cargo.toml and use the release-with-logs profile when you need logs.

[profile.release-with-logs]
inherits = "release"
debug-assertions = true

See the logging example for more information about how to log.

Wrapping it Up

The steps above should produce a Cargo.toml that looks like so.

Cargo.toml
[package]
name = "project-name"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = "20.0.0"

[dev_dependencies]
soroban-sdk = { version = "20.0.0", features = ["testutils"] }

[features]
testutils = ["soroban-sdk/testutils"]

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

[profile.release-with-logs]
inherits = "release"
debug-assertions = true

Write a Contract

Once you've created a project, writing a contract involves writing Rust code in the project's lib.rs file. You can delete the default functions that cargo added to lib.rs.

All contracts should begin with #![no_std] to ensure that the Rust standard library is not included in the build. The Rust standard library is large and not well suited to being deployed into small programs like those deployed to blockchains.

#![no_std]

The contract will need to import the types and macros that it needs from the soroban-sdk crate. Take a look at Create a Project to see how to setup a project.

use soroban_sdk::{contract, contractimpl, symbol_short, vec, Env, Symbol, Vec};

Many of the types available in typical Rust programs, such as std::vec::Vec, are not available, as there is no allocator and no heap memory in Soroban contracts. The soroban-sdk provides a variety of types like Vec, Map, Bytes, BytesN, Symbol, that all utilize the Soroban environment's memory and native capabilities. Primitive values like u128, i128, u64, i64, u32, i32, and bool can also be used. Floats and floating point math are not supported.

Contract inputs must not be references.

The #[contract] attribute designates the Contract struct as the type to which contract functions are associated. This implies that the struct will have contract functions implemented for it. Contract functions are defined within an impl block for the struct, which is annotated with #[contractimpl]. It is important to note that contract functions should have names with a maximum length of 32 characters. Additionally, if a function is intended to be invoked from outside the contract, it should be marked with the pub visibility modifier. It is common for the first argument of a contract function to be of type Env, allowing access to a copy of the Soroban environment, which is typically necessary for various operations within the contract.

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol_short!("Hello"), to]
}
}

Putting those pieces together a simple contract will look like this.

src/lib.rs
#![no_std]
use soroban_sdk::{contract, contractimpl, symbol_short, vec, Env, Symbol, Vec};

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
/// Say Hello to someone or something.
/// Returns a length-2 vector/array containing 'Hello' and then the value passed as `to`.
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol_short!("Hello"), to]
}
}

Testing

Writing tests for Soroban contracts involves writing Rust code using the test facilities and toolchain that you'd use for testing any Rust code.

You'll need to add an annotation to bottom of lib.rs to tell Rust to compile and run the test code.

#[cfg(test)]
mod test;

Given a simple contract like the contract demonstrated in the Write a Contract section, a simple test will look like this.

use crate::{Contract, ContractClient};
use soroban_sdk::{symbol_short, vec, Env};

#[test]
fn hello() {
let env = Env::default();
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(&env, &contract_id);

let words = client.hello(&symbol_short!("Dev"));
assert_eq!(
words,
vec![&env, symbol_short!("Hello"), symbol_short!("Dev"),]
);
}

In any test the first thing that is always required is an Env, which is the Soroban environment that the contract will run inside of.

let env = Env::default();

The contract is registered with the environment using the contract type. Contracts can specify a fixed contract ID as the first argument, or provide None and one will be generated.

let contract_id = env.register_contract(None, Contract);

All public functions within an impl block that is annotated with the #[contractimpl] attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with Client appended. For example, in our contract the contract type is Contract, and the client is named ContractClient.

let client = ContractClient::new(&env, &contract_id);
let words = client.hello(&symbol_short!("Dev"));

The values returned by functions can be asserted on:

assert_eq!(
words,
vec![&env, symbol_short!("Hello"), symbol_short!("Dev"),]
);

Run the Tests

Run cargo test and watch the contract run. You should see the following output:

cargo test
running 1 test
test test::test ... ok

Try changing the values in the test to see how it works.

note

The first time you run the tests you may see output in the terminal of cargo compiling all the dependencies before running the tests.

Build

To build a Soroban contract to deploy or run, use the soroban contract build command.

soroban contract build

This is a small wrapper around cargo build that sets the target to wasm32-unknown-unknown and the profile to release. You can think of it as a shortcut for the following command:

cargo build --target wasm32-unknown-unknown --release

A .wasm file will be outputted in the target directory. The .wasm file is the built contract.

target/wasm32-unknown-unknown/release/hello_soroban.wasm

The .wasm file contains the logic of the contract, as well as the contract's specification / interface types, which can be imported into other contracts who wish to call it. This is the only artifact needed to deploy the contract, share the interface with others, or integration test against the contract.

Optimizing Builds

Use soroban contract optimize to further minimize the size of the .wasm. First, re-install soroban-cli with the opt feature:

cargo install --locked --version 20.1.1 soroban-cli --features opt

Then build an optimized .wasm file:

soroban contract optimize \
--wasm target/wasm32-unknown-unknown/release/hello_soroban.wasm

This will optimize and output a new hello_soroban.optimized.wasm file in the same location as the input .wasm.

tip

Building optimized contracts is only necessary when deploying to a network with fees or when analyzing and profiling a contract to get it as small as possible. If you're just starting out writing a contract, these steps are not necessary. See Build for details on how to build for development.

Commit to version control

Before we go on to deploying the contract to Testnet in the next section, this is a great time to commit your code to version control. Even if you don't share your project with others, this will make it easier for you to see and understand your own changes throughout the rest of the tutorial.

Cargo will have already setup the hello-soroban directory as a git repository.

You should update the .gitignore file with .soroban directory as it will contain cached information about your local development and use of the soroban-cli:

echo .soroban >> .gitignore

Now you can make your initial commit:

git add .
git commit -m "Initial commit: hello-soroban contract"

Summary

In this section, we wrote a simple contract that can be deployed to a Soroban network.

Next we'll learn to deploy the hello-soroban contract to Soroban's Testnet network and interact with it over RPC using the CLI.