npm.io
0.14.6 • Published 5 months ago

@tari-project/tari-provider

Licence
ISC
Version
0.14.6
Deps
2
Size
3 kB
Vulns
0
Weekly
0
Stars
4

ootle.ts

TypeScript SDK for building on the Tari Ootle network.

License

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-modules when running under tsx or plain node (the WASM ESM gating in Node will be lifted in a future release). See examples/node/README.md for the rationale and forward plan; every script in examples/node/ wires the flag into its pnpm invocation 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.ts

For 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 signing
  • submitTransaction(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 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, and sendTransaction. You do not need to manage a WASM module or encoder — @tari-project/ootle-wasm is 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 WalletDaemonSigner so 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 esme

Stealth 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 dev

Requires 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 dev

Pre-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 dev
stealth-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 dev

Requires 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 install
Build
# Build all SDK packages
pnpm -r build

# Build a specific package
pnpm --filter @tari-project/ootle build
Test
# 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
Lint and format
# ESLint + Prettier across all packages
pnpm lint

# Check for unused exports and dependencies
pnpm knip
Documentation

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:dev

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

See each example's own README for prerequisites (e.g. running a wallet daemon).

Clean everything
./scripts/clean_everything.sh

Repository 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 publish

Contributing

Contributions are welcome! Here's how to get involved.

Workflow
  1. Fork the repo and create a branch from main
  2. pnpm install && pnpm -r build — make sure the baseline builds
  3. Make your changes
  4. pnpm lint — fix any lint errors
  5. pnpm -r test — ensure all tests pass
  6. pnpm knip — check for unused exports or dependencies
  7. 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 main when their version number changes
  • Docs: the documentation site auto-deploys to GitHub Pages on push to main

License

BSD 3-Clause — see LICENSE.


Built with the Tari Project