Playing the Guessing Game
Where we are
Section titled “Where we are”So far in this series we have:
- Built a guessing game template — a Rust crate that compiles to WebAssembly and defines the on-chain logic for the game. See Building a Guessing Game Template.
- Published that template to the Tari Ootle network, which gave us a
TemplateAddresswe can use to instantiate the game. See Publishing Templates.
If you haven’t completed those steps yet, start there before continuing.
What we’ll cover
Section titled “What we’ll cover”In this guide we will write a Rust program that interacts with the published guessing game template using
ootle-rs — the Rust client library for the Tari Ootle network. We will cover:
- Initializing a wallet and connecting to the network via the indexer.
- Building and submitting a transaction to deploy a new game component.
- Submitting transactions to start a round, submit player guesses, and end the game.
- Reading the transaction receipt and events to find out who won.
We won’t build a full CLI application here — instead we’ll focus on the core ootle-rs concepts that apply
to any template you write. A complete, runnable CLI example is linked at the bottom of this page.
Dependencies
Section titled “Dependencies”Add ootle-rs to your Cargo.toml:
[dependencies]# TODO: when ootle-rs is published to crates.io, change this to `ootle-rs = "x.y.z"`ootle-rs = { git = "https://github.com/tari-project/tari-ootle.git", branch = "development" }tari_ootle_transaction = { git = "https://github.com/tari-project/tari-ootle.git", branch = "development" }tari_ootle_common_types = { git = "https://github.com/tari-project/tari-ootle.git", branch = "development" }tari_template_lib_types = { git = "https://github.com/tari-project/tari-ootle.git", branch = "development" }ootle-rs— wallet, provider, and signing utilities.tari_ootle_transaction—TransactionBuilderand theargs!macro for calling template methods.tari_template_lib_types— address and type definitions shared between templates and clients.
Initializing a wallet and provider
Section titled “Initializing a wallet and provider”Every interaction with the network starts with a wallet (holds your secret keys and signs transactions) and a provider (connects to an indexer node and submits transactions).
use ootle_rs::{ key_provider::PrivateKeyProvider, keys::OotleSecretKey, provider::ProviderBuilder, wallet::OotleWallet, default_indexer_url,};use tari_ootle_common_types::Network;
const NETWORK: Network = Network::Esmeralda; // Testnet// Generate a new key pair. In a real app, use a key provider// that stores secrets securely rather than in memory.let secret = PrivateKeyProvider::random(NETWORK);let wallet = OotleWallet::from(secret);
// Connect to an indexer node — this is your entry point to the network.let mut provider = ProviderBuilder::new() .wallet(wallet) // Use the default indexer URL for the network .connect(default_indexer_url(NETWORK)) .await?;OotleSecretKey holds two Ristretto key pairs: one for signing transactions and one for the view key.
OotleSecretKey::random generates fresh keys. To restore an existing wallet, use
OotleSecretKey::new(network, account_sk, view_sk) with keys loaded from secure storage.
The provider is used for everything that follows: resolving inputs, sending transactions, and watching
for results.
Funding an account
Section titled “Funding an account”Before you can pay transaction fees, the account needs funds. On testnet the built-in faucet handles this:
use ootle_rs::{ TransactionRequest, builtin_templates::{UnsignedTransactionBuilder, faucet::IFaucet},};use tari_template_lib_types::constants::ONE_XTR;
// Build a transaction that calls the faucet to deposit 10 tXTR into our account.let unsigned_tx = IFaucet::new(&provider) .take_faucet_funds(10 * ONE_XTR) .pay_fee(500u64) .prepare() .await?;
// Sign and send the transactionlet tx = TransactionRequest::default() .with_transaction(unsigned_tx) .build(provider.wallet()) .await?;
// Send the transactionlet pending = provider.send_transaction(tx).await?;// Wait for the transaction to be finalizedlet outcome = pending.watch().await?;println!("Faucet transaction outcome: {outcome}");IFaucet is a built-in transaction builder that calls the network faucet component and deposits funds into
your account, creating the account on-chain if it doesn’t exist yet.
Create the game component
Section titled “Create the game component”To create a new instance of the guessing game, call the template’s new constructor using call_function:
use tari_template_lib_types::{TemplateAddress, constants::XTR};
// template_addr comes from publishing your template (see Publishing Templates)let unsigned_tx = TransactionBuilder::new(provider.network()) .with_auto_fill_inputs() .pay_fee_from_component(account_addr, 2000u64) // call_function invokes a function (no &self in the func signature) // on the template .call_function(template_addr, "new", args![]) .build_unsigned();After the transaction is confirmed, find the deployed addresses in the receipt:
// The new component addresslet component_addr = receipt.diff_summary.upped .iter() .find_map(|s| s.substate_id.as_component_address()) .expect("component address in receipt");
// The prize resource the constructor created// (every resource except XTR is the game resource)let resource_addr = receipt.diff_summary.upped .iter() .find_map(|s| s.substate_id.as_resource_address().filter(|a| *a != XTR)) .expect("resource address in receipt");diff_summary.upped lists every substate that was created or updated by the transaction.
Starting a round
Section titled “Starting a round”Call a method on the component using call_method. The arguments are serialized by the args! macro and
deserialized by the execution engine on the other side — the same types used in the template.
use tari_template_lib_types::NonFungibleId;
let nft_id = NonFungibleId::from_string("round-1");
let unsigned_tx = TransactionBuilder::new(provider.network()) .with_auto_fill_inputs() .pay_fee_from_component(account_addr, 2000u64) // call_method invokes a method on an existing component .call_method(component_addr, "start_game", args![nft_id]) .build_unsigned();nft_id is passed directly to start_game in the template, which uses it as the identifier for the prize
NFT it mints this round.
Submitting a player guess
Section titled “Submitting a player guess”The guessing game template identifies each player by the signer of the transaction, not by an argument. This means each player must sign their own guess transaction with their own wallet — you cannot submit on their behalf.
// Build a provider for the player using their own walletlet player_wallet = OotleWallet::from(PrivateKeyProvider::new(player_secret));let mut player_provider = ProviderBuilder::new() .wallet(player_wallet) .connect(&indexer_url) .await?;
let player_account_addr = player_provider .default_signer_address() .to_account_address();
let unsigned_tx = TransactionBuilder::new(player_provider.network()) .with_auto_fill_inputs() .pay_fee_from_component(player_account_addr, 2000u64) .call_method(component_addr, "guess", args![number, payout_component_addr]) .build_unsigned();
// Signing with the player's wallet authenticates them in the templatelet tx = TransactionRequest::default() .with_transaction(unsigned_tx) .build(player_provider.wallet()) .await?;
player_provider.send_transaction(tx).await?.watch().await?;Ending the game and reading events
Section titled “Ending the game and reading events”Ending the game requires the vaults of every player who guessed as inputs, because the prize may be
deposited into the winner’s account. Add them manually with .add_input and .with_inputs:
let unsigned_tx = TransactionBuilder::new(provider.network()) .with_auto_fill_inputs() // The prize NFT must be an input so it can be transferred or burned .add_input(prize_nft_address) // Each player's account that participated needs to be an input .with_inputs(player_account_addrs.iter().copied().map(Into::into)) .pay_fee_from_component(account_addr, 2000u64) .call_method(component_addr, "end_game_and_payout", args![]) .build_unsigned();After the transaction is confirmed, the template will have emitted a GuessingGame.GameEnded event. Events
are included in the receipt and can be read by topic name:
let event = receipt.events .iter() .find(|e| e.topic() == "GuessingGame.GameEnded") .expect("GameEnded event in receipt");
match event.get_payload("winner_account") { Some(addr) => println!("Winner: {addr}"), None => println!("No winner this round — the prize was burned."),}
if let Some(number) = event.get_payload("number") { println!("The number was: {number}");}get_payload returns named fields from the event’s payload, matching the field names used when the event
was emitted in the template.
Full working example
Section titled “Full working example”A complete CLI application that implements all of the above — with persistent state, interactive prompts, and support for multiple players — is available here:
Use cargo-generate to download the full crate locally:
cargo generate https://github.com/tari-project/wasm-template examples/guessing_game/cli