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.yAPI may shift; every breaking change is recorded inCHANGELOG.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 untypedIComponentpath 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d4df6b75878b5b04825e8adc637d6eb9848f324262af2bec9c1dd9f44335eb4
|
|
| MD5 |
800dc92478d6139c05294198e695a5a1
|
|
| BLAKE2b-256 |
7de3ffd4eef6f803a33117d98880535a5c4d1dfb7cd6f643326fb51b3c170ca8
|
Provenance
The following attestation bundles were made for ootle_py-0.1.1.tar.gz:
Publisher:
release.yml on tari-project/ootle-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ootle_py-0.1.1.tar.gz -
Subject digest:
8d4df6b75878b5b04825e8adc637d6eb9848f324262af2bec9c1dd9f44335eb4 - Sigstore transparency entry: 1628859974
- Sigstore integration time:
-
Permalink:
tari-project/ootle-py@a0fcff1e6f0d7a3d8b563910607dd065e50b6b1d -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/tari-project
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a0fcff1e6f0d7a3d8b563910607dd065e50b6b1d -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
28514ac35449f2ff99e65760a3224cfba5f95d55ed4d937a40254163fe69adfe
|
|
| MD5 |
b137731de6f2d65bba5023f4fca0b1c0
|
|
| BLAKE2b-256 |
826953bf58c618204a69eedfe4e1cc9c80f000c05520d3e329c0ce48cbe3f5f1
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ootle_py-0.1.1-py3-none-any.whl -
Subject digest:
28514ac35449f2ff99e65760a3224cfba5f95d55ed4d937a40254163fe69adfe - Sigstore transparency entry: 1628860003
- Sigstore integration time:
-
Permalink:
tari-project/ootle-py@a0fcff1e6f0d7a3d8b563910607dd065e50b6b1d -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/tari-project
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a0fcff1e6f0d7a3d8b563910607dd065e50b6b1d -
Trigger Event:
push
-
Statement type: