Saltar al contenido principal

Tutorial: A Guess the Number Game

This section will guide you through the development workflow for using Scaffold Stellar to build and deploy a Guess the Number game with a simple smart contract and an integrated frontend application. You should have a basic understanding of the command line and of general programming concepts, but we'll walk through all the code together so don't worry if you're new to Stellar, Rust, or dApp development.

tip

If you just want to get up and running quickly, check out the Quick Start guide.

We'll cover:

  1. Setting up a development environment
  2. Initializing a new project
  3. Exploring the scaffolded project structure
  4. Understanding code in an example contract
  5. Running the application

🛠️ Setup Your Development Environment

First, follow the Setup Instructions here to install the necessary tools for Stellar contract development, specifically these sections:

  • Install Rust, Cargo (for managing Rust projects), and the compilation target
  • Configure your editor for Rust development
  • Install the Stellar CLI

To work with Scaffold Stellar, we'll need a few more things.

Node

Go to the Node.js download page and follow the instructions to the the LTS version on your operating system. You can also use a version manager like nvm or install using Homebrew if you prefer. This should also install npm as well.

brew install node@22

# Verify installation
node -v # should print "v22.20.0" or higher
npm -v # should print "10.9.3" or higher

Docker

We'll run a local Stellar network inside a Docker container, so head to the Get Docker page and follow the instructions for installing Docker Desktop for your operating system. Once it's installed, open it up. It needs to be running in the background but then Scaffold Stellar will handle the rest.

Scaffold Stellar

Lastly, we'll install the Scaffold Stellar plugin for the Stellar CLI. We suggest using cargo-binstall to install it, which is a tool for installing Rust binaries.

If you don't have it installed, you can do so with:

Macos or Linux:

curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash

Windows:

Set-ExecutionPolicy Unrestricted -Scope Process; iex (iwr "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.ps1").Content

Then install Scaffold Stellar with:

cargo binstall -y stellar-scaffold-cli

Or if you prefer, you can install it directly with Cargo which will compile it from source:

cargo install --locked stellar-scaffold-cli

Preferred installation is binstall

🏗️ Initialize Your Project

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!

Let's initialize a project. Open your terminal and navigate to the directory where you keep your projects, then type:

stellar scaffold init guessing-game-tutorial

This creates a new project from our starter template containing everything you need. You can call your project anything you'd like. Navigate into the created directory and install its NPM dependencies:

cd guessing-game-tutorial
npm install

Now you're ready to run it:

npm start

This command does two things:

  1. Starts the development server for the frontend using Vite.
  2. Watches for changes in contract code and rebuilds them automatically using Stellar Scaffold's watch command.

The first time you compile these smart contracts can take a while. While it does its thing, let's have a look around.

🗂️ Exploring the Project Structure

Open the project in your editor. You will see a generated project structure including these files and folders:

.
├── .env
├── Cargo.lock
├── Cargo.toml
├── contracts/
│ └──guess-the-number
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ └── test.rs
├── environments.toml
├── packages/
├── README.md
└── rust-toolchain.toml

There are a few more files than the ones listed here, but let's highlight some important ones:

  • Rust and Cargo configuration:
    • Cargo.toml: the project's manifest, containing metadata needed to compile everything and package it up. This is where you can name and version your project as well as list the dependencies you need.
    • Cargo.lock: Cargo's lockfile with exact info about the project's dependencies. We should not manually edit this file, though we should check it into git or other source control.
    • rust-toolchain.toml: specifies which version of Rust we're using and what platform we're targeting.
  • contracts/: holds each smart contract as a separate package in our project's Rust workspace. We only need 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. The other example contracts in this folder come from our friends at OpenZeppelin.
  • packages/: holds each smart contract's client and types as a separate package for the project's NPM workspace. These are built by Scaffold Stellar and we should not manually edit them. They'll be used by the frontend.
  • .env: is where we store environment variables that we'll be used by Scaffold Stellar commands.
  • environments.toml: This is the Scaffold Stellar secret sauce! This file is where we configure:
    • our project's various environments, ...
    • which networks are used by each environment, ...
    • all in service of which contracts our project depends on in each of those environments.

So how do all these pieces work together? Here's what Scaffold Stellar handles for you:

  1. Our npm start command runs stellar scaffold watch --build-clients
  2. Our .env file set an environment variable to say we're in our development environment (STELLAR_SCAFFOLD_ENV=development)
  3. Scaffold Stellar looked to environments.toml for the development environment's configuration, which told it to:
    • Start up a local Stellar network
    • Create an account on the network
    • Build the contracts
    • Deploy them to the network
    • Generate their clients for the frontend

