1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
// Copyright 2026 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
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::Transaction;
#[tokio::main]
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_1em0npr8f3uzs3fznygglr4eujwl0qgr6e30hu3whr9fpfh2w99r743uxd7qg6f7jchgm6pdht7lpcwcggcjgfejfpd4jgvfmzve9vec5xep5t" );
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();
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 so that the transfer below spends two stealth inputs. One of them
// seals that transaction and the other must attach 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).
const FEE_BUDGET: u64 = 20_000;
const FIRST_INPUT_AMOUNT: u64 = 6 * TARI;
const SECOND_INPUT_AMOUNT: u64 = 10 * TARI + FEE_BUDGET - FIRST_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!(FIRST_INPUT_AMOUNT))
)
.to_stealth_output(
Output::new(sender_address.clone(), tari_token, const_nonzero_u64!(SECOND_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)
.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
// This builder creates a stealth transfer statement (spend proof). This is added to the transaction later.
let (transfer, required_signers) = StealthTransfer::new(tari_token, &provider)
// Spend both existing stealth inputs that are controlled by the sender address.
// Together these are worth 10 TARI plus the second transaction's fee budget.
.spend_stealth_input(sender_address.clone(), inputs_to_spend[0].commitment())
.spend_stealth_input(sender_address.clone(), inputs_to_spend[1].commitment())
// The transfer will output the fee budget as revealed funds to pay for the fee.
.to_revealed_output(FEE_BUDGET)
// Spend to a new output (8 TARI) that we'll generate for the recipient address.
.to_stealth_output(
Output::new(recipient, tari_token, const_nonzero_u64!(8 * TARI))
// 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 some change (2 TARI) back to ourselves (NOTE once this example exits, we'll lose the keys for this output!)
.to_stealth_output(Output::new(sender_address, tari_token, const_nonzero_u64!(2*TARI)))
// Load the inputs from the provider to build the transfer statement. NOTE: this will error if the total input amounts != total output amounts.
.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())
.with_fee_instructions_builder(|builder| {
builder
.stealth_transfer(tari_token, transfer)
.put_last_instruction_output_on_workspace("fees")
.pay_fee_from_bucket("fees")
})
// 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();
// This authorizer adds the required (stealth) signatures to spend inputs
let authorizer = provider.wallet().stealth_authorizer(required_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.logs.is_empty() {
println!("\n🪵 Logs:");
for log in receipt.logs() {
println!(" - {}", log);
}
}
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
}