Skip to content

Playing the Guessing Game

So far in this series we have:

  1. 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.
  2. Published that template to the Tari Ootle network, which gave us a TemplateAddress we can use to instantiate the game. See Publishing Templates.

If you haven’t completed those steps yet, start there before continuing.

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.


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_transactionTransactionBuilder and the args! macro for calling template methods.
  • tari_template_lib_types — address and type definitions shared between templates and clients.

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.


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 transaction
let tx = TransactionRequest::default()
.with_transaction(unsigned_tx)
.build(provider.wallet())
.await?;
// Send the transaction
let pending = provider.send_transaction(tx).await?;
// Wait for the transaction to be finalized
let 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.


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 address
let 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.


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.


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 wallet
let 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 template
let tx = TransactionRequest::default()
.with_transaction(unsigned_tx)
.build(player_provider.wallet())
.await?;
player_provider.send_transaction(tx).await?.watch().await?;

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.


A complete CLI application that implements all of the above — with persistent state, interactive prompts, and support for multiple players — is available here:

Guessing Game CLI on GitHub

Use cargo-generate to download the full crate locally:

Terminal window
cargo generate https://github.com/tari-project/wasm-template examples/guessing_game/cli