ootle-rs 0.20.0

A Rust library for interacting with the Tari Ootle network.
Documentation
//   Copyright 2026 The Tari Project
//   SPDX-License-Identifier: BSD-3-Clause

use std::num::NonZeroU64;

use ootle_rs::{
    ToAccountAddress,
    TransactionRequest,
    address,
    builtin_templates::{UnsignedTransactionBuilder, faucet::IFaucet},
    const_nonzero_u64,
    default_indexer_url,
    displayable::Displayable,
    key_provider::PrivateKeyProvider,
    provider::{PendingTransaction, Provider, ProviderBuilder, WalletProvider},
    stealth::{Output, StealthTransfer},
    template_types::{
        UtxoAddress,
        constants::{TARI, TARI_TOKEN},
    },
    transaction::TransactionSigner,
    wallet::OotleWallet,
};
use tari_ootle_common_types::engine_types::transaction_receipt::TransactionReceipt;
use tari_ootle_transaction::{Epoch, Transaction};

#[tokio::main]
#[allow(clippy::too_many_lines)]
async fn main() {
    // env_logger::builder()
    //     .filter_level(tracing::log::LevelFilter::Debug)
    //     .init();

    // This is the address that we will transfer to (Feel free to change this another address!)
    let recipient = address!( "otl_loc_162dtv4375eg54pn2g7c3tgu7j89e96hes5hvrxac4qxex6g4v3q7fsantdmgrs7mlg3hc9v4kdaktkp5l8t495fmkdvgpyz4whe6qvckjl8v6" );

    let indexer_api_url = default_indexer_url(recipient.network());

    let sender_secret = PrivateKeyProvider::random(recipient.network());
    let sender_address = sender_secret.address().clone();
    println!("Sender address: {sender_address}");
    // Don't print secrets in production code!
    println!(
        "Sender secrets: {} | {}",
        sender_secret.credentials().account_secret().reveal(),
        sender_secret.credentials().view_only_secret().reveal()
    );
    let account_component_addr = sender_address.to_account_address();
    println!("Sender account address: {account_component_addr}");

    let wallet = OotleWallet::from(sender_secret.clone());

    let mut provider = ProviderBuilder::new()
        .wallet(wallet)
        .connect(indexer_api_url)
        .await
        .unwrap();

    // Get the network from the indexer (must be the same as the network specified in the builder).
    let network = provider.get_network().await.unwrap();
    println!("Provider network ID: {network}");
    assert_eq!(network, provider.network());
    // Get the latest block number.
    let latest_epoch = provider.get_epoch().await.unwrap();
    // Every transaction declares the last epoch it may be sequenced in; past it the transaction can
    // never land. Ten epochs is a comfortable window for an example — the network caps how far
    // ahead this may be set.
    let max_epoch = Epoch(provider.get_epoch().await.unwrap().as_u64() + 10);
    println!("Latest epoch: {latest_epoch}");

    // Send some TARI to another address. You can replace TARI with any other fungible token resource address.
    let tari_token = TARI_TOKEN; // resource_address!("resource_0123456789abcdef...");

    // The faucet funds are split across two outputs because the transfer below is split across two statements: a
    // small one in the fee intent that sources the fee, and the sixteen-output one in the main intent that the fee
    // then pays for. Each statement spends its own input. One of the two inputs seals that transaction and the other
    // attaches an authorization signature committing to the seal signer's one-time public key.

    // The revealed amount each transaction reserves to pay its fee. It is deliberately generous rather than fitted to
    // a dry-run estimate: the budget is part of the transfer statement, so spending an estimate would change the
    // transaction it was estimated from. Whatever is not charged is refunded (see the receipt's overcharge line).
    //
    // It also has to fund the *compute allowance* the sixteen-output statement verifies under, not just the fee that
    // statement is charged: the allowance is what the payment buys, at the fee table's point rate.
    const FEE_BUDGET: u64 = 250_000;
    // Spent by the fee-intent statement: the budget it reveals to pay the fee, plus a stealth output so the statement
    // has somewhere to put the remainder.
    const FEE_INPUT_AMOUNT: u64 = FEE_BUDGET + TARI;
    // Spent by the main-intent statement, which fans it out into sixteen outputs.
    const TRANSFER_INPUT_AMOUNT: u64 = 10 * TARI + FEE_BUDGET - FEE_INPUT_AMOUNT;
    // // This builder creates a stealth transfer statement (spend proof). This is added to the transaction later.
    let (faucet_transfer, required_signers) = StealthTransfer::new(tari_token, &provider)
        // Tell the transfer to expect 10 TARI (plus a fee budget for this transaction and the transfer below) as revealed funds from a bucket (the faucet looks at this value and automatically provides the bucket).
        .spend_revealed_input(10 * TARI + 2 * FEE_BUDGET)
        // The transfer will output the fee budget as revealed funds to pay for the fee.
        .to_revealed_output(FEE_BUDGET)
        // Spend the remaining value (10 TARI - fee) into outputs for the sender address. NOTE: the sender address is not actually included in the output (privacy!),
        // but a supporting wallet that holds the secret key would be able to spend the output.
        // You can specify any address here and split up into many outputs as needed, as long as ∑inputs == ∑outputs.
        .to_stealth_output(
            Output::new(sender_address.clone(), tari_token, const_nonzero_u64!(FEE_INPUT_AMOUNT))
        )
        .to_stealth_output(
            Output::new(sender_address.clone(), tari_token, const_nonzero_u64!(TRANSFER_INPUT_AMOUNT))
        )
        .prepare()
        .await
        .unwrap();

    // Keep track of the input commitments to spend later.
    let inputs_to_spend = faucet_transfer.stealth_outputs().to_vec();

    // First let's transfer some faucet TARI to our account to have funds for fees and transfers.
    let unsigned_tx = IFaucet::new(&provider, max_epoch)
        .take_faucet_funds()
        .into_stealth_transfer(faucet_transfer)
        .and_pay_fee_from_revealed_output()
        .prepare()
        .await
        .expect("Failed to prepare faucet transaction");

    // This authorizer adds the required (stealth) signatures to spend inputs
    let authorizer = provider.wallet().stealth_authorizer(required_signers);

    let transaction = TransactionRequest::default()
        .with_transaction(unsigned_tx)
        .build(&authorizer)
        .await
        .unwrap();

    let pending_tx = provider.send_transaction(transaction).await.unwrap();
    print_fancy_results("Faucet transfer", &pending_tx).await;

    // Then we'll send it to the recipient, across two statements.
    //
    // The fee intent runs on a fixed credit of compute before anything has been paid — enough to source a fee, and no
    // more. Verifying sixteen outputs costs several times that, so a statement that size cannot live there: it is the
    // main intent that the fee, once paid, buys the compute allowance for. The engine allows the fee intent exactly
    // one transfer statement for this reason, and this is the shape it expects — a small statement that reveals the
    // fee, then the real transfer.

    // The fee-sourcing statement: reveal the budget to pay the fee, and put the remainder in a stealth output so the
    // statement balances. Two outputs verify well inside the pre-payment credit.
    let (fee_transfer, fee_signers) = StealthTransfer::new(tari_token, &provider)
        .spend_stealth_input(sender_address.clone(), inputs_to_spend[0].commitment())
        .to_revealed_output(FEE_BUDGET)
        .to_stealth_output(Output::new(
            sender_address.clone(),
            tari_token,
            const_nonzero_u64!(FEE_INPUT_AMOUNT - FEE_BUDGET),
        ))
        .prepare()
        .await
        .unwrap();

    // The transfer itself. One statement may carry up to 16 stealth outputs, and all of them share a single
    // aggregated range proof. That is what makes fanning out inside one statement cheaper than splitting the same
    // outputs across several transfers, each of which would pay the fixed per-statement cost and need its own change
    // output. Here the input goes to two outputs worth spending plus fourteen dust ones.
    const DUST_OUTPUT_COUNT: u64 = 14;
    // Dust in the literal sense: each of these holds 1 µT while costing ~6,000 µT of range-proof verification to
    // create. Fine for showing the fan-out, ruinous as a spending habit.
    const DUST_AMOUNT: u64 = 1;
    const RECIPIENT_AMOUNT: u64 = 8 * TARI;
    // The change absorbs the dust so that ∑inputs == ∑outputs still holds.
    const CHANGE_AMOUNT: u64 = TRANSFER_INPUT_AMOUNT - RECIPIENT_AMOUNT - DUST_OUTPUT_COUNT * DUST_AMOUNT;

    let transfer_builder = StealthTransfer::new(tari_token, &provider)
        // Spend the other stealth input controlled by the sender address. This statement pays no fee of its own — the
        // fee-sourcing statement above covers the whole transaction.
        .spend_stealth_input(sender_address.clone(), inputs_to_spend[1].commitment())
        // Spend to a new output (8 TARI) that we'll generate for the recipient address.
        .to_stealth_output(
            Output::new(recipient.clone(), tari_token, const_nonzero_u64!(RECIPIENT_AMOUNT))
                // NOTE: this memo is stored on-chain, and longer memos increase fees. It is encrypted so that only the recipient can read it.
                .with_memo_message("transfer from ootle-rs!")
        )
        // Send the change back to ourselves (NOTE once this example exits, we'll lose the keys for this output!)
        .to_stealth_output(Output::new(sender_address.clone(), tari_token, const_nonzero_u64!(CHANGE_AMOUNT)));

    // Fan the rest of the statement out into dust, taking the output count up to the per-statement maximum.
    let transfer_builder = (0..DUST_OUTPUT_COUNT).fold(transfer_builder, |builder, _| {
        builder.to_stealth_output(Output::new(
            recipient.clone(),
            tari_token,
            NonZeroU64::new(DUST_AMOUNT).expect("DUST_AMOUNT is non-zero"),
        ))
    });

    // Load the inputs from the provider to build the transfer statement. NOTE: this will error if the total input
    // amounts != total output amounts.
    let (transfer, transfer_signers) = transfer_builder.prepare().await.unwrap();

    // We'll generate an unsigned transaction directly using the Transaction builder. In future, we may make this
    // easier.
    let unsigned_tx = Transaction::builder(provider.network(), max_epoch)
        .with_fee_instructions_builder(|builder| {
            builder
                .stealth_transfer(tari_token, fee_transfer)
                .put_last_instruction_output_on_workspace("fees")
                .pay_fee_from_bucket("fees")
        })
        // The sixteen-output statement, funded by the fee the instructions above just paid. It reveals nothing, so it
        // leaves no bucket behind to account for.
        .stealth_transfer(tari_token, transfer)
        // This isn't necessary because all transactions implicitly use TARI for fees, but you'd need to include this if other resources are being used
        .add_input(tari_token)
        // Add the UTXO substates as inputs. These will be DOWNed (destroyed) if the transaction is successful.
        .add_input(UtxoAddress::new(tari_token, inputs_to_spend[0].commitment().into()))
        .add_input(UtxoAddress::new(tari_token, inputs_to_spend[1].commitment().into()))
        .build_unsigned();

    // Both statements are spent by one transaction, which has a single seal: merging their requirements settles which
    // input seals it and leaves the other to authorize against that seal signer's one-time public key.
    let authorizer = provider
        .wallet()
        .stealth_authorizer(fee_signers.merge(transfer_signers));

    let result = provider
        .sign_and_send_dry_run_with(&authorizer, unsigned_tx.clone())
        .await
        .unwrap();
    let _diff = result.expect_success();
    println!("Dry run successful!");
    // Informational: the fee budget above is not derived from this number. Spending the estimate would mean changing
    // the transfer statement, which would change the transaction the estimate was taken from.
    println!(
        "Estimated fees for transfer: {}",
        result.finalize.fee_receipt.total_fees_charged()
    );
    // One of the two stealth inputs seals the transaction; `build` asks the authorizer for the authorization signature
    // the other one needs, which commits to the seal signer's one-time public key.
    let transaction = TransactionRequest::default()
        .with_transaction(unsigned_tx)
        .build(&authorizer)
        .await
        .unwrap();

    let pending_tx = provider.send_transaction(transaction).await.unwrap();
    print_fancy_results("Stealth transfer", &pending_tx).await;
}

