Skip to content

Commit caa3df1

Browse files
committed
feat: first draft of tutorial
1 parent 7392ee1 commit caa3df1

File tree

4 files changed

+897
-0
lines changed

4 files changed

+897
-0
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Getting Started with Scaffold Stellar
2+
3+
We'll use Scaffold Stellar to create a new Smart Contract project and walk through some simple improvements to show common blockchain interactions that will be helpful when you start crafting your own contracts. This includes:
4+
5+
- storing data
6+
- authentication
7+
- handling transactions
8+
- obfuscating private data
9+
10+
While building these features, you'll also learn the life cycle of smart contract development including
11+
12+
- compiling
13+
- debugging
14+
- testing
15+
- deploying
16+
- and upgrading
17+
18+
19+
## Prerequisites
20+
21+
You should have a basic understanding of using the command line and of general programming concepts. Stellar Contracts are written in a subset of Rust, although we'll walk through the code together so don't worry if this is your first time with the language.
22+
23+
- install rust
24+
- add rust target
25+
- install scaffold stellar
26+
27+
28+
# 🏗️ Create the Scaffold
29+
30+
Our smart contract will be a Guess The Number game. You (the admin) can deploy the contract, randomly select a number between 1 and 10, and seed the contract with a prize. Users can make guesses and win the prize if they're correct!
31+
32+
Let's use the Stellar CLI tool to get a starting point. Open your terminal and navigate to the directory you keep your projects, then type:
33+
34+
```bash
35+
$ stellar scaffold init my-project
36+
```
37+
38+
39+
You can call your project anything you'd like. Navigate into the created directory and you will see a generated project structure including many of these files and folders:
40+
41+
```
42+
.
43+
├── Cargo.lock
44+
├── Cargo.toml
45+
├── contracts/
46+
│ ├── guess_the_number/
47+
│ │   ├── Cargo.toml
48+
│ │   ├── Makefile
49+
│ │   ├── src/
50+
│ │   │   ├── lib.rs
51+
│ │   │   └── test.rs
52+
├── environments.toml
53+
├── packages/
54+
├── README.md
55+
└── rust-toolchain.toml
56+
```
57+
58+
The `Cargo.toml` file is called the project's [manifest](https://doc.rust-lang.org/cargo/reference/manifest.html) and it contains metadata needed to compile everything and package it up. It's where you can name and version your project as well as list the dependencies you need. But we'll look at this later.
59+
60+
`Cargo.lock` is Rust's [lockfile](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html) and it has exact info about the dependencies we actually installed. It's maintained by Cargo and we shouldn't touch it.
61+
62+
The `contracts` directory holds each smart contract as a separate package in our project's workspace. We only need the one for this project, but it's nice to know that we can use the same structure for more complex projects that require multiple contracts.
63+
64+
We can configure how and where our contract will be built and deployed to in the `environments.toml` file. And if we generate client code from our contract, that will go in the `packages/` directory which is currently empty (because we haven't built anything yet!). We'll do that soon.
65+
66+
Finally, the `rust-toolchain.toml` file notes which version of Rust we're using and what platform we're targeting. We won't worry about the rest of the files in here yet, they're all for the frontend dApp we'll talk about later.
67+
68+
## 🔎 Understand the Starter Code
69+
70+
Let's open up the initial smart contract code in `contracts/guess-the-number/src/lib.rs` and walk through it.
71+
72+
```rust
73+
#![no_std]
74+
use admin_sep::{Administratable, Upgradable};
75+
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol};
76+
```
77+
78+
Rust has a great [standard library](https://doc.rust-lang.org/std/) of types, functions, and other abstractions. But our smart contract will run in a constrained WebAssembly environment where the full library isn't available or needed. The `#![no_std]` directive forces us to use only core Rust features.
79+
80+
We can still use explicitly imported features, though, and that's what the next two lines are doing. Here we're importing two traits from the `admin_sep` crate to help with admin functionality:
81+
- `Administratable` provides functions like `require_admin()` to manage who can preform administrative actions
82+
- `Upgradable` allows the contract to be upgraded to new versions while preserving its state
83+
84+
We're also importing essential items from Stellar's Soroban SDK, and we'll explain each as we get to them. You'll see that many of them replace items from the standard library but are designed for use in Soroban's environment. And the first is `contract`:
85+
86+
```rust
87+
#[contract]
88+
pub struct GuessTheNumber;
89+
90+
#[contractimpl]
91+
impl Administratable for GuessTheNumber {}
92+
93+
#[contractimpl]
94+
impl Upgradable for GuessTheNumber {}
95+
```
96+
97+
The `#[...]` syntax in Rust is called an [attribute](https://doc.rust-lang.org/reference/attributes.html). It's a way to label code for the compiler to handle it with special instructions. *Inner* attributes (with the `#!`) apply to the scope they're within, and *Outer* attributes (just the `#`) apply to the next line.
98+
99+
Here defining a [struct](https://doc.rust-lang.org/book/ch05-01-defining-structs.html) (a "structure" to hold values) and applying attributes of a Stellar smart contract. Then we'll use the pre-defined [traits](https://doc.rust-lang.org/book/ch10-02-traits.html) we just explained to add more functionality to our contract without actually having to write it ourselves.
100+
101+
```rust
102+
const THE_NUMBER: Symbol = symbol_short!("n");
103+
```
104+
105+
Now the most important part of our contract: the number! This line creates a key for storing and retrieving contract data. A `Symbol` is a short string type (max 32 characters) that is more optimized for use on the blockchain. And we're using the `symbol_short` macro for an even smaller key (max 9 characters). As a contract author, you want to use tricks like this to lower costs as much as you can.
106+
107+
```rust
108+
#[contractimpl]
109+
impl GuessTheNumber {
110+
```
111+
112+
Let's `impl`ement our contract's functionality.
113+
114+
```rust
115+
pub fn __constructor(env: &Env, admin: &Address) {
116+
Self::set_admin(env, admin);
117+
}
118+
```
119+
120+
A contract's `constructor` runs when it is deployed. In this case, we're saying who has access to the admin functions. We don't want anyone to be able to reset our number, do we?!
121+
122+
```rust
123+
/// Update the number. Only callable by admin.
124+
pub fn reset(env: &Env) {
125+
Self::require_admin(env);
126+
let new_number: u64 = env.prng().gen_range(1..=10);
127+
env.storage().instance().set(&THE_NUMBER, &new_number);
128+
}
129+
```
130+
131+
And here is the reset function. Note that we use `require_admin()` here so only you can run this function. It generates a random number between 1 and 10 and uses our key to store it.
132+
133+
```rust
134+
/// Guess a number between 1 and 10
135+
pub fn guess(env: &Env, a_number: u64) -> bool {
136+
a_number == env.storage().instance().get::<_, u64>(&THE_NUMBER).unwrap()
137+
}
138+
}
139+
```
140+
141+
Finally, we add the `guess` function which accepts a number as the guess and compares it to the stored number, returning the result. Notice we're using our defined key (that small Symbol) to find stored data that may or may not be there. That's why we need [`unwrap()`](https://doc.rust-lang.org/rust-by-example/error/option_unwrap.html), but we'll talk more about `Option` values later in the tutorial.
142+
143+
```rust
144+
mod test;
145+
```
146+
147+
Post Script: this last line includes the test module into this file. It's handy to write unit tests for our code in a separate file (`contracts/guess-the-number/src/test.rs`), but you could also write them inline if you want.
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# Making Some Improvements
2+
3+
In our initial version, we had a problem: the `guess` function would crash if no number was set yet. Let's fix this by improving how our contract initializes and by creating reusable code for number generation.
4+
5+
## What We'll Accomplish
6+
7+
By the end of this step, you'll have:
8+
9+
- A contract that sets a number immediately upon deployment
10+
- A private helper function for generating random numbers
11+
- A more robust `reset` function that uses our helper
12+
- Better error handling in the `guess` function
13+
14+
## Understanding the Problem
15+
16+
In our current contract, the `__constructor` only sets the admin, but doesn't set an initial number. This means:
17+
18+
1. If someone calls `guess` before `reset`, it will crash with `unwrap()` on `None`
19+
2. The number generation logic is only in `reset`, making it hard to reuse
20+
21+
Let's fix these issues!
22+
23+
## Step 1: 🔒 Create a Private Helper Function
24+
25+
First, let's extract the number generation into a private helper function. This follows the DRY principle (Don't Repeat Yourself) and makes our code more maintainable.
26+
27+
Open `contracts/guess-the-number/src/lib.rs` and add this private function inside the `impl GuessTheNumber` block:
28+
29+
```rust
30+
#[contractimpl]
31+
impl GuessTheNumber {
32+
// ... existing functions ...
33+
34+
/// Private helper function to generate and store a new random number
35+
fn set_random_number(env: &Env) {
36+
let new_number: u64 = env.prng().gen_range(1..=10);
37+
env.storage().instance().set(&THE_NUMBER, &new_number);
38+
}
39+
}
40+
```
41+
42+
### Understanding Private Functions
43+
44+
Notice that this function doesn't have `pub` in front of it - this makes it private. Private functions:
45+
46+
- Can only be called from within the same contract
47+
- Don't become part of the contract's public API
48+
- Are useful for internal logic and code reuse
49+
- Help keep your contract interface clean and focused
50+
51+
## Step 2: 👷‍♂️ Update the Constructor
52+
53+
Now let's modify the `__constructor` to set an initial number when the contract is deployed:
54+
55+
```rust
56+
pub fn __constructor(env: &Env, admin: &Address) {
57+
Self::set_admin(env, admin);
58+
Self::set_random_number(env); // Add this line
59+
}
60+
```
61+
62+
### Why This Improves Things
63+
64+
By setting a number in the constructor:
65+
66+
1. **Immediate functionality**: The contract works right after deployment
67+
2. **No crash risk**: `guess` will never encounter a missing number
68+
3. **Better user experience**: Players can start guessing immediately
69+
70+
## Step 3: ♻️ Update the Reset Function
71+
72+
Let's simplify our `reset` function to use the new helper:
73+
74+
```rust
75+
/// Update the number. Only callable by admin.
76+
pub fn reset(env: &Env) {
77+
Self::require_admin(env);
78+
Self::set_random_number(env);
79+
}
80+
```
81+
82+
Much cleaner! The logic is now centralized in our helper function.
83+
84+
## Step 4: ⚠️ Improve Error Handling
85+
86+
Let's make the `guess` function more robust by replacing `unwrap()` with `expect()`:
87+
88+
```rust
89+
/// Guess a number between 1 and 10
90+
pub fn guess(env: &Env, a_number: u64) -> bool {
91+
let stored_number = env.storage()
92+
.instance()
93+
.get::<_, u64>(&THE_NUMBER)
94+
.expect("No number has been set");
95+
96+
a_number == stored_number
97+
}
98+
```
99+
100+
### Understanding `expect()` vs `unwrap()`
101+
102+
- `unwrap()`: Crashes with a generic error message
103+
- `expect()`: Crashes with your custom error message
104+
- Both will panic if the value is `None`, but `expect()` gives better debugging info
105+
106+
In our case, this should never happen since we now set a number in the constructor, but it's good defensive programming.
107+
108+
## Step 5: Your Complete Updated Contract
109+
110+
Here's what your `lib.rs` should look like now:
111+
112+
```rust
113+
#![no_std]
114+
use admin_sep::{Administratable, Upgradable};
115+
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol};
116+
117+
#[contract]
118+
pub struct GuessTheNumber;
119+
120+
#[contractimpl]
121+
impl Administratable for GuessTheNumber {}
122+
123+
#[contractimpl]
124+
impl Upgradable for GuessTheNumber {}
125+
126+
const THE_NUMBER: Symbol = symbol_short!("n");
127+
128+
#[contractimpl]
129+
impl GuessTheNumber {
130+
pub fn __constructor(env: &Env, admin: &Address) {
131+
Self::set_admin(env, admin);
132+
Self::set_random_number(env);
133+
}
134+
135+
/// Update the number. Only callable by admin.
136+
pub fn reset(env: &Env) {
137+
Self::require_admin(env);
138+
Self::set_random_number(env);
139+
}
140+
141+
/// Guess a number between 1 and 10
142+
pub fn guess(env: &Env, a_number: u64) -> bool {
143+
let stored_number = env.storage()
144+
.instance()
145+
.get::<_, u64>(&THE_NUMBER)
146+
.expect("No number has been set");
147+
148+
a_number == stored_number
149+
}
150+
151+
/// Private helper function to generate and store a new random number
152+
fn set_random_number(env: &Env) {
153+
let new_number: u64 = env.prng().gen_range(1..=10);
154+
env.storage().instance().set(&THE_NUMBER, &new_number);
155+
}
156+
}
157+
158+
mod test;
159+
```
160+
161+
## Step 6: 🧪 Test Your Improvements
162+
163+
Let's test that our improvements work:
164+
165+
### Build and Deploy
166+
167+
```bash
168+
$ stellar contract build
169+
170+
$ stellar contract deploy \
171+
--wasm target/wasm32v1-none/release/guess_the_number.wasm \
172+
--source alice \
173+
--network local
174+
```
175+
176+
### Test Immediate Functionality
177+
178+
Try guessing right after deployment (without calling reset first):
179+
180+
```bash
181+
$ stellar contract invoke \
182+
--id [CONTRACT_ID] \
183+
--source alice \
184+
--network local \
185+
-- guess --a_number 5
186+
```
187+
188+
This should now work! You'll get either `true` or `false` instead of a crash.
189+
190+
### Test the Reset Function
191+
192+
```bash
193+
$ stellar contract invoke \
194+
--id [CONTRACT_ID] \
195+
--source alice \
196+
--network local \
197+
-- reset
198+
199+
# Try guessing again
200+
$ stellar contract invoke \
201+
--id [CONTRACT_ID] \
202+
--source alice \
203+
--network local \
204+
-- guess --a_number 3
205+
```
206+
207+
Perfect! The contract now works reliably from the moment it's deployed.
208+
209+
## What We've Learned
210+
211+
In this step, we covered several important concepts:
212+
1. Code Organization
213+
- **Private functions**: Help organize code and prevent external access to internal logic
214+
- **DRY principle**: Don't repeat yourself - extract common logic into reusable functions
215+
2. Contract Lifecycle
216+
- **Constructor patterns**: Set up initial state when the contract is deployed
217+
- **Defensive programming**: Always ensure your contract is in a valid state
218+
3. Error Handling
219+
- **expect() vs unwrap()**: Better error messages help with debugging
220+
- **Graceful degradation**: Handle edge cases so your contract doesn't crash
221+
4. Blockchain Development Best Practices
222+
- **Immediate functionality**: Contracts should work right after deployment
223+
- **Consistent state**: Always maintain valid state throughout the contract's lifecycle
224+
225+
Our contract is now much more robust:
226+
227+
- ✅ Works immediately after deployment
228+
- ✅ Clean, reusable code structure
229+
- ✅ Better error handling
230+
- ❌ Still no authentication (anyone can guess)
231+
- ❌ Still no transactions (how do you win the prize? what prize?)
232+
233+
## What's Next?
234+
235+
In the next step, we'll tackle authentication by:
236+
237+
- Converting `guess` from a view function to a transaction
238+
- Requiring users to be signed in to guess
239+
- Adding a `guesser` parameter to track who made each guess
240+
241+
This will prepare us for adding economic incentives in later steps!

0 commit comments

Comments
 (0)