Skip to content

Transaction Overview

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 TransactionBuilder to construct transactions.
  • See real examples of calling template functions, invoking component methods, and passing values between instructions.

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.

An instruction is the smallest unit of work in a transaction. The Ootle engine supports the following instructions:

InstructionWhat 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
DropAllProofsInWorkspaceDrops 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 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 Bucket
Instruction 2: put result on workspace as <payment>
Instruction 3: call component.deposit(Workspace(<payment>)) → consumes bucket

The 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 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.

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);

Once you have chained all your instructions, call one of the following to produce a final transaction:

MethodDescription
.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)

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


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);

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])

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.


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")

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:

MethodDescription
assert_bucket_contains_anyBucket has more than zero tokens
assert_bucket_contains_at_leastBucket has at least N tokens
assert_bucket_contains_exactlyBucket has exactly N tokens
assert_bucket_contains_at_mostBucket has at most N tokens
assert_bucket_contains_non_fungibles_allBucket contains all of the specified NFTs
assert_bucket_contains_non_fungibles_anyBucket contains any of the specified NFTs
assert_workspace_item_is_not_nullWorkspace 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:

  • PublishTemplate references its WASM binary as a blob.
  • InstructionArg::Blob(idx) passes the blob’s bytes as a function/method argument — same shape as a Literal argument from the template’s perspective, just carried out-of-band.

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.

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:

ConcernLiteralBlob
Bytes are storedInline in the instructionIn the prunable blob list
Bytes participate in signingYes (full content)Only the 32-byte commitment
Network gossip costPer-instructionOnce per transaction, irrespective of references
Reviewable on hardware signersYes (if small enough to display)Only the hash is shown; contents are opaque
Indexed reuse in same txNo (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.

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);

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 template
let 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);

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.