Skip to content

Instantly share code, notes, and snippets.

@sdbondi
Last active February 18, 2026 14:05
Show Gist options
  • Select an option

  • Save sdbondi/dd14fb4aaed87d36c1734f3578161c37 to your computer and use it in GitHub Desktop.

Select an option

Save sdbondi/dd14fb4aaed87d36c1734f3578161c37 to your computer and use it in GitHub Desktop.
Guessing game GIST for Ootle docs
[workspace]
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
tari_template_lib = "*"
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
tari_template_test_tooling = { git = "https://github.com/tari-project/tari-ootle.git", tag = "v0.23.0" }
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = 's' # Optimize for size.
lto = true # Enable Link Time Optimization.
codegen-units = 1 # Reduce number of codegen units to increase optimizations.
panic = 'abort' # Abort on panic.
strip = "debuginfo" # Strip debug info.
// Copyright 2026 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
use tari_template_lib::prelude::*;
// #[template]
mod template {
use std::{collections::HashMap, mem};
use super::*;
const MAXIMUM_GUESSES_PER_ROUND: usize = 5;
/// The guessing game component.
///
/// This component contains a vault for the prize NFT, the guesses and a number for the current round.
pub struct GuessingGame {
/// A vault containing the prize to be won for the round, or is empty if no round is active
prize_vault: Vault,
/// Contains up to 5 guesses from different users
guesses: HashMap<RistrettoPublicKeyBytes, Guess>,
round_number: u32,
}
impl GuessingGame {
/// Constructs a new Guessing Game component.
///
/// The caller must provide a component address allocation.
/// This allows the component to be created and called within one transaction instead of one transaction to
/// create the component and others to call it with the new address. While this is not strictly necessary, it is
/// good practice to provide this in constructors.
pub fn new(address: ComponentAddressAllocation) -> Component<Self> {
// Create a new NFT Resource that will be awarded to winners
let prize_resource = ResourceBuilder::non_fungible()
// Optionally give it a name
.metadata("name", "Guessing Game Prize")
// Optionally, provide a token symbol that exchanges and explorers will display for the new resource
.with_token_symbol("🎲")
// Create the resource with no supply (use `.initial_supply(...)` to mint new NFTs in the builder)
.build();
// By default, all component methods are restricted and can only be
// called by the owner that created it.
// For our game, that is perfect for starting and ending a round, we want to allow anybody to place a
// guess.
let access_rules = ComponentAccessRules::new()
// Here we allow anyone to call the "guess" method.
.method("guess", rule![allow_all]);
// Construct the component
Component::new(Self {
// We create an empty vault that will hold our prize NFT
prize_vault: Vault::new_empty(prize_resource),
guesses: HashMap::new(),
round_number: 0,
})
.with_address_allocation(address)
.with_access_rules(access_rules)
.create()
}
/// Starts a new game and mint the prize to be won.
///
/// Callable by: the component owner
///
/// # Panics
///
/// Panics if a round is already started.
pub fn start_game(&mut self, prize: NonFungibleId) {
assert!(!self.is_game_in_progress(), "Game already in progress!");
self.round_number += 1;
// To mint the prize, we need a ResourceManager. For convenience, a Vault provides the
// `get_resource_manager` method that returns a resource manager for the resource it holds.
let manager = self.prize_vault.get_resource_manager();
// Mine the prize. Each NFT has immutable (cannot be changed) data and mutable (holder can change) data.
// We pass in the round number as the immutable data to mint this round in NFT history
// and empty (unit) for the mutable data.
let prize = manager.mint_non_fungible(prize, &metadata!["round" => self.round_number.to_string()], &());
self.prize_vault.deposit(prize);
}
/// Places a guess. If the guess is correct and selected to win, payouts will be made into the provided
/// component. NOTE: this component must have a `deposit(bucket: Bucket)` function (typically a built-in
/// Account component)
///
/// Callable by: anyone
///
/// # Panics
///
/// Panics if the guess is not between 0 and 10, if the player has already made a guess this round, if the
/// maximum number of guesses has already been reached or if no game is in progress.
pub fn guess(&mut self, guess: u8, payout_to: ComponentAddress) {
assert!(guess <= 10, "Guess must be from 0 to 10");
assert!(
self.guesses.len() < MAXIMUM_GUESSES_PER_ROUND,
"No more guesses allowed"
);
assert!(self.is_game_in_progress(), "No game has been started");
// We'll get the signer of the transaction to use as the player identifier for the guess.
let player = CallerContext::transaction_signer_public_key();
// Create a ComponentManager. This is a wrapper around a component address that allows us
// to call methods on the component. We'll use this in end_game_and_payout.
let payout_to = ComponentManager::get(payout_to);
// Insert the guess into the hashmap, assert that the player hasn't already made a guess this round.
let maybe_previous_guess = self.guesses.insert(player, Guess { payout_to, guess });
assert!(maybe_previous_guess.is_none(), "You already guessed in this round");
}
/// Ends the game, determines the winner and pays out the prize. If there are multiple winners, the first one
/// found will win. If there are no winners, the prize is burned.
///
/// Callable by: the component owner
///
/// # Panics
///
/// Panics if no game is in progress.
pub fn end_game_and_payout(&mut self) {
// Withdraw the prize into `Bucket`. A Bucket is a container for a single resource and is used to pass
// resources around. A bucket MUST either be deposited into a vault or burnt by the end of the transaction.
// You can also return buckets which allows them to be used at the transaction-level.
// In fact, returning them is typically how you'd use buckets but that doesn't work in this case, since we
// don't know the winner ahead of time.
//
// This will panic if there is no prize in the vault, which also means that no game is in progress.
let mut prize = self.prize_vault.withdraw(1u64);
// Generate a (pseudo) random number to determine the winner.
let number = generate_number();
// Take the guesses and reset the guesses for the next round.
let guesses = mem::take(&mut self.guesses);
let num_participants = guesses.len();
for (player, guess) in guesses {
if guess.guess == number {
// We have a winner! Payout the prize to the component specified in the guess.
// This is a cross component call that invokes the `deposit` method on the component specified in
// the guess. We pass in the bucket containing the prize as an argument.
guess.payout_to.invoke("deposit", args![prize]);
// Emit an event with the winner and some game metadata.
// Events are a great way to provide information about what happened during a transaction execution.
// They can be indexed and queried by explorers and other tools.
emit_event("GameEnded", metadata![
"winner" => player.to_string(),
"winner_account" => guess.payout_to.component_address().to_string(),
"number" => number.to_string(),
"num_participants" => num_participants.to_string(),
]);
return;
}
}
// No winner, bye bye prize!
emit_event(
"GameEnded",
metadata!["number" => number.to_string(), "num_participants" => num_participants.to_string()],
);
prize.burn();
}
fn is_game_in_progress(&self) -> bool {
// If the prize vault has an NFT, then game on!
!self.prize_vault.balance().is_zero()
}
}
}
fn generate_number() -> u8 {
use tari_template_lib::rand::random_bytes;
let num = random_bytes(1)[0];
// Squish it to between 0 and 10
num % 11
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Guess {
pub payout_to: ComponentManager,
pub guess: u8,
}
// Copyright 2025 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
use tari_ootle_transaction::{Transaction, args};
use tari_template_lib::{
prelude::ComponentAddress,
types::{NonFungibleAddress, NonFungibleId},
};
use tari_template_test_tooling::{TemplateTest, support::assert_error::assert_reject_reason};
const TEMPLATE_PATHS: &[&str] = &["tests/templates/guessing_game"];
const TEMPLATE_NAME: &str = "GuessingGame";
const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR");
#[test]
fn it_works() {
// Initialize the test harness. `my_crate` expects the wasm template code to be in the same crate as this test.
let mut test = TemplateTest::my_crate();
let template = test.get_template_address(TEMPLATE_NAME);
let prize = NonFungibleId::from_string("💎");
// The owner of the template starts a new game by calling the `start_game` method of the component. This will create
// a new game instance and allocate a new component address for it. NOTE: for simplicity in tests, fees are
// turned off by default. To turn them on use `test.enable_fees()`, you will then need to add fee instructions to
// pay the fee.
test.execute_expect_success(
Transaction::builder_localnet()
// Allocate a new component address
.allocate_component_address("guessing_game")
// Construct the component by calling the `new` function of the template, passing the address allocation as an argument
.call_function(template, "new", args![Workspace("guessing_game")])
// Start a new game
.call_method("guessing_game", "start_game", args![prize])
.build_and_seal(test.secret_key()),
vec![test.owner_proof()],
);
// A game has started and the players can now make their guesses.
// First, we need to find the component address of the game that was just started. In tests, we can do this by
// querying the state store for components that were created from the template address.
let (game_address, _) = test
.read_only_state_store()
.get_components_by_template_address(template)
.unwrap()
.remove(0);
// Let's create some accounts for the players
let (user1_account, _user1_proof, user1_secret) = test.create_empty_account();
let (user2_account, _user2_proof, user2_secret) = test.create_empty_account();
let (user3_account, _user3_proof, user3_secret) = test.create_empty_account();
// Just to demonstrate, we'll test an "unhappy path": user 1 makes a bad guess, since our template requires guesses
// between 0 and 10.
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.call_method(game_address, "guess", args![100, user1_account])
.build_and_seal(&user1_secret),
vec![],
);
// Matches the panic message in the template.
assert_reject_reason(reason, "Guess must be from 0 to 10");
// TIP: It is always a good idea to test all the various failure cases of a template in separate tests, but for
// brevity we'll skip this here.
// Let's make a correct guess with user 1
test.execute_expect_success(
Transaction::builder_localnet()
.call_method(game_address, "guess", args![5, user1_account])
.build_and_seal(&user1_secret),
vec![],
);
// User 2 makes a correct guess
test.execute_expect_success(
Transaction::builder_localnet()
.call_method(game_address, "guess", args![7, user2_account])
.build_and_seal(&user2_secret),
vec![],
);
// User 3 makes a correct guess
test.execute_expect_success(
Transaction::builder_localnet()
.call_method(game_address, "guess", args![3, user3_account])
.build_and_seal(&user3_secret),
vec![],
);
// Now let's end the game and check the results.
let result = test.execute_expect_success(
Transaction::builder_localnet()
.call_method(game_address, "end_game_and_payout", args![])
.build_and_seal(test.secret_key()),
vec![],
);
// Get the event out of the execution result.
let event = result
.finalize
.events
.iter()
.find(|event| {
// Template events are prefixed with the template name
event.topic() == "GuessingGame.GameEnded"
})
.expect("GameEnded event not found");
// It's difficult to test randomness, we have a 33% chance of a winner, so we'll have to assert either.
match event.get_payload("winner_account") {
Some(winner) => {
// Someone won, let's assert that it's one of the three players.
let winner = winner.parse::<ComponentAddress>().unwrap();
let winner = [user1_account, user2_account, user3_account]
.into_iter()
.find(|w| *w == winner)
.expect("Winner must be one of the three players");
let account = test.read_only_state_store().get_account(winner).unwrap();
let (_, vault) = account.vaults().first_key_value().unwrap();
let vault = test.read_only_state_store().get_vault(&vault.vault_id()).unwrap();
let nfts = vault.get_non_fungible_ids();
assert!(nfts.contains(&prize), "Winner must have received the prize");
eprintln!("Congratulations to the winner: {}", winner);
},
None => {
// No winner, better luck next time!
let resource_addr = test
.read_only_state_store()
.get_resources_by_owner(&test.to_public_key_bytes())
.unwrap()
.first()
.map(|(addr, _)| *addr)
.unwrap();
let nft = test
.read_only_state_store()
.get_substate(&NonFungibleAddress::new(resource_addr, prize.clone()).into())
.unwrap();
assert!(
nft.substate_value().as_non_fungible().unwrap().is_burnt(),
"Prize must be burnt if there is no winner"
);
eprintln!("No winner this time, the prize was burnt: {}", prize);
},
}
}

Comments are disabled for this gist.