async fn print_fancy_results(label: &str, pending_tx: &PendingTransaction) -> TransactionReceipt {
    println!("⌛️ {label} transaction pending... {}", pending_tx.tx_id());
    let outcome = pending_tx.watch().await.unwrap();
    println!("🏁 Transaction Finalized {}", pending_tx.tx_id());

    println!("✅ Outcome: {:?}", outcome);

    // Wait for the transaction to be finalized and get the receipt.
    let receipt = pending_tx.get_receipt().await.unwrap();
    println!("-------------------------------------------");
    println!("  Transaction Receipt");
    println!("-------------------------------------------");
    println!("🔹 Epoch: {}", receipt.epoch);
    println!("🔹 Transaction ID: {}", pending_tx.tx_id());
    println!("🔹 Outcome: {:?}", receipt.outcome);
    let fee_receipt = &receipt.fee_receipt;
    println!("🔹 Fees Paid: {}", fee_receipt.total_fees_paid());
    println!(
        "🔹 Fees Overcharge: {} = {} (paid) - {} (charged) - {} (refunded)",
        fee_receipt.total_fee_overcharge(),
        fee_receipt.total_fees_paid(),
        fee_receipt.total_fees_charged(),
        fee_receipt.total_refunded()
    );

    if !receipt.events.is_empty() {
        println!("\n🎉 Events:");
        for event in receipt.events() {
            println!("  - Substate ID: {}", event.substate_id().display());
            println!("    Template Address: {}", event.template_address());
            println!("    Topic: {}", event.topic());
            println!("    Payload: {{{}}}", event.payload());
            println!();
        }
    }
    println!("-------------------------------------------");
    receipt
}