Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Rework storing-data.mdx
Browse files Browse the repository at this point in the history
  • Loading branch information
elizabethengelman committed Feb 19, 2024
1 parent 385ece0 commit a2d51cd
Showing 1 changed file with 74 additions and 148 deletions.
222 changes: 74 additions & 148 deletions docs/getting-started/storing-data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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
Expand All @@ -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};
Expand All @@ -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`:

Expand All @@ -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.

0 comments on commit a2d51cd

Please sign in to comment.