|
| 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