That's a lot of heavy lifting! Normally you'd have to do all this yourself, perhaps in a procedural script, but Scaffold Stellar does it for you. And it's deterministic, meaning you can always reproduce the same results from the same environment configuration. You set configuration values, specifying the desired starting state for your app, and Scaffold Stellar does all the work to get your app into that state.

And we're just getting started! We haven't even looked at the application yet.

🚀 Open the App

The npm start command should have finished building the contracts by now so you can open the app in your browser. It should be running at Vite's default port, http://localhost:5173, and you should see the home page:

Welcome to your app!

...

<GuessTheNumber />
Connect wallet to play the guessing game

In order to test out our deployed example contract, we'll need to connect to a wallet.

💰 Connect a Wallet

In the top right corner, you'll see a big "Connect" button. Click it. You need to have a Wallet in order to interact with the dApp. The modal that opened will show a few options if you don't have one already. We recommend using Freighter.

Once it's installed, we need to connect it to our local network running in Docker. Open the extension, click the menu, and navigate to "Settings," then "Network." Click the "Add custom network" button and enter the following info:

  • Name: Local
  • HORIZON RPC URL: http:localhost:8000
  • SOROBAN RPC URL: http:localhost:8000/rpc
  • Passphrase: Standalone Network ; February 2017
  • Friendbot URL: http:localhost:8000/friendbot
  • Check Allow connecting to non-HTTPS networks

ℹ️ The 🌐 icon in the extension lets you switch back and forth between this Local network as well as test and main net.

Now click the dApp's "Connect" button and follow the prompts to let the application communicate with Freighter. If it's successful, you should see your account info in the header along with a new "Fund Account" button and a tag for the current network. Click the "Fund Account" button so we can test some transactions.

Once your wallet balance has some XLM, you should see the "GuessTheNumber" component update with a text box. Go ahead and enter some guesses. Right out of the gate we have nice UI to invoke methods on the contract.

🔎 Understand the Contract Code

Now navigate to our contract explorer. Click the "Debugger" button in the header. These are our contract developer tools. They'll let you explore the contracts available to your application, view documentation, and even run methods to help debug them right from your app!

Select the guess_the_number contract and you should see its Contract ID from the local network deployment. You'll also see the contract's documentation for methods like:

  • reset: Update the number. Only callable by admin.
  • guess: Guess a number between 1 and 10

This is coming directly from our contract's documentation. Let's open up the initial smart contract code in contracts/guess-the-number/src/lib.rs and walk through it.

#![no_std]
use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Symbol};

Rust has a great standard library 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.

We can still use explicitly imported features, though, and that's what the next line is doing. Here we're importing some 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:

#[contract]
pub struct GuessTheNumber;

The #[...] syntax in Rust is called an attribute. 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 (meaning !#[no_std] applies to the whole file/module), and Outer attributes (just the #) apply to the next line.

In this case #[contract] is an attribute macro, which is a special function called by the compiler that generates code at compile time.

Here we're defining a struct (a "structure" to hold values) and applying attributes of a Stellar smart contract. A struct also allows defining methods. In this case the structs holds no values but we will still define methods on it.

const THE_NUMBER: &Symbol = &symbol_short!("n");
pub const ADMIN_KEY: &Symbol = &symbol_short!("ADMIN");

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.

The second line creates a key for storing the address of this contract's administrator. It's almost the same code as storing our number, but uses the & which is called a reference. Instead of the value, it's a pointer to where the value lives.

#[contractimpl]
impl GuessTheNumber {

Let's implement our contract's functionality.

    pub fn __constructor(env: &Env, admin: &Address) {
Self::set_admin(env, admin);
}

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 just anyone to be able to reset our number, do we?!

    /// Update the number. Only callable by admin.
pub fn reset(env: &Env) {
Self::require_admin(env);
let new_number: u64 = env.prng().gen_range(1..=10);
env.storage().instance().set(&THE_NUMBER, &new_number);
}

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.

    /// Guess a number between 1 and 10
pub fn guess(env: &Env, a_number: u64) -> bool {
a_number == env.storage().instance().get::<_, u64>(&THE_NUMBER).unwrap()
}
}

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(), but we'll talk more about Option values later in the tutorial.

mod test;

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.

👷 Let's Make a Change

We should still have our original npm start command running. I told you it did a lot of heavy lifting for you, but it also updates all of that automatically whenever you make changes to your code. Let's test it out by making a small change and watch the dev server update immediately.

The docstring for our guess function says to guess a number "between 1 and 10". But does that include "10"? Let's clarify:

  /// Guess a number between 1 and 10, inclusive
