This repository has been archived by the owner on Aug 21, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
385ece0
commit a2d51cd
Showing
1 changed file
with
74 additions
and
148 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,141 +23,58 @@ description: Write a smart contract that stores and retrieves data. | |
import Tabs from "@theme/Tabs"; | ||
import TabItem from "@theme/TabItem"; | ||
|
||
Now that we've built a basic Hello World example to see the rough structure of Soroban contracts, we'll write a simple contract that stores and retrieves data. This will help you see the basics of Soroban's storage system. We'll also organize the two contracts as one combined project using a Cargo Workspace, which is a common pattern for Soroban projects. | ||
Now that we've built a basic Hello World example contract, we'll write a simple contract that stores and retrieves data. This will help you see the basics of Soroban's storage system. | ||
|
||
This is going to follow along with the [increment example](https://github.com/stellar/soroban-examples/tree/v20.0.0/increment), which has a single function that increments an internal counter and returns the value. If you want to see a working example, [try it in | ||
GitPod](https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0). | ||
This is going to follow along with the [increment example](https://github.com/stellar/soroban-examples/tree/v20.0.0/increment), which has a single function that increments an internal counter and returns the value. If you want to see a working example, [try it in GitPod](https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0). | ||
|
||
This tutorial assumes that you've already completed the previous steps in Getting Started: [Setup](./setup.mdx), [Hello World](./hello-world.mdx), and [Deploy to Testnet](./deploy-to-testnet.mdx). | ||
|
||
## Setting up a multi-contract project | ||
## Adding the increment contract | ||
|
||
Many Soroban projects need more than one contract. Cargo makes this easy with workspaces, though it doesn't yet give a way to initialize a new project as a workspace (see [#8365](https://github.com/rust-lang/cargo/issues/8365)). Let's set it up manually. | ||
|
||
Rather than just a `hello-soroban` folder, we want a new `soroban-tutorial` folder with a `contracts` folder inside, into which we'll move the existing `hello-soroban` project. As a diff, we want this: | ||
|
||
```diff | ||
-hello-soroban | ||
+soroban-tutorial/contracts/hello-soroban | ||
``` | ||
|
||
So change into the parent directory of `hello-soroban` and: | ||
First, we'll need to create a new `contracts/increment` directory for our contract. | ||
|
||
```bash | ||
mkdir -p soroban-tutorial/contracts | ||
mv hello-soroban soroban-tutorial/contracts | ||
cd soroban-tutorial | ||
mkdir contracts/increment | ||
``` | ||
|
||
You're going to want some Rust and Cargo stuff in different spots. From the new project root, run: | ||
Then, we need to create a `Cargo.toml` file for the `increment` contract. We can copy the existing `contracts/hello_world/Cargo.toml` file, making sure to change the package name to `increment`. | ||
|
||
```bash | ||
rm contracts/hello-soroban/Cargo.lock | ||
mv contracts/hello-soroban/target . | ||
mv contracts/hello-soroban/.soroban . | ||
cp contracts/hello-soroban/Cargo.toml . | ||
cp contracts/hello_world/Cargo.toml contracts/increment/Cargo.toml | ||
``` | ||
|
||
Note that we copied the Cargo.toml file. That's because we're going to need some of it in the root and some of it in the subdirectory. | ||
|
||
In the root `Cargo.toml`: | ||
|
||
- remove the `[package]`, `[lib]`, `[features]`, and `[dev_dependencies]` sections | ||
- keep the `[profile.release*]` stuff | ||
- replace the line `[dependencies]` with `[workspace.dependencies]` | ||
- add a `[workspace]` section (see below for specific values) | ||
|
||
In the contract-specific `Cargo.toml`: | ||
|
||
- remove the `[profile.release*]` stuff | ||
- set the dependency versions to use the workspace versions (see example below) | ||
|
||
It all ends up looking like this: | ||
|
||
<Tabs> | ||
<TabItem value="soroban-tutorial/Cargo.toml" label="Cargo.toml"> | ||
|
||
```toml | ||
[workspace] | ||
resolver = "2" | ||
members = [ | ||
"contracts/*", | ||
] | ||
|
||
[workspace.dependencies] | ||
soroban-sdk = "20.0.0" | ||
|
||
[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 | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="soroban-tutorial/contracts/hello-soroban/Cargo.toml" label="contracts/hello-soroban/Cargo.toml"> | ||
|
||
```toml | ||
```diff title="contracts/increment/Cargo.toml" | ||
[package] | ||
name = "hello-soroban" | ||
version = "0.1.0" | ||
-name = "hello-world" | ||
+name = "increment" | ||
version = "0.0.0" | ||
authors = ["Stellar Development Foundation <[email protected]>"] | ||
license = "Apache-2.0" | ||
edition = "2021" | ||
rust-version = "1.74.0" | ||
publish = false | ||
|
||
[lib] | ||
crate-type = ["cdylib"] | ||
doctest = false | ||
|
||
[dependencies] | ||
soroban-sdk = { workspace = true } | ||
|
||
[dev_dependencies] | ||
soroban-sdk = { workspace = true, features = ["testutils"] } | ||
|
||
[features] | ||
testutils = ["soroban-sdk/testutils"] | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
Now make sure everything works: | ||
|
||
soroban contract build | ||
|
||
Everything should build. | ||
|
||
cargo test | ||
|
||
All tests should pass. | ||
|
||
## Code | ||
|
||
Rather than initializing the new contract with `cargo new`, let's copy the `hello-soroban` project: | ||
We also need to create the `contracts/increment/src` directory, and `lib.rs` and `test.rs` files within it. | ||
|
||
```bash | ||
cp -r contracts/hello-soroban contracts/incrementor | ||
``` | ||
|
||
You'll need to update the `Cargo.toml` file to reflect the correct name: | ||
|
||
```diff title="contracts/incrementor/Cargo.toml" | ||
[package] | ||
-name = "hello-soroban" | ||
+name = "incrementor" | ||
version = "0.1.0" | ||
edition = "2021" | ||
mkdir -p contracts/increment/src && \ | ||
touch contracts/increment/src/lib.rs contracts/increment/src/test.rs | ||
``` | ||
|
||
And now in `contracts/incrementor/src/lib.rs`, we'll replace the contents with the following: | ||
We'll add the following code to `contracts/increment/src/lib.rs`, and go over it in more detail below. | ||
|
||
```rust title="contracts/incrementor/src/lib.rs" | ||
```rust | ||
#![no_std] | ||
use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol}; | ||
|
||
|
@@ -184,44 +101,31 @@ impl IncrementorContract { | |
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test; | ||
``` | ||
|
||
Make sure it builds: | ||
|
||
soroban contract build | ||
|
||
Check that it built: | ||
|
||
ls target/wasm32-unknown-unknown/release/*.wasm | ||
### Imports | ||
|
||
You should see both `hello_soroban.wasm` and `incrementor.wasm`. | ||
This contract begins similarly to our Hello World contract, with an annotation to exclude the Rust standard library, and imports of the types and macros we need from the `soroban-sdk` crate. | ||
|
||
## How it Works | ||
|
||
Follow along in your `contracts/incrementor/src/lib.rs` file. | ||
```rust title="contracts/increment/src/lib.rs" | ||
#![no_std] | ||
use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol}; | ||
``` | ||
|
||
### Contract Data Keys | ||
|
||
Contract data is associated with a key. The key can be used at | ||
a later time to look up the value. | ||
|
||
`Symbol` is a short (up to 32 characters long) string type with limited | ||
character space (only `a-zA-z0-9_` characters are allowed). Identifiers like | ||
contract function names and contract data keys are represented by `Symbol`s. | ||
|
||
The `symbol_short!()` macro is a convenient way to pre-compute short symbols up to 9 characters in length at compile time using `Symbol::short`. It generates a compile-time constant that adheres to the valid character set of letters (a-zA-Z), numbers (0-9), and underscores (\_). If a symbol exceeds the 9-character limit, `Symbol::new` should be utilized for creating symbols at runtime. | ||
|
||
```rust | ||
const COUNTER: Symbol = symbol_short!("COUNTER"); | ||
``` | ||
|
||
### Contract Data Access | ||
Contract data is associated with a key, which can be used at a later time to look up the value. | ||
|
||
The `Env.storage()` function is used to access and update contract data. The executing contract is the only contract that can query or modify contract data that it has stored. The data stored is viewable on ledger anywhere the ledger is viewable, but contracts executing within the Soroban environment are restricted to their own data. | ||
`Symbol` is a short (up to 32 characters long) string type with limited character space (only `a-zA-z0-9_` characters are allowed). Identifiers like contract function names and contract data keys are represented by `Symbol`s. | ||
|
||
The `get()` function gets the current value associated with the counter key. | ||
The `symbol_short!()` macro is a convenient way to pre-compute short symbols up to 9 characters in length at compile time using `Symbol::short`. It generates a compile-time constant that adheres to the valid character set of letters (a-zA-Z), numbers (0-9), and underscores (\_). If a symbol exceeds the 9-character limit, `Symbol::new` should be utilized for creating symbols at runtime. | ||
|
||
### Contract Data Access | ||
|
||
```rust | ||
let mut count: u32 = env | ||
|
@@ -231,29 +135,59 @@ let mut count: u32 = env | |
.unwrap_or(0); // If no value set, assume 0. | ||
``` | ||
|
||
The `Env.storage()` function is used to access and update contract data. The executing contract is the only contract that can query or modify contract data that it has stored. The data stored is viewable on ledger anywhere the ledger is viewable, but contracts executing within the Soroban environment are restricted to their own data. | ||
|
||
The `get()` function gets the current value associated with the counter key. | ||
|
||
If no value is currently stored, the value given to `unwrap_or(...)` is returned instead. | ||
|
||
Values stored as contract data and retrieved are transmitted from [the environment](../soroban-internals/environment-concepts.mdx) and expanded into the type specified. In this case a `u32`. If the value can be expanded, the type returned will be a `u32`. Otherwise, if a developer caused it to be some other type, a panic would occur at the unwrap. | ||
|
||
The `set()` function stores the new count value against the key, replacing the existing value. | ||
|
||
```rust | ||
env.storage() | ||
.instance() | ||
.set(&COUNTER, &count); | ||
``` | ||
|
||
The `set()` function stores the new count value against the key, replacing the existing value. | ||
|
||
### Managing Contract Data TTLs with `extend_ttl()` | ||
|
||
All contract data has a Time To Live (TTL), measured in ledgers, that must be periodically extended. If an | ||
entry's TTL is not periodically extended, the entry will eventually become "archived". You can learn more about this in the [State Archival](../soroban-internals/state-archival.mdx) document. | ||
```rust | ||
env.storage().instance().extend_ttl(100, 100); | ||
``` | ||
|
||
All contract data has a Time To Live (TTL), measured in ledgers, that must be periodically extended. If an entry's TTL is not periodically extended, the entry will eventually become "archived". You can learn more about this in the [State Archival](../soroban-internals/state-archival.mdx) document. | ||
|
||
For now, it's worth knowing that there are three kinds of storage: `Persistent`, `Temporary`, and `Instance`. This contract only uses `Instance` storage: `env.storage().instance()`. Every time the counter is incremented, this storage's TTL gets extended by 100 [ledgers](https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/ledgers), or about 500 seconds. | ||
|
||
### Build the contract | ||
|
||
For now, it's worth knowing that there are three kinds of storage: | ||
`Persistent`, `Temporary`, and `Instance`. This contract only uses `Instance` storage: `env.storage().instance()`. Every time the counter is incremented, this storage's TTL gets extended by 100 [ledgers](https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/ledgers), or about 500 seconds. | ||
```sh | ||
soroban contract build | ||
``` | ||
|
||
Check that it built: | ||
|
||
ls target/wasm32-unknown-unknown/release/*.wasm | ||
|
||
You should see both `hello_world.wasm` and `increment.wasm`. | ||
|
||
:::info | ||
|
||
The `contract init` command allows us to initialize a new project with any of the example contracts from the [https://github.com/stellar/soroban-examples](https://github.com/stellar/soroban-examples) repo, with the `--with-example` (or `-w`) flag. | ||
|
||
It will not overwrite existing files, so we could run the command again, from inside the `hello-world` directory, with a `--with-example` flag to add an `increment` contract. | ||
|
||
```bash | ||
soroban contract init ./ --with-example increment | ||
``` | ||
|
||
::: | ||
|
||
## Tests | ||
|
||
Open the `contracts/increment/src/test.rs` file and replace the contents with: | ||
Open the `contracts/increment/src/test.rs` file and add the following test: | ||
|
||
```rust title="contracts/incrementor/src/test.rs" | ||
use crate::{IncrementorContract, IncrementorContractClient}; | ||
|
@@ -275,9 +209,11 @@ This uses the same concepts described in the Hello World example. | |
|
||
Make sure it passes: | ||
|
||
cargo test | ||
```sh | ||
cargo test | ||
``` | ||
|
||
You'll see that this runs tests for the whole workspace; both the Hello World contract and the new Incrementor. | ||
You'll see that this runs tests for the whole workspace; both the Hello World contract and the new Increment. | ||
|
||
If you want to see the output of the `log!` call, run the tests with `--nocapture`: | ||
|
||
|
@@ -299,18 +235,8 @@ test test::incrementor ... ok | |
|
||
Can you figure out how to add `get_current_value` function to the contract? What about `decrement` or `reset` functions? | ||
|
||
## Commit your changes | ||
|
||
Looking at your git diff will be interesting now. It's probably kind of noisy if you just run `git status` or `git diff` right away, but once you `git add .`, git will understand the _renames_ (aka "moves") of all the old `hello-soroban` files better. | ||
|
||
Go ahead and commit it. | ||
|
||
```bash | ||
git commit -m "add incrementor contract" | ||
``` | ||
|
||
## Summary | ||
|
||
In this section, we added a new contract to this project, reorganizing the project as a multi-contract project using Cargo Workspaces. The new contract made use of Soroban's storage capabilities to store and retrieve data. We also learned about the different kinds of storage and how to manage their TTLs. | ||
In this section, we added a new contract to this project, that made use of Soroban's storage capabilities to store and retrieve data. We also learned about the different kinds of storage and how to manage their TTLs. | ||
|
||
Next we'll learn a bit more about deploying contracts to Soroban's Testnet network and interact with our incrementor contract using the CLI. |