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.0.tar.gz (649.9 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.0-py3-none-any.whl (617.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: ootle_py-0.1.0.tar.gz
  • Upload date:
  • Size: 649.9 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.0.tar.gz
Algorithm Hash digest
SHA256 1b71353e9b14bd48a9be058de5fef8217518aaaedf704c677c6a388831abe787
MD5 fd17d4be9f8d75fcee645dbcc3657331
BLAKE2b-256 bdf513aa8c221f8008e6cf8f78fcbad60e2aef27346cfe9274dd84001f9997d8

See more details on using hashes here.

Provenance

The following attestation bundles were made for ootle_py-0.1.0.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.0-py3-none-any.whl.

File metadata

  • Download URL: ootle_py-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 617.5 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8ab2559727defec20401537b65cfc5a3620ef026c6c63a5d708e6fc3d463ad7b
MD5 a42acd45aa777d110cc336375be03ecc
BLAKE2b-256 200371a172ef841648d95a9fc10676096d279a93effb6f515553b58faf92d33c

See more details on using hashes here.

Provenance

The following attestation bundles were made for ootle_py-0.1.0-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