pub fn guess(env: &Env, a_number: u64) -> bool {

Save the file and watch your terminal output. The contracts get rebuilt, redeployed, and clients for them get regenerated for your frontend. Then Vite hot-reloads your app and you should see the change in the contract explorer in your browser.

Tada!

👀 Looking Deeper at the Output

Sharp eyed readers might have noticed interesting lines in the build output. Let's take a look:

[0] ℹ️ Starting local network

This is the Docker container running Stellar locally. You can manage it with stellar container commands, but Scaffold Stellar should handle everything for you. We recommend using the local network for development because it's simpler than working with the other networks, but we'll make use of them in later parts of the tutorial.

ℹ️ Build Summary:
[0] Wasm File: target/stellar/local/guess_the_number.wasm
[0] Wasm Hash: 2cda774ed515acb3af208478e64841d6253d52366f22156c5e7a550ee7b06cbe
[0] Exported Functions: 5 found
[0] • _
[0] • __constructor
[0] • guess
[0] • reset
[0] • upgrade
[0] ✅ Build Complete

You'll see a build summary for each of our contracts as they compile to Wasm, the code that will actually run on the blockchain. There's a few other functions in our contract that weren't exported here. That's because they're private. We'll implement our own in the next part of the tutorial and explain why they're useful.

[0] ℹ️ Creating keys for "me"
[0] ✅ Key saved with alias me in "/Users/zach/code/guessing-game-tutorial/.config/stellar/identity/me.toml"
[0] ✅ Account me funded on "Standalone Network ; February 2017"

Scaffold Stellar creates an account for you to use on the local network and funds it so you can actually deploy your contracts. You can manage these accounts with the stellar keys command.

[0] ✅ Deployed!
[0] ℹ️ ↳ contract_id: CBPAPSB7SXM3MNJVLXPSD6BRQ2ZN33OQVYWO45332TOP4PQLMCHJV4QN
[0] ℹ️ Running after_deploy script for "guess_the_number"

After deployment, our __constructor method is run which initializes our contract. This is a perfect time to setup anything your contract needs to run. We also ran an after_deploy script to call our contract's reset method and generate a random number for us to guess. These are all configured in the environments.toml file and we'll dive into that in the next part of the tutorial.

[0] ℹ️ Binding "guess_the_number" contract
...
[0] ✅ Client "guess_the_number" created successfully

This is final piece of the puzzle. Now that the contract is deployed on chain, we can use it to generate an RPC client and TypeScript bindings used in our frontend code.

🔎 Understand the Application Code

The app's home page uses the <GuessTheNumber /> component, so we can start by looking at that file in src/components/GuessTheNumber.tsx:

export const GuessTheNumber = () => {
const [guessedIt, setGuessedIt] = useState<boolean>();
const [theGuess, setTheGuess] = useState<number>();
const { address } = useWallet();

if (!address) {
return (
<Text as="p" size="md">
Connect wallet to play the guessing game
</Text>
);
}

We're storing some state for tracking the input's value and whether the guess was successful or not. And we're also using our custom useWallet hook to connect to the user's wallet and get their address. This is how we know whether or not you connected to Freighter.

  const submitGuess = async () => {
if (!theGuess) return;
const { result } = await game.guess({ a_number: BigInt(theGuess) });
setGuessedIt(result);
};

Next, we create a function to handle the user's submission. Hey! Look at that! It's one of our contract's methods right in our TypeScript code: game.guess(). Let's follow that import and look at src/contracts/guess_the_number.ts.

import * as Client from 'guess_the_number';
import { rpcUrl } from './util';

export default new Client.Client({
networkPassphrase: 'Standalone Network ; February 2017',
contractId: 'CBPAPSB7SXM3MNJVLXPSD6BRQ2ZN33OQVYWO45332TOP4PQLMCHJV4QN',
rpcUrl,
allowHttp: true,
publicKey: undefined,
});

This is the generated RPC client that Scaffold Stellar built for us. It allows us to call methods on the contract and even understand the types for their arguments and return values. You won't ever have to change this file, or the Client class in the /packages directory.

All you have to do is the fun part, focus on building your application instead of fussing about with all the details of how to get your application to talk to your contracts.

Summary

That covered a lot, but let's summarize how simple it actually was:

  1. We ran stellar scaffold init guessing-game-tutorial to generate a project from a starter template
  2. We ran npm install to get our dependencies
  3. We ran npm start to build and deploy the contracts to our local network, then run the application

That's it! Scaffold Stellar does all the heavy lifting, letting you jump right in to the fun parts of developing your contract and applications. 🎉

What's Next?

This is a great place to start, but there's a few things we can improve on. For example, our number is currently stored in plain text on the contract's storage. We

  • Improve the contract code to make it more robust
  • Learn about private contract methods
  • Practice debugging and handling errors
  • Write tests to make sure our contract code is sound

That will give you a better sense of the typical development workflow for contracts and dApps.