@tari-project/ootle.ts
v0.14.6
Published
TypeScript SDK for building on the Tari Ootle network
Readme
ootle.ts
TypeScript SDK for building on the Tari Ootle network.
ootle.ts is a modular, strongly-typed SDK that lets you connect to wallets, query chain state, build transactions, and submit them to the Tari Ootle network — all from TypeScript or JavaScript.
Packages
The SDK is split into four focused packages — each will be published to npm under the @tari-project scope. Install only what you need.
| Package | Description |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| @tari-project/ootle | Core interfaces, transaction builder, and flow helpers |
| @tari-project/ootle-indexer | Indexer REST provider — read chain state and submit transactions |
| @tari-project/ootle-secret-key-wallet | Local in-memory signer backed by WASM crypto |
| @tari-project/ootle-wallet-daemon-signer | Remote signer — delegates signing to a running wallet daemon |
Cryptographic operations (key generation, Schnorr signing, BOR encoding) are provided by @tari-project/ootle-wasm, an external dependency used internally by ootle and ootle-secret-key-wallet.
Runtime support
Every package in this repo ships one universal artifact that runs in both browsers (via a bundler such as Vite) and Node ≥ 22 (via tsx or plain node).
| Package | Browser | Node ≥ 22 | Notes |
| ------------------------------------------ | ------- | -------------------- | ----------------------------------------------------- |
| @tari-project/ootle | ✓ | ✓ | Core; WASM crypto via @tari-project/ootle-wasm |
| @tari-project/ootle-indexer | ✓ | ✓ | fetch + SSE native in both |
| @tari-project/ootle-secret-key-wallet | ✓ | ✓ | Stealth scan/spend needs randomWithViewKey(network) |
| @tari-project/ootle-wallet-daemon-signer | ✓ | ✓ (with authToken) | WebAuthn passkeys are browser-only |
Node note: Node ≥ 22 currently requires
NODE_OPTIONS=--experimental-wasm-moduleswhen running undertsxor plainnode(the WASM ESM gating in Node will be lifted in a future release). Seeexamples/node/README.mdfor the rationale and forward plan; every script inexamples/node/wires the flag into itspnpminvocation so most users never set it manually.
Choose your path
Pick a row that matches what you want to build first.
| I want to… | Use | Start at |
| -------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Build a web dApp | ootle + ootle-indexer + daemon signer | Quick start — browser |
| Script/automate from Node | ootle + ootle-indexer + secret-key wallet | Quick start — Node |
| Just read chain state | ootle-indexer | indexer-explorer example |
| Send confidential payments | ootle stealth API | Stealth overview |
Choose your track Browser / dApp: Quick start — browser · Node script / server: Quick start — Node
Quick start
1. Connect to the Esmeralda testnet and read a substate
import { ProviderBuilder, Network } from "@tari-project/ootle-indexer";
const provider = await ProviderBuilder.new().withNetwork(Network.Esmeralda).connect(); // uses the default public indexer URL
const substate = await provider.getSubstate("component_0x…");
console.log(substate);2. Build and submit a transaction (wallet daemon)
import { TransactionBuilder, sendTransaction, Network } from "@tari-project/ootle";
import { ProviderBuilder } from "@tari-project/ootle-indexer";
import { WalletDaemonSigner } from "@tari-project/ootle-wallet-daemon-signer";
const provider = await ProviderBuilder.new().withNetwork(Network.LocalNet).connect();
const signer = await WalletDaemonSigner.connect({ url: "http://localhost:18103", authToken: "…" });
const unsignedTx = TransactionBuilder.new(Network.LocalNet)
.feeTransactionPayFromComponent(await signer.getAddress(), 1000n)
.callMethod({ componentAddress: accountAddress, methodName: "withdraw" }, [
{ Literal: resourceAddress },
{ Literal: "500" },
])
.saveVar("bucket")
.callMethod({ componentAddress: recipientAddress, methodName: "deposit" }, [{ Workspace: "bucket" }])
.buildUnsignedTransaction();
const result = await sendTransaction(provider, signer, unsignedTx);3. Local signing (testing / scripting)
import { SecretKeyWallet } from "@tari-project/ootle-secret-key-wallet";
// Generate a fresh wallet with a view-only key (for stealth output scanning)
const wallet = SecretKeyWallet.randomWithViewKey(Network.Esmeralda);
// Or restore from an existing key (Uint8Array)
const wallet = SecretKeyWallet.fromSecretKey(ownerSecretKey, Network.Esmeralda);4. 5-minute Node quickstart
The browser-flavoured blocks above use a wallet daemon. From a headless Node script the canonical signer is SecretKeyWallet — no daemon, no React. Save the following as transfer.ts and run it against a LocalNet:
import {
AccountInvokeBuilder,
FaucetInvokeBuilder,
Network,
TARI_RESOURCE_ADDRESS,
XTR_FAUCET_COMPONENT_ADDRESS,
defaultIndexerUrl,
sendTransaction,
} from "@tari-project/ootle";
import { IndexerProvider } from "@tari-project/ootle-indexer";
import { EphemeralKeySigner, SecretKeyWallet } from "@tari-project/ootle-secret-key-wallet";
const url = process.env.OOTLE_INDEXER_URL ?? defaultIndexerUrl(Network.LocalNet);
const provider = await IndexerProvider.connect({ url, network: Network.LocalNet });
const sender = SecretKeyWallet.randomWithViewKey(Network.LocalNet);
const recipient = EphemeralKeySigner.generate(Network.LocalNet);
const faucetTx = new FaucetInvokeBuilder(Network.LocalNet, XTR_FAUCET_COMPONENT_ADDRESS)
.feeTransactionPayFromComponent(await sender.getAddress(), 1000n)
.takeFaucetFunds(await sender.getAddress(), 10_000_000n)
.build();
await sendTransaction(provider, sender, faucetTx);
const transferTx = new AccountInvokeBuilder(Network.LocalNet, await sender.getAddress())
.feeTransactionPayFromComponent(await sender.getAddress(), 1000n)
.publicTransfer(await sender.getAddress(), TARI_RESOURCE_ADDRESS, 2_000_000n, await recipient.getAddress())
.build();
await sendTransaction(provider, sender, transferTx);
provider.stopWatcher();Run it:
OOTLE_INDEXER_URL=http://localhost:12500 \
NODE_OPTIONS=--experimental-wasm-modules tsx transfer.tsFor the production-grade pattern (multi-signer co-authorisation, dry-run fee estimation, receipt-diff parsing) see examples/node/src/fungible-transfer.ts and the full Quick start — Node guide.
Architecture
ootle.ts uses two core abstractions:
Provider — reads chain state
Implemented by IndexerProvider. Provides:
getSubstate(id)/fetchSubstates(ids)resolveInputs(inputs)— fills in missing versions before signingsubmitTransaction(envelope)getTransactionResult(txId)listRecentTransactions(params)/getTemplateDefinition(address)
Signer — produces signatures
Implemented by SecretKeyWallet, WalletDaemonSigner, and EphemeralKeySigner. Provides:
getAddress()/getPublicKey()signTransaction(unsignedTx)
Transaction flow
unsignedTx
→ resolveTransaction(provider, …) // fill in substate versions
→ signTransaction(signers, …) // generate seal keypair, collect Schnorr signatures
→ sealTransaction(signed) // BOR-encode into a TransactionEnvelope
→ submitTransaction(provider, …) // submit the envelope to the network
→ watchTransaction(provider, txId) // wait for finalizationOr use the sendTransaction / sendDryRun convenience helpers which chain all steps.
Note: WASM crypto operations (hashing, signing, encoding) are handled internally by
signTransaction,sealTransaction, andsendTransaction. You do not need to manage a WASM module or encoder —@tari-project/ootle-wasmis a dependency of the core package.
Package Reference
@tari-project/ootle
Core package. Everything else depends on it.
import {
Network,
TransactionBuilder,
literalArg,
resolveTransaction,
signTransaction,
sealTransaction,
submitTransaction,
watchTransaction,
sendTransaction,
sendDryRun,
classifyOutcome,
OotleWallet,
WalletStealthAuthorizer,
StealthTransfer,
AccountInvokeBuilder,
FaucetInvokeBuilder,
defaultIndexerUrl,
} from "@tari-project/ootle";Network
enum Network {
MainNet = 0x00,
StageNet = 0x01,
NextNet = 0x02,
LocalNet = 0x10,
Igor = 0x24,
Esmeralda = 0x26,
}TransactionBuilder
Fluent builder for UnsignedTransactionV1.
const unsignedTx = TransactionBuilder.new(Network.Esmeralda)
.feeTransactionPayFromComponent(accountAddress, 1000n)
.callFunction({ templateAddress, functionName: "new" }, [literalArg("hello")])
.saveVar("component")
.callMethod({ componentAddress, methodName: "do_something" }, [{ Workspace: "component" }])
.withMinEpoch(10)
.addInput({ substate_id: vaultId, version: 3 })
.buildUnsignedTransaction();Key methods:
| Method | Description |
| --------------------------------------------------------- | ---------------------------------------------- |
| callFunction(func, args) | Call a template function |
| callMethod(method, args) | Call a component method |
| createAccount(ownerPublicKey) | Create a new account component |
| saveVar(name) | Save last output to a named workspace variable |
| feeTransactionPayFromComponent(addr, amount) | Add fee instruction |
| feeTransactionPayFromComponentConfidential(addr, proof) | Confidential fee |
| claimBurn(claim, output_data) | Claim a Minotari burn |
| allocateAddress(type, name) | Pre-allocate an address |
| addInput(req) / withInputs(reqs) | Add substate inputs |
| withMinEpoch(n) / withMaxEpoch(n) | Set epoch bounds |
| buildUnsignedTransaction() | Return the finished UnsignedTransactionV1 |
Transaction flow functions
// Individual steps
const resolved = await resolveTransaction(provider, unsignedTx);
const signed = await signTransaction([signer], resolved); // returns a signed Transaction
const envelope = sealTransaction(signed); // BOR-encode into TransactionEnvelope
const txId = await submitTransaction(provider, envelope); // submit to network
const receipt = await watchTransaction(provider, txId, { timeoutMs: 30_000 });
// All-in-one
const receipt = await sendTransaction(provider, signer, unsignedTx);
// Dry-run (simulates without committing)
const result = await sendDryRun(provider, signer, unsignedTx);
// Inspect the outcome
const outcome = classifyOutcome(receipt.result);
// outcome: { outcome: "Commit" }
// | { outcome: "FeeIntentCommit", reason: string }
// | { outcome: "Reject", reason: string }OotleWallet
Multi-signer wallet that manages multiple key providers — one per address. Useful when a transaction requires authorizations from several components.
import { OotleWallet } from "@tari-project/ootle";
const wallet = new OotleWallet();
wallet.registerKeyProvider(address, secretKeyWallet);
wallet.setDefaultSigner(address);
// Sign on behalf of any registered signer
const auth = await wallet.authorizeTransaction(address, unsignedTx);
// Sign with the default signer
const signatures = await wallet.signTransaction(unsignedTx);Builtin template helpers
Pre-built builders for the standard account and faucet templates.
import { AccountInvokeBuilder, FaucetInvokeBuilder } from "@tari-project/ootle";
// Withdraw from account
const tx = new AccountInvokeBuilder(Network.Esmeralda, accountAddress)
.feeTransactionPayFromComponent(accountAddress, 1000n)
.publicTransfer(accountAddress, resourceAddress, 500n, recipientAddress)
.build();
// Take faucet funds
const tx = new FaucetInvokeBuilder(Network.Esmeralda, faucetAddress)
.feeTransactionPayFromComponent(accountAddress, 1000n)
.takeFaucetFunds(accountAddress, 10_000n)
.build();defaultIndexerUrl(network)
Returns the well-known indexer URL for a network. Currently returns URLs for LocalNet and Esmeralda; throws for others.
import { defaultIndexerUrl, Network } from "@tari-project/ootle";
const url = defaultIndexerUrl(Network.Esmeralda);
// "https://ootle-indexer-a.tari.com"@tari-project/ootle-indexer
Provider implementation backed by the indexer REST API. Wraps @tari-project/indexer-client with the SDK's Provider interface and adds SSE-based transaction watching.
import {
IndexerProvider,
ProviderBuilder,
IndexerClient,
TransactionWatcher,
PendingTransaction,
resolveWantInputs,
} from "@tari-project/ootle-indexer";
import type { WantInput, TransactionEntry, TemplateMetadata } from "@tari-project/ootle-indexer";ProviderBuilder
Fluent factory for IndexerProvider. Falls back to defaultIndexerUrl when no URL is set.
const provider = await ProviderBuilder.new()
.withNetwork(Network.Esmeralda)
.withUrl("http://my-indexer:18300") // optional — defaults to known URL
.withTransactionTimeoutMs(60_000)
.connect();IndexerProvider
// Connect
const provider = await IndexerProvider.connect({ url, network });
// Read chain state
const substate = await provider.getSubstate("component_0x…");
const substates = await provider.fetchSubstates([id1, id2]);
const template = await provider.getTemplateDefinition(templateAddress);
const list = await provider.listRecentTransactions({ limit: 5, last_id: null });
// Submit
const { transaction_id } = await provider.submitTransaction(envelope);
// Watch for finalization via SSE (falls back to polling on timeout)
const outcome = await provider.watchTransactionSSE(transaction_id).watch();
// Full receipt (after watching)
const receipt = await provider.watchTransactionSSE(transaction_id).getReceipt();
// Stop the SSE watcher when done
provider.stopWatcher();TransactionWatcher and PendingTransaction
The TransactionWatcher maintains a persistent SSE connection to the indexer's /events endpoint and routes TransactionFinalized events to registered waiters. It starts lazily on the first watch() call and can be shared across many transactions.
import { TransactionWatcher } from "@tari-project/ootle-indexer";
const watcher = new TransactionWatcher("http://localhost:18300");
watcher.start();
// Submit your transaction, then:
const pending = watcher.watch(txId, client, 32_000);
const outcome = await pending.watch(); // SSE-first, poll fallback
const receipt = await pending.getReceipt(); // raw indexer response
watcher.stop();PendingTransaction.watch() returns a TransactionOutcome and does not throw on FeeIntentCommit or Reject — the caller decides how to handle each outcome.
WantInput and resolveWantInputs
Lazily resolve inputs by querying the indexer rather than supplying exact versions upfront.
import { resolveWantInputs } from "@tari-project/ootle-indexer";
import type { WantInput } from "@tari-project/ootle-indexer";
const wants: WantInput[] = [
{ type: "SpecificSubstate", substateId: "component_0x…" },
{ type: "VaultForResource", resourceAddress: "resource_0x…" },
];
const inputs = await resolveWantInputs(provider.getClient(), wants);
// inputs: SubstateRequirement[] with versions filled in@tari-project/ootle-secret-key-wallet
Local signer that holds secret key material in JavaScript memory and uses @tari-project/ootle-wasm for all cryptographic operations.
Warning: The secret key lives unencrypted in memory. For production use, prefer
WalletDaemonSignerso the key never touches JavaScript.
import { SecretKeyWallet, EphemeralKeySigner } from "@tari-project/ootle-secret-key-wallet";SecretKeyWallet
Implements Signer. Holds an account secret key and an optional view-only key (required for stealth output scanning).
// Generate a new random wallet with a view-only key (for stealth support)
const wallet = SecretKeyWallet.randomWithViewKey(Network.Esmeralda);
// Restore from a stored secret key (Uint8Array)
const wallet = SecretKeyWallet.fromSecretKey(ownerSecretKey, Network.Esmeralda);
// Restore with both account key and view-only key
const wallet = SecretKeyWallet.fromSecretKey(ownerSecretKey, Network.Esmeralda, viewOnlySecretKey);
// Restore from both secret and public keys (e.g. from a keystore)
const wallet = SecretKeyWallet.fromKeypair(ownerSecretKey, publicKey, Network.Esmeralda);
// With view-only key for stealth
const wallet = SecretKeyWallet.fromKeypair(ownerSecretKey, publicKey, Network.Esmeralda, viewOnlySecretKey);
// Sign a transaction
const signatures = await wallet.signTransaction(unsignedTx);
// Access view-only key (for scanning stealth outputs)
const viewKey = wallet.getViewOnlySecret();EphemeralKeySigner
Generates a one-time throwaway keypair. Used in privacy-preserving transactions where no link to the sender's identity should exist. The key is discarded when the object is garbage-collected.
const signer = EphemeralKeySigner.generate(); // defaults to Esmeralda
const signed = await signTransaction([signer], unsignedTx);@tari-project/ootle-wallet-daemon-signer
Delegates signing to a running tari_ootle_walletd process via @tari-project/wallet_jrpc_client. The secret key never enters JavaScript memory.
import { WalletDaemonSigner } from "@tari-project/ootle-wallet-daemon-signer";
import type { WalletDaemonSignerOptions } from "@tari-project/ootle-wallet-daemon-signer";const options: WalletDaemonSignerOptions = {
url: "http://localhost:18103",
authToken: "your-auth-token",
};
// Connect and cache account info
const signer = await WalletDaemonSigner.connect(options);
const address = await signer.getAddress();
const publicKey = await signer.getPublicKey();
// Sign a transaction — the daemon returns signatures, the key stays on the daemon
const signatures = await signer.signTransaction(unsignedTx);To start the wallet daemon:
./tari_ootle_walletd --network esmeStealth transfers
ootle.ts includes a WASM-backed confidential (stealth) transfer stack. Amounts are hidden in Pedersen commitments and each output carries an encrypted payload only the recipient (who holds the matching view-only key) can scan and unblind.
import {
StealthTransfer,
WalletStealthAuthorizer,
OotleWallet,
createOutput,
submitTransaction,
watchTransaction,
} from "@tari-project/ootle";
// 1. Build: withdraw revealed funds, emit a confidential output (+ optional revealed change).
const spec = await new StealthTransfer(provider, resourceAddress)
.spendRevealedInput(sourceAccount, 5_000_000n)
.toStealthOutput(createOutput({ destination: recipientAddress, amount: 3_000_000n, resourceAddress }))
.toRevealedOutput(2_000_000n)
.payFeeFromRevealed(1_000n)
.prepare();
// 2. Authorize with a multi-signer wallet (supplies the account-key signature).
const wallet = new OotleWallet();
wallet.registerKeyProvider(senderAddress, secretKeyWallet);
wallet.setDefaultSigner(senderAddress);
const authorizer = WalletStealthAuthorizer.fromSpec(wallet, spec);
// 3. Prepare (hydrate the balance proof), seal, submit, watch.
await authorizer.prepare(provider);
const envelope = await authorizer.seal(provider);
const txId = await submitTransaction(provider, envelope);
await watchTransaction(provider, txId);To receive, decrypt a fetched UTXO with your view secret via decryptOwnedUtxo (returns null when the output is not yours). To spend a confidential UTXO, register it with .spendStealthInput(ownerAddress, commitment) and pass the owner's viewSecret to WalletStealthAuthorizer.fromSpec. Client-side scan/spend requires a SecretKeyWallet created with a view key (SecretKeyWallet.randomWithViewKey(network)).
See the Stealth Transfers guide for the full receive / send / spend walkthrough.
Examples
Four React + Vite example apps are included under examples/.
connect-button
Minimal wallet connection UI. Connects to a running wallet daemon and displays the account address and public key.
cd examples/connect-button
pnpm devRequires tari_ootle_walletd running locally. Default endpoint: http://127.0.0.1:9000/json_rpc.
indexer-explorer
Browse on-chain state. Look up substates by ID, or browse recent transactions from the indexer.
cd examples/indexer-explorer
pnpm devPre-configured to connect to the public Esmeralda testnet indexer. No local setup required.
template-inspector
Browse published template ABIs. Lists all templates cached by the indexer and renders their function definitions, argument types, and return values.
cd examples/template-inspector
pnpm devstealth-wallet
Stealth receive/decrypt/send demo backed by SecretKeyWallet. Generates a fresh stealth-capable wallet, faucets a confidential deposit, decrypts the owned UTXO, and sends a stealth transfer.
cd examples/stealth-wallet
pnpm devRequires a LocalNet indexer + faucet reachable from the browser. See examples/stealth-wallet/README.md for prerequisites.
Development
This repo uses pnpm workspaces. You'll need Node.js 22+ and pnpm 10+.
Setup
# Clone and install
git clone https://github.com/tari-project/ootle.ts.git
cd ootle.ts
pnpm installBuild
# Build all SDK packages
pnpm -r build
# Build a specific package
pnpm --filter @tari-project/ootle buildTest
# Run all package tests
pnpm -r test
# Run a single package's tests
pnpm --filter @tari-project/ootle run test
# Watch mode (from a package directory)
cd packages/ootle && pnpm vitestLint and format
# ESLint + Prettier across all packages
pnpm lint
# Check for unused exports and dependencies
pnpm knipDocumentation
The documentation site uses Starlight (Astro) with auto-generated API reference via TypeDoc.
# Build the docs site (outputs to docs/dist/)
pnpm docs
# Run the docs dev server with hot reload
pnpm docs:devThe API reference is generated from the TypeScript source of all four SDK packages using a dedicated tsconfig.typedoc.json. Hand-written guides live in docs/src/content/docs/.
Run an example
cd examples/connect-button # or indexer-explorer, template-inspector
pnpm install
pnpm devSee each example's own README for prerequisites (e.g. running a wallet daemon).
Clean everything
./scripts/clean_everything.shRepository structure
ootle.ts/
├── packages/
│ ├── ootle/ Core SDK (builder, types, transaction flow)
│ ├── ootle-indexer/ Indexer REST provider
│ ├── ootle-secret-key-wallet/ Local in-memory signer (testing)
│ └── ootle-wallet-daemon-signer/ Remote wallet daemon signer
├── examples/
│ ├── connect-button/ Wallet connection demo
│ ├── indexer-explorer/ Read-only transaction/substate browser
│ └── template-inspector/ Template ABI viewer
├── docs/ Starlight documentation site
├── scripts/ CI and utility scripts
└── .github/workflows/ CI, docs deploy, npm publishContributing
Contributions are welcome! Here's how to get involved.
Workflow
- Fork the repo and create a branch from
main pnpm install && pnpm -r build— make sure the baseline builds- Make your changes
pnpm lint— fix any lint errorspnpm -r test— ensure all tests passpnpm knip— check for unused exports or dependencies- Open a pull request against
main
CI checks
Every PR runs these GitHub Actions automatically:
| Workflow | What it does | | ------------------ | ---------------------------------------------------------------------------- | | CI | Builds all packages | | Lint | Runs ESLint + Prettier | | Docs test | Verifies the documentation site builds | | PR title | Enforces Conventional Commits format | | Signed commits | Verifies commits are signed |
Conventions
- TypeScript strict mode — all packages use
"strict": true - ESLint flat config — shared root config extended by each package
- Prettier — 120-char lines, double quotes, trailing commas
- Commit messages — follow Conventional Commits (enforced by CI)
- No default exports — use named exports everywhere
Deployment
- npm: packages are auto-published to npm on push to
mainwhen their version number changes - Docs: the documentation site auto-deploys to GitHub Pages on push to
main
License
BSD 3-Clause — see LICENSE.
