Skip to content

Stealth overview

Stealth transfers move value confidentially: amounts are hidden in Pedersen commitments and each output carries an encrypted payload that only the recipient — who holds the matching view-only key — can scan and unblind.

All stealth crypto is hidden behind the StealthCryptoProvider interface. The default implementation, WasmStealthCrypto, calls @tari-project/ootle-wasm; tests can inject a fake. You normally never construct it yourself — the builder and authorizer default to it.

  1. Build. Compose the transfer via StealthTransfer (or generateOutputsStatement for receive-only flows).
  2. Prepare. Resolve substate versions, emit the on-chain StealthTransfer instruction, produce a StealthTransferSpec.
  3. Authorize / sign. WalletStealthAuthorizer.fromSpec hydrates the balance proof and signs each spent stealth input with its one-time spend key (view secret needed only when spending).
  4. Seal + watch. Submit the sealed envelope and watch for finality.
import { StealthTransfer, WalletStealthAuthorizer, WasmStealthCrypto } from "@tari-project/ootle";
// 1. build — assemble the transfer via the SDK builder (or
// generateOutputsStatement for faucet-only flows).
const transfer = new StealthTransfer(provider, TARI_RESOURCE, new WasmStealthCrypto(NETWORK))
.spendRevealedInput(account, 4n * TARI)
.toStealthOutput({ destination: recipient, amount: 1n * TARI, resourceAddress: TARI_RESOURCE })
.toRevealedOutput(2n * TARI)
.payFeeFromRevealed(1n * TARI);
// 2. prepare — emits the on-chain StealthTransfer instruction, resolves
// substate versions, and produces the StealthTransferSpec.
const spec = await transfer.prepare();
// 3. authorize/sign — hydrates the balance proof (decrypting any stealth
// inputs with the view secret) and signs each spent stealth input with
// its one-time spend key.
const authorizer = WalletStealthAuthorizer.fromSpec(wallet, spec, { viewSecret });
const tx = await authorizer.prepare(provider);
// 4. seal + watch — submit the sealed envelope and watch for finality.
const envelope = await tx.seal();
const txId = await submitTransaction(provider, envelope);
const outcome = await watchTransaction(provider, txId);
PageWhat it coversRunnable example
ReceivingdecryptOwnedUtxo, scanning for owned UTXOs.examples/node/src/stealth/faucet-deposit.ts
SendingStealthTransfer builder, revealed vs stealth in/out.examples/node/src/stealth/stealth-to-revealed.ts + stealth-to-stealth.ts
SpendingSpending a stealth UTXO end-to-end.examples/node/src/stealth/spend-stealth-utxo.ts

Prefer a browser-side demo? The stealth-wallet React app walks through setup → receive → send in three cards.

SymbolRole
StealthTransferFluent builder for a confidential transfer (spendRevealedInput / spendStealthInput / toStealthOutput / toRevealedOutput / payFeeFromRevealed / prepare)
WalletStealthAuthorizerHydrates the balance proof, signs, and seals a StealthTransferSpec
createOutputConstruct an Output with conventional defaults (payTo = { StealthPublicKey: {} }, minimumValuePromise = 0n)
decryptOwnedUtxoReceive-side owner read: decrypt a fetched UTXO, or null if not owned
decryptInputDataLower-level decrypt of raw commitment + ciphertext bytes
generateOutputsStatementOne-shot complete transfer statement (no stealth inputs — e.g. a faucet)
parseSubstateUtxo / commitmentOf / stealthUtxoSubstateIdProvider-agnostic substate / id helpers
WasmStealthCrypto / StealthCryptoProviderThe crypto seam (real WASM impl + the interface)
Mask, EncryptedData, StealthInput, BalanceProofSignature, StealthInputsStatement, StealthOutputsStatement, StealthTransferStatementDomain types

SecretKeyWallet holds an optional view-only key, required for scanning and spending stealth outputs:

import { SecretKeyWallet } from "@tari-project/ootle-secret-key-wallet";
const wallet = SecretKeyWallet.randomWithViewKey(Network.Esmeralda);
const viewSecret = wallet.getViewOnlySecret(); // Uint8Array | null

For ready-to-run end-to-end examples, see examples/node/src/stealth/ — the canonical reference for the Node-side stealth flow.