Skip to main content

Async-first Python client library for the Tari L2 (Ootle) network.

Project description

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(500).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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ootle_py-0.1.1.tar.gz (650.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ootle_py-0.1.1-py3-none-any.whl (617.6 kB view details)

Uploaded Python 3

File details

Details for the file ootle_py-0.1.1.tar.gz.

File metadata

  • Download URL: ootle_py-0.1.1.tar.gz
  • Upload date:
  • Size: 650.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for ootle_py-0.1.1.tar.gz
Algorithm Hash digest
SHA256 8d4df6b75878b5b04825e8adc637d6eb9848f324262af2bec9c1dd9f44335eb4
MD5 800dc92478d6139c05294198e695a5a1
BLAKE2b-256 7de3ffd4eef6f803a33117d98880535a5c4d1dfb7cd6f643326fb51b3c170ca8

See more details on using hashes here.

Provenance

The following attestation bundles were made for ootle_py-0.1.1.tar.gz:

Publisher: release.yml on tari-project/ootle-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ootle_py-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: ootle_py-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 617.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for ootle_py-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 28514ac35449f2ff99e65760a3224cfba5f95d55ed4d937a40254163fe69adfe
MD5 b137731de6f2d65bba5023f4fca0b1c0
BLAKE2b-256 826953bf58c618204a69eedfe4e1cc9c80f000c05520d3e329c0ce48cbe3f5f1

See more details on using hashes here.

Provenance

The following attestation bundles were made for ootle_py-0.1.1-py3-none-any.whl:

Publisher: release.yml on tari-project/ootle-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page