1. Hello World
Once you've set up your development environment, you're ready to create your first smart contract.
Create a New Project
Create a new project using the init
command to create a soroban-hello-world
project.
stellar contract init soroban-hello-world
The init
command will create a Rust workspace project, using the recommended structure for including Soroban contracts. Let’s take a look at the project structure:
.
├── Cargo.lock
├── Cargo.toml
├── README.md
└── contracts
└── hello_world
├── Cargo.toml
└── src
├── lib.rs
└── test.rs
Cargo.toml
The Cargo.toml
file at the root of the project is set up as Rust Workspace, which allows us to include multiple smart contracts in one project.
Rust Workspace
The Cargo.toml
file sets the workspace’s members as all contents of the contracts
directory and sets the workspace’s soroban-sdk
dependency version including the testutils
feature, which will allow test utilities to be generated for calling the contract in tests.
[workspace]
resolver = "2"
members = [
"contracts/*",
]
[workspace.dependencies]
soroban-sdk = "20.3.2"
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.
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.
The Cargo.toml
file has the following release profile configured.
[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true
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 stellar-cli
. Note that this is not necessary to access debug logs in tests or to use a step-through-debugger.
[profile.release-with-logs]
inherits = "release"
debug-assertions = true
See the logging example for more information about how to log.
Contracts Directory
The contracts
directory is where Soroban contracts will live, each in their own directory. There is already a hello_world
contract in there to get you started.
Contract-specific Cargo.toml file
Each contract should have its own Cargo.toml
file, which relies on the top-level Cargo.toml
that we just discussed.
This is where we can specify contract-specific package information.
[package]
name = "hello-world"
version = "0.0.0"
edition = "2021"
publish = false
The crate-type
is configured to cdylib
which is required for building contracts.
[lib]
crate-type = ["cdylib"]
doctest = false
We also have included the soroban-sdk dependency, configured to use the version from the workspace Cargo.toml.
[dependencies]
soroban-sdk = { workspace = true }
[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
Contract Source Code
Creating a Soroban contract involves writing Rust code in the project’s lib.rs
file.
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 imports the types and macros that it needs from the soroban-sdk
crate.
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]
pub struct HelloContract;
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.
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol_short!("Hello"), to]
}
}
Putting those pieces together a simple contract looks like this.
#![no_std]
use soroban_sdk::{contract, contractimpl, symbol_short, vec, Env, Symbol, Vec};
#[contract]
pub struct HelloContract;
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol_short!("Hello"), to]
}
}
mod test;
Note the mod test
line at the bottom, this will tell Rust to compile and run the test code, which we’ll take a look at next.
Contract Unit Tests
Writing tests for Soroban contracts involves writing Rust code using the test facilities and toolchain that you'd use for testing any Rust code.
Given our HelloContract, a simple test will look like this.
- contracts/hello_world/src/lib.rs
- contracts/hello_world/src/test.rs
#![no_std]
use soroban_sdk::{contract, contractimpl, symbol_short, vec, Env, Symbol, Vec};
#[contract]
pub struct HelloContract;
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol_short!("Hello"), to]
}
}
mod test;
#![cfg(test)]
use super::*;
use soroban_sdk::{symbol_short, vec, Env};
#[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_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 HelloContract
, and the client is named HelloContractClient
.
let client = HelloContractClient::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 unit test 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.
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 the contract
To build a smart contract to deploy or run, use the stellar contract build
command.
stellar contract build
If you get an error like can't find crate for 'core'
, it means you didn't install the wasm32 target during the setup step. You can do it so by running rustup target add wasm32-unknown-unknown
.
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, at target/wasm32-unknown-unknown/release/hello_world.wasm
. The .wasm
file is the built contract.
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 stellar contract optimize
to further minimize the size of the .wasm
. First, re-install stellar-cli with the opt
feature:
cargo install --locked stellar-cli --features opt
Then build an optimized .wasm
file:
stellar contract optimize --wasm target/wasm32-unknown-unknown/release/hello_world.wasm
This will optimize and output a new hello_world.optimized.wasm
file in the same location as the input .wasm
.
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.
Summary
In this section, we wrote a simple contract that can be deployed to a Soroban network.
Next we'll learn to deploy the HelloWorld contract to Stellar's Testnet network and interact with it over RPC using the CLI.