Transaction Overview
Covered in this guide
Section titled “Covered in this guide”In this guide, you will:
- Understand what a Tari Ootle transaction is and how it works.
- Learn about instructions, the workspace, and fees.
- Learn how to use the
TransactionBuilderto construct transactions. - See real examples of calling template functions, invoking component methods, and passing values between instructions.
What is a Transaction?
Section titled “What is a Transaction?”A Tari Ootle transaction is an ordered sequence of instructions that the network executes atomically. Like a database transaction, either all instructions succeed and their state changes are committed on-chain, or all fail and nothing changes. There is no partial execution.
You typically construct a transaction to:
- Invoke functions on a template for e.g. to create a new component.
- Invoke a method on an existing component.
- Transfer tokens between accounts or components.
- Chain multiple of the above together in a single atomic operation.
Instructions
Section titled “Instructions”An instruction is the smallest unit of work in a transaction. The Ootle engine supports the following instructions:
| Instruction | What it does |
|---|---|
CallFunction(address, function_name, args) | Calls a function (e.g. a constructor) on a deployed template |
CallMethod(component, method_name, args) | Calls a method on an existing component |
PutLastInstructionOutputOnWorkspace(key) | Saves the return value of the previous instruction to the workspace under a label |
TakeFromBucket(input, amount, output) | Splits a bucket, writing a new bucket with the given amount back to the workspace |
CreateAccount(owner_public_key, owner_rule?, access_rules?, bucket?) | Creates a built-in account component for a given public key (idempotent) |
Assert(key, assertion) | Verifies a condition about a workspace item, failing the transaction if not met |
AllocateAddress(type, workspace_id) | Pre-allocates a component or resource address, placing it on the workspace |
PublishTemplate(blob_index) | Publishes a new WASM template referenced from the transaction’s blob list |
DropAllProofsInWorkspace | Drops all proofs currently held in the workspace |
ClaimBurn(claim_proof, output_data) | Claims a burn proof from the Minotari layer-1 chain |
ClaimValidatorFees(address) | Claims accumulated validator fees from a fee pool |
The Workspace
Section titled “The Workspace”The workspace is a temporary scratchpad that exists only for the duration of a transaction. It allows you to pass values — such as buckets, addresses, or component references — from one instruction to the next.
When an instruction returns a value (for example, a Bucket from calling withdraw on an account),
you use the PutLastInstructionOutputOnWorkspace instruction to save it under a label (called a WorkspaceId).
Subsequent instructions can then reference it using Workspace("label") in the args![] macro.
Instruction 1: call account.withdraw(resource, 100) → returns BucketInstruction 2: put result on workspace as <payment>Instruction 3: call component.deposit(Workspace(<payment>)) → consumes bucketThe workspace is not persisted — it disappears when the transaction finishes.
Every transaction must pay fees to validators. Fee instructions are a separate sequence that runs before the main instructions. They lock up a maximum amount of tTARI (the native token) from a specified account or bucket.
After execution, any unused portion of the locked fees is refunded. If the main instructions fail, a partial fee is still charged for the work done by validators up to that point.
The TransactionBuilder
Section titled “The TransactionBuilder”The TransactionBuilder provides a fluent API for constructing transactions.
You start with Transaction::builder(network) (or the shorthand Transaction::builder_localnet() for local testing),
chain instruction methods, and finally sign and seal the transaction with a private key.
The builder is available in the tari_ootle_transaction crate.
Basic usage
Section titled “Basic usage”use tari_ootle_transaction::{Transaction, args};
let transaction = Transaction::builder(Network::Esmeralda) // Specify how fees will be paid .pay_fee_from_component(my_account_address, 1000u64) // Add your instructions here .call_function(my_template_address, "new", args![]) // Sign and seal the transaction with your private key .finish() .seal(&secret_key);Signing and building
Section titled “Signing and building”Once you have chained all your instructions, call one of the following to produce a final transaction:
| Method | Description |
|---|---|
.build_and_seal(&secret_key) | Signs and seals the transaction, returning a Transaction ready to submit |
.build_unsigned() | Returns an UnsignedTransaction (useful for multi-sig flows) |
.finish() | Returns an UnsealedTransaction (has 0 or more signers, needs to be sealed) |
Calling a Template Function
Section titled “Calling a Template Function”To call a function (such as a constructor) on a deployed template, use .call_function().
use tari_ootle_transaction::{Transaction, args};use tari_template_lib::types::TemplateAddress;
// The address of the deployed template you want to calllet template_address: TemplateAddress = /* ... */;
let transaction = Transaction::builder(network) .pay_fee_from_component(my_account, 1000u64) // Call the "new" function on the template with two arguments. // `fn new(name: String, initial_value: u64) -> Component<Self>` .call_function(template_address, "new", args!["Ootle", 42u64]) .build_and_seal(&secret_key);Any type that implements serde::Serialize can be passed as an argument, including addresses, amounts, strings, structs, and more.
Calling a Component Method
Section titled “Calling a Component Method”To call a method on an existing component, use .call_method(). You’ll need the component’s ComponentAddress.
use tari_ootle_transaction::{Transaction, args};use tari_template_lib::types::ComponentAddress;
let component_address: ComponentAddress = /* ... */;
let transaction = Transaction::builder(network) .pay_fee_from_component(my_account, 1000u64) // Call the "set_value" method with a u64 argument .call_method(component_address, "set_value", args![99u64]) .build_and_seal(&secret_key);Passing Values Between Instructions (The Workspace)
Section titled “Passing Values Between Instructions (The Workspace)”A common pattern is to withdraw tokens from one place and deposit them in another, all in a single atomic transaction. Use put_last_instruction_output_on_workspace to capture the return value of an instruction, then reference it in the next with Workspace("label").
use tari_ootle_transaction::{Transaction, args};
let transaction = Transaction::builder(network) .pay_fee_from_component(sender_account, Amount::from(1000)) // Withdraw 100 tokens of `my_resource` into a Bucket // from the sender's account .call_method(sender_account, "withdraw", args![my_resource, 100u64]) // Save the returned Bucket to the workspace as "payment" .put_last_instruction_output_on_workspace("payment") // Deposit the bucket into the receiver's account .call_method(receiver_account, "deposit", args![Workspace("payment")]) .build_and_seal(&secret_key);Splitting a Bucket
Section titled “Splitting a Bucket”You can take a portion from a bucket using .take_from_bucket(). This creates a new bucket with the specified amount, leaving the remainder in the original.
.call_method(account, "withdraw", args![my_resource, 1000u64]).put_last_instruction_output_on_workspace("big_bucket")// Split off 250 into a new bucket called "payment".take_from_bucket("big_bucket", 250u64, "payment")// Deposit "payment" into one component.call_method(component_a, "deposit", args![Workspace("payment")])// Deposit the rest ("big_bucket") into another.call_method(component_b, "deposit", args![Workspace("big_bucket")])Calling a Component Created in the Same Transaction
Section titled “Calling a Component Created in the Same Transaction”Sometimes you want to create a component and immediately call a method on it — before the transaction is committed. You can reference a workspace item as the target of a call_method call by passing a workspace label string directly as the component:
.call_function(template_address, "new", args![]).put_last_instruction_output_on_workspace("my_new_component")// Call a method on the component we just created (it's still in the workspace).call_method("my_new_component", "initialize", args![42u64])Pre-allocating Addresses
Section titled “Pre-allocating Addresses”Sometimes you need to know an address before it is created — for example, to pass a component’s own address to its constructor, or to set up cross-references between two components. Use allocate_component_address to pre-allocate an address and put it on the workspace, then pass it to call_function:
use tari_ootle_transaction::{ Transaction, args, builder::named_component_call::CallFromWorkspace,};
let transaction = Transaction::builder(network) .pay_fee_from_component(account, Amount::from(1000)) // Pre-allocate an address for our new component .allocate_component_address("my_addr") // Pass the pre-allocated address to the constructor .call_function( template_address, "new_with_address", args![Workspace("my_addr")], ) .build_and_seal(&secret_key);Similarly, use allocate_resource_address to pre-allocate a resource address.
Creating Accounts
Section titled “Creating Accounts”The create_account method adds a CreateAccount instruction, which creates a built-in account component for the given public key.
This is idempotent — if the account already exists, it is a no-op.
use tari_template_lib::types::crypto::RistrettoPublicKeyBytes;
let owner_public_key: RistrettoPublicKeyBytes = /* ... */;
let transaction = Transaction::builder(network) .create_account(owner_public_key) .build_and_seal(&secret_key);You can also create an account and immediately deposit a bucket into it. If the component doesn’t exist, it will be created. If it does exist, the bucket will simply be deposited.
.call_method(faucet, "take_free_coins", args![]).put_last_instruction_output_on_workspace("coins")// Create the account and deposit the bucket from the workspace.create_account_with_bucket(owner_public_key, "coins")Assertions
Section titled “Assertions”You can add assertions to a transaction to verify conditions about workspace items. If an assertion fails, the entire transaction is rejected.
use tari_template_lib::types::ResourceAddress;
let resource: ResourceAddress = /* ... */;
let transaction = Transaction::builder(network) .call_method(account, "withdraw", args![resource, 100u64]) .put_last_instruction_output_on_workspace("bucket") // Assert the bucket contains exactly 100 tokens of `resource` .assert_bucket_contains_exactly("bucket", resource, 100u64) .call_method(other_account, "deposit", args![Workspace("bucket")]) .build_and_seal(&secret_key);Available assertion methods:
| Method | Description |
|---|---|
assert_bucket_contains_any | Bucket has more than zero tokens |
assert_bucket_contains_at_least | Bucket has at least N tokens |
assert_bucket_contains_exactly | Bucket has exactly N tokens |
assert_bucket_contains_at_most | Bucket has at most N tokens |
assert_bucket_contains_non_fungibles_all | Bucket contains all of the specified NFTs |
assert_bucket_contains_non_fungibles_any | Bucket contains any of the specified NFTs |
assert_workspace_item_is_not_null | Workspace item is non-null |
A blob is an opaque byte payload carried alongside the transaction’s instructions. Blobs are
referenced from instructions and arguments by a small BlobIndex (u8), and only their per-blob
hashes — not the raw bytes — participate in the transaction’s signing domain and id. This means
blob payloads can be pruned by storage layers after a transaction has finalised without affecting
signature verifiability or the transaction id.
Two places use blobs today:
PublishTemplatereferences its WASM binary as a blob.InstructionArg::Blob(idx)passes the blob’s bytes as a function/method argument — same shape as aLiteralargument from the template’s perspective, just carried out-of-band.
Adding and referencing blobs
Section titled “Adding and referencing blobs”The TransactionBuilder mirrors the workspace-item naming pattern. Register a blob under a
caller-supplied name, then reference it by name in args![Blob(name)] or via the
*_from_blob(name) methods.
let transaction = Transaction::builder(network) .pay_fee_from_component(account, Amount::from(1000)) // Register a blob under the name "wasm". .add_blob("wasm", template_binary) // Reference it from PublishTemplate. .publish_template_from_blob("wasm") // Or pass the same bytes as a method arg on a different call. .call_method(other_component, "consume_payload", args![Blob("wasm")]) .build_and_seal(&secret_key);For the common one-shot case where you only want to publish a template and never read the same
bytes again, .publish_template(binary) registers an unnamed blob in one step:
let transaction = Transaction::builder(network) .pay_fee_from_component(account, Amount::from(1000)) .publish_template(template_binary) // auto-creates an unnamed blob .build_and_seal(&secret_key);add_blob panics if the transaction already has 256 blobs (the BlobIndex cap). Use
add_blob_checked if you need the fallible variant.
Blob vs. Literal: which to use
Section titled “Blob vs. Literal: which to use”Both Literal and Blob arguments arrive at the template as the same decoded value — there is
no behavioural difference at the template boundary. The difference is in transaction structure:
| Concern | Literal | Blob |
|---|---|---|
| Bytes are stored | Inline in the instruction | In the prunable blob list |
| Bytes participate in signing | Yes (full content) | Only the 32-byte commitment |
| Network gossip cost | Per-instruction | Once per transaction, irrespective of references |
| Reviewable on hardware signers | Yes (if small enough to display) | Only the hash is shown; contents are opaque |
| Indexed reuse in same tx | No (each occurrence repeats the data) | Yes (multiple Blob(name) refs share one payload) |
Rule of thumb: prefer Literal for small values, especially anything a user might want to
review on a hardware signer. Reach for Blob when the payload is large (template binaries,
big CBOR structs) or when it’s referenced more than once in the same transaction.
Blobs in the manifest DSL
Section titled “Blobs in the manifest DSL”Manifest text can reference blobs via the blob!(name) macro. The actual bytes are supplied
out-of-band when you submit the manifest (e.g. as the blobs map on the wallet daemon’s
submit_manifest JSON-RPC request):
use template_<address> as MyTemplate;
fn main() { let comp = MyTemplate::new(blob!(payload)); comp.update(blob!("payload")); // string-literal name form is also accepted}Repeated references to the same name reuse the same BlobIndex — you don’t need to add the
same payload multiple times.
To pay fees, use .pay_fee_from_component(). This calls the pay_fee method on a component (the built-in account supports this), locking up a maximum amount and refunding the remainder after execution.
let transaction = Transaction::builder(network) // Lock up at most 1000 TTARI for fees; refund the rest .pay_fee_from_component(my_account, Amount::from(1000)) .call_function(template_address, "new", args![]) .build_and_seal(&secret_key);For more advanced fee scenarios (e.g., funding fees from a faucet before paying), use with_fee_instructions_builder:
let transaction = Transaction::builder(network) .with_fee_instructions_builder(|builder| { builder // Obtain coins from a faucet .call_method(faucet, "take", args![100_000u64]) .put_last_instruction_output_on_workspace("faucet_coins") // Deposit into account .call_method( my_account, "deposit", args![Workspace("faucet_coins")], ) // Then pay the fee from the account .call_method(my_account, "pay_fee", args![Amount::from(1000)]) }) .call_function(template_address, "new", args![]) .build_and_seal(&secret_key);Putting it All Together
Section titled “Putting it All Together”Here is a complete example that creates a component from a template and immediately calls a method on it, all in a single atomic transaction:
use tari_ootle_transaction::{Transaction, args};use tari_template_lib::types::{Amount, TemplateAddress};
// the address of your deployed templatelet template_address: TemplateAddress = /* ... */;let my_account: ComponentAddress = /* your account address */;
let transaction = Transaction::builder(network) // Pay fees (refunded if unused) .pay_fee_from_component(my_account, Amount::from(2000)) // Call the template constructor to create a new component .call_function(template_address, "new", args!["Hello, Ootle!"]) // Save the new component address to the workspace .put_last_instruction_output_on_workspace("my_component") // Immediately call a method on the freshly created component .call_method("my_component", "greet", args![]) // Sign and seal the transaction .build_and_seal(&secret_key);Testing Transactions
Section titled “Testing Transactions”When writing tests for your template, the tari_template_test_tooling crate provides a TemplateTest harness that lets you execute transactions against a local in-process engine. This is the same approach used throughout the engine’s own test suite.
use tari_template_test_tooling::TemplateTest;use tari_ootle_transaction::{Transaction, args};
#[test]fn test_my_template() { let mut test = TemplateTest::new( env!("CARGO_MANIFEST_DIR"), ["path/to/my_template"], );
// You can call functions directly via the helper methods... let component_address: ComponentAddress = test.call_function("MyTemplate", "new", call_args![], vec![]);
// ...or build and execute a full transaction let result = test.build_and_execute( test.transaction() .call_method(component_address, "do_something", args![42u64]) .build_and_seal(&test.secret_key()), vec![test.owner_proof()], ).unwrap_success();}See the Building a Guessing Game guide for a worked example of testing a complete template.
Next Steps
Section titled “Next Steps”- Implement an app that you can use to play the guessing game template.
- Build a Guessing Game template and write tests that use the transaction builder.
- Publish your template to the Tari Ootle network.