Skip to main content

ootle

A modern, async-first Python client library for the Tari L2 network (codename Ootle). It is the Pythonic counterpart to the Rust crate ootle-rs and the JavaScript package @tari-project/ootle-wasm.

Status: pre-1.0. The 0.x.y API may shift; every breaking change is recorded in CHANGELOG.md.

Install

pip install ootle-py
# or
uv add ootle-py

The distribution name on PyPI is ootle-py; you import it as ootle:

from ootle import AsyncOotleClient, OotleClient

Requirements: Python >= 3.13. The package is pure-Python; the Tari WASM crypto blob ships inside the wheel — no native build step, and the same py3-none-any wheel runs everywhere.

Quick start: read-only query

Connect to a LocalNet indexer and read an account balance. No wallet is needed for read-only queries.

import asyncio
from ootle import (
    AsyncOotleClient, ComponentAddress, Network,
    ResourceAddress, TARI_TOKEN, default_indexer_url,
)


async def main() -> None:
    account = ComponentAddress("component_...your-address...")
    async with AsyncOotleClient.connect(default_indexer_url(Network.LOCAL_NET)) as client:
        balance = await client.get_account_balance(account, ResourceAddress(TARI_TOKEN))
        print(f"TARI balance: {balance}")
        for resource, amount in (await client.get_account_balances(account)).items():
            print(f"  {resource}: {amount}")


asyncio.run(main())

Full version: examples/balance_query.py (and the sync mirror, examples/balance_query_sync.py).

Quick start: public transfer

Faucet a fresh sender, then transfer TARI to a fresh recipient, watching each transaction to finalisation. Note that seal_transaction is synchronous — only the I/O calls are awaited.

import asyncio
from ootle import (
    AsyncOotleClient, LocalSigner, Network, OotleSecretKey,
    OotleWallet, ResourceAddress, TARI, TARI_TOKEN, default_indexer_url,
)


async def main() -> None:
    sender = OotleSecretKey.random(Network.LOCAL_NET)
    wallet = OotleWallet(default=LocalSigner(sender))
    resource = ResourceAddress(TARI_TOKEN)
    async with AsyncOotleClient.connect(
        default_indexer_url(Network.LOCAL_NET), wallet=wallet
    ) as client:
        # Faucet the public XTR faucet's fixed dispense into the sender.
        funded = await client.faucet().take_funds().pay_fee(5_000).prepare()
        await (await client.send_transaction(client.seal_transaction(funded))).watch()

        # Transfer 2 TARI to a fresh recipient.
        recipient = OotleSecretKey.random(Network.LOCAL_NET).to_address()
        unsigned = await (
            client.account()
            .pay_fee(1000)
            .public_transfer(recipient, resource, 2 * TARI)
            .prepare()
        )
        sealed = client.seal_transaction(unsigned)
        await (await client.send_transaction(sealed)).watch()


asyncio.run(main())

Register extra signers with wallet.register(LocalSigner(...)) and they are folded in automatically at seal time (multi-signer co-authorisation); manual_co_signing.py shows the explicit authorize → attach → seal hand-off for remote-signer / HSM setups. Estimate fees first with await client.send_dry_run(unsigned). Full file: examples/fungible_transfer.py.

Stealth (confidential) transfers

Confidential transfers run entirely on the vendored ootle-wasm blob — the default crypto provider. Bulletproofs, ElGamal viewable balances, balance-proof signatures, and input-mask aggregation all execute locally; no wallet daemon or external service is required.

Build the transfer, hydrate the balance proof with an AsyncWalletStealthAuthorizer, then seal with the wallet's default signer:

import asyncio
from ootle import (
    AsyncOotleClient, AsyncStealthTransfer, AsyncWalletStealthAuthorizer,
    LocalSigner, Network, OotleSecretKey, OotleWallet, Output,
    ResourceAddress, TARI, TARI_TOKEN, default_indexer_url,
)


async def main() -> None:
    sender = OotleSecretKey.random(Network.LOCAL_NET)
    wallet = OotleWallet(default=LocalSigner(sender))
    resource = ResourceAddress(TARI_TOKEN)
    recipient = OotleSecretKey.random(Network.LOCAL_NET).to_address()
    async with AsyncOotleClient.connect(
        default_indexer_url(Network.LOCAL_NET), wallet=wallet
    ) as client:
        # ... faucet the sender first (see examples/stealth/) ...
        transfer = AsyncStealthTransfer(client, resource)
        transfer.spend_revealed_input(sender.to_address().to_component_address(), 5 * TARI)
        transfer.to_stealth_output(
            Output(destination=recipient, amount=4 * TARI, resource_address=resource)
        )
        transfer.to_revealed_output(TARI)
        transfer.pay_fee_from_revealed(500)

        spec = await transfer.prepare()
        authorizer = AsyncWalletStealthAuthorizer(
            wallet, spec, view_secret=sender.view_secret
        )
        hydrated = await authorizer.prepare(client)
        sealed = client.seal_transaction(hydrated.unsigned)
        await (await client.send_transaction(sealed)).watch()


