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 [project-name]
Open the Cargo.toml
, it should look something like this:
[package]
name = "project-name"
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
that will import the soroban-sdk
, and configure a set of features explained below.
The soroban-sdk
is in early development. Report issues here.
[dependencies]
soroban-sdk = "0.6.0"
[dev_dependencies]
soroban-sdk = { version = "0.6.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.
The testutils
test utilities 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 256KB. 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 for if you need to build a .wasm
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 below should produce a Cargo.toml
that looks like so.
[package]
name = "project-name"
version = "0.1.0"
edition = "2022"
[lib]
crate-type = ["cdylib"]
[features]
testutils = ["soroban-sdk/testutils"]
[dependencies]
soroban-sdk = "0.6.0"
[dev_dependencies]
soroban-sdk = { version = "0.6.0", features = ["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 projects lib.rs
file.
#![no_std]
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.
use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec};
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.
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.
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
todo!()
}
}
Contract functions live inside an impl
for a struct. The impl
block is annotated with #[contractimpl]
. Contract functions must be given names that are no more than 10 characters long. Functions that are intended to be called externally should be marked with pub
visibility. The first argument can be an Env
argument to get a copy of the Soroban environment, which is necessary for most things.
Putting those pieces together a simple contract will look like this.
#![no_std]
use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec};
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol!("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.
Given a simple contract like the contract demonstrated in the Write a Contract section, a simple test will look like this.
- src/lib.rs
- src/test.rs
#![no_std]
use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec};
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol!("Hello"), to]
}
}
#[cfg(test)]
mod test;
#![cfg(test)]
use super::{Contract, ContractClient};
use soroban_sdk::{symbol, vec, Env};
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(&env, &contract_id);
let words = client.hello(&symbol!("Dev"));
assert_eq!(
words,
vec![&env, symbol!("Hello"), symbol!("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!("Dev"));
The values returned by functions can be asserted on:
assert_eq!(
words,
vec![&env, symbol!("Hello"), symbol!("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.
Build
To build a Soroban contract to deploy or run, use the cargo build
command.
cargo build --target wasm32-unknown-unknown --release
Use the release
profile outlined in Configure the Release Profile, otherwise the contract will exceed Soroban's size limits.
A .wasm
file will be outputted in the target
directory. The .wasm
file is the built contract.
target/wasm32-unknown-unknown/release/[project-name].wasm
The .wasm
file contains the logic of the contract, as well as the contract's specification that 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.
To further optimize builds to be as small as possible, see Optimizing Builds.
Run on Sandbox
If you have the soroban-cli
installed, you can run contracts in a local sandbox environment.
The Soroban sandbox environment is the same Soroban environment that runs on Stellar networks, but it runs without nodes, and without the other features you find on a Stellar network.
It's also possible to run a contract on a fully featured local network. See Deploy to a Local Network for more details.
Using the code we wrote in Write a Contract and the resulting .wasm
file we built in Build, run the following command to invoke the hello
function. Here we're setting the to
argument to friend
:
soroban contract invoke \
--wasm target/wasm32-unknown-unknown/release/[project-name].wasm \
--id 1 \
--fn hello \
-- \
--to friend
The following output should appear.
["Hello", "friend"]
--
double-dash is required!This is a general CLI pattern used by other commands like cargo run. Everything after the --
, sometimes called slop, is passed to a child process. In this case, soroban contract invoke
builds an implicit CLI on-the-fly for the hello
method in your contract. It can do this because Soroban SDK embeds your contract's schema / interface types right in the .wasm
file that gets deployed on-chain. Try this, too:
soroban contract invoke ... --fn hello -- --help
Hello World Example
The hello world example demonstrates how to write a simple contract, with a single function that takes one input and returns it as an output.
Optimizing Builds
Use soroban contract optimize
to further minimize the size of the .wasm
.
soroban contract optimize \
--wasm target/wasm32-unknown-unknown/release/first_project.wasm
This will optimize and output a new first_project.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.