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.
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.
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);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);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);ootle.ts uses two core abstractions:
Implemented by IndexerProvider. Provides:
getSubstate(id)/fetchSubstates(ids)resolveInputs(inputs)— fills in missing versions before signingsubmitTransaction(envelope)getTransactionResult(txId)listRecentTransactions(params)/getTemplateDefinition(address)
Implemented by SecretKeyWallet, WalletDaemonSigner, and EphemeralKeySigner. Provides:
getAddress()/getPublicKey()signTransaction(unsignedTx)
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 finalization
Or 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.
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";enum Network {
MainNet = 0x00,
StageNet = 0x01,
NextNet = 0x02,
LocalNet = 0x10,
Igor = 0x24,
Esmeralda = 0x26,
}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 |
// 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 }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);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();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"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";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();// 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();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.
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 inLocal 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";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();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);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 esmeootle.ts includes support for stealth (privacy-preserving) transfers, mirroring the stealth module in the Rust ootle-rs crate.
Stealth transfers produce outputs with one-time public keys — only the recipient (who holds the matching view-only key) can scan and spend them.
import {
StealthTransfer,
WalletStealthAuthorizer,
OotleWallet,
signTransaction,
sealTransaction,
resolveTransaction,
submitTransaction,
} from "@tari-project/ootle";
// 1. Build the stealth transfer
const spec = await new StealthTransfer(Network.Esmeralda, factory)
.from(sourceAccount, resourceAddress)
.to(recipientPublicKeyHex, 1_000_000n)
.feeFrom(feeAccount, 1000n)
.build();
// 2. Create the authorizer (signs with the account key)
const wallet = new OotleWallet();
wallet.registerKeyProvider(senderAddress, secretKeyWallet);
wallet.setDefaultSigner(senderAddress);
const authorizer = WalletStealthAuthorizer.fromSpec(wallet, spec);
// 3. Sign, seal, and submit
const resolved = await resolveTransaction(provider, spec.unsignedTx);
const signed = await signTransaction([authorizer], resolved);
const envelope = sealTransaction(signed);
const txId = await submitTransaction(provider, envelope);Interfaces for implementing your own stealth providers:
StealthOutputStatementFactory— generates output statements (proofs + encrypted data)InputDecryptor— decrypts stealth inputs owned by your keyOutputMaskProvider— provides fresh output masks (blinding factors)DiffieHellmanKdfKeyProvider— derives shared secrets for output encryption
Three React + Vite example apps are included under examples/.
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.
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.
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 devThis repo uses pnpm workspaces. You'll need Node.js 22+ and pnpm 10+.
# Clone and install
git clone https://github.com/tari-project/tari.js.git
cd tari.js
pnpm install# Build all SDK packages
pnpm -r build
# Build a specific package
pnpm --filter @tari-project/ootle build# 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 vitest# ESLint + Prettier across all packages
pnpm lint
# Check for unused exports and dependencies
pnpm knipThe 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/.
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).
./scripts/clean_everything.shtari.js/
├── 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 publish
Contributions are welcome! Here's how to get involved.
- 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
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 |
- 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
- 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
BSD 3-Clause — see LICENSE.