asyncio.run(main())

Stealth supports revealed and stealth inputs and outputs, mixed in one transfer: spend_revealed_input / spend_stealth_input (with input-mask aggregation) feed to_stealth_output / to_revealed_output, with fees paid from a revealed bucket (pay_fee_from_revealed) or a stealth account's revealed vault (pay_fee_from_stealth). The public faucet has a stealth path too (IFaucet.take_funds_stealth), and owned UTXOs are read back with AsyncOotleClient.decrypt_owned_utxo (AEAD owner-read).

The full set of runnable stealth examples — faucet deposit, stealth↔revealed, stealth↔stealth, spending an owned UTXO, and the sync mirror — lives in examples/stealth/.

Sync vs. async

Every async API has a sync mirror with the same shape. Swap the imports and drop the awaits:

from ootle import OotleClient                  # sync
from ootle import AsyncOotleClient             # async

The sync names mirror the async ones — IAccount / IAsyncAccount, StealthTransfer / AsyncStealthTransfer, PendingTransaction / AsyncPendingTransaction, and so on. The sync tree is generated from the async source by scripts/unasync.py and committed; CI asserts the two trees stay byte-identical. Both ship in the wheel.

Running the examples

The examples are self-contained — each generates fresh keys and faucets its own funds against a LocalNet indexer:

OOTLE_INDEXER_URL=http://localhost:12500 \
uv run python -m examples.fungible_transfer

See examples/README.md and examples/stealth/README.md for the full catalogue and the few examples that need an external artifact (a deployed template, a live component to watch, …).

Logging

Every internal module emits diagnostics under the ootle.* logger namespace. The library installs a NullHandler on ootle so unconfigured callers see nothing; opt in by configuring logging:

import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("ootle").setLevel(logging.DEBUG)

Useful sub-namespaces:

  • ootle._async._transport / ootle._sync._transport — request paths.
  • ootle._async._watcher / ootle._sync._watcher — SSE open/close, fallbacks.
  • ootle._async._resolver / ootle._sync._resolver — chunked substate fetches.
  • ootle._crypto._wasm_runtime — WASM blob load + SHA-256 verification.
  • ootle.wallet — multi-signer co-authorisation traces.

Scope

In v1: read-only queries · the public XTR faucet · public transfers (IAccount) · multi-signer co-authorisation · the untyped component DSL (IComponent.call_function / call_method) · dry-run fee estimation · transaction and component event watching · confidential stealth transfers (revealed + stealth inputs/outputs, input-mask aggregation, the faucet stealth path, and UTXO read helpers), all backed by the vendored WASM blob.

Not in v1:

  • Typed templates / ootle_template! — only the untyped IComponent path ships; there is no codegen for typed bindings.
  • HD wallets, BIP-32, mnemonics — caller-application concerns.
  • Borsh in Python — all Borsh encoding/decoding is delegated to the WASM blob, never reimplemented in Python.

Contributing

The engineering contract — including the 200-line file limit and the strict typing rules — is in CLAUDE.md.

make sync          # bootstrap the venv
make ci            # the gate the pipeline runs
make unasync       # regenerate src/ootle/_sync/ from _async/

After any change under src/ootle/_async/, run make unasync and commit the regenerated _sync/ mirror in the same commit — make ci fails on drift.

License & acknowledgements

Released under the BSD 3-Clause license — see LICENSE.

The vendored WASM blob is built from @tari-project/ootle-wasm and ships inside the wheel under src/ootle/_crypto/wasm/ootle_wasm_bg.wasm, with its upstream version and SHA-256 recorded in VERSION and verified at load.

Metadata

Release files for ootle-py 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ootle-py 0.3.2
File Size Uploaded Provenance
ootle_py-0.3.2.tar.gz 729.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ootle-py 0.3.2
File Interpreter ABI Platform
ootle_py-0.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 1.4 MB

Release files / ootle_py-0.3.2.tar.gz

Download URL ootle_py-0.3.2.tar.gz
Size 729.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d4186f4f3820d37d79f23f9c06c5f6becba79be3bb5f4dfbb1774bc4b49f5fc6
BLAKE2b-256 checksum
How to use checksums
1aec7e8bbd245f377f84ad31930917b30f31322da84ac2c9747878b66249c04f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / ootle_py-0.3.2-py3-none-any.whl

Download URL ootle_py-0.3.2-py3-none-any.whl
Size 697.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
46e0c85f2cb287917474832da15b4a29065370abc34a67f431e33e1f9e2782f8
BLAKE2b-256 checksum
How to use checksums
34f89461d41922e899d7b7c042da7fc870ed287174bac660d7f85c968af695ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for ootle-py 0.3.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ootle_py-0.3.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page