Metadata-Version: 2.4
Name: titon-network-fortuna-sdk
Version: 0.4.0
Summary: Python SDK for Fortuna — TON-native threshold-BLS VRF. Request randomness, decode events, verify beta. TSA-audited zero findings.
Project-URL: Homepage, https://github.com/titon-network/fortuna
Project-URL: Repository, https://github.com/titon-network/fortuna.git
Project-URL: Issues, https://github.com/titon-network/fortuna/issues
Author: titon.network
License-Expression: MIT
License-File: LICENSE
Keywords: atlas,blockchain,bls12-381,forgeton,fortuna,threshold-bls,tolk,ton,verifiable-randomness,vrf
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: py-ecc>=7.0.0
Requires-Dist: pytoniq-core>=0.1.40
Requires-Dist: pytoniq>=0.1.40
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# titon-network-fortuna-sdk

Python SDK for [**Fortuna**](https://github.com/titon-network/fortuna) — TON's
threshold-BLS VRF. Verifiable randomness for TON, operator-bonded via
ForgeTON shared-security and group-key-rotated via Atlas.

**1:1 surface parity** with [`@titon-network/fortuna-sdk`](../typescript/) (TS),
so docs translate by transliteration. Contract wrapper, VRF primitives, event
decoder, error explainer, bundled compiled artifact, live-deployment constants.

## Install

```bash
pip install titon-network-fortuna-sdk
```

Requires Python ≥ 3.11.

## Connect — testnet vs mainnet (the only difference)

The SDK is network-agnostic. You pick the network in **two lines**: the
LiteServer config and which deployment constant you import.

```python
import asyncio
from pytoniq import LiteBalancer
from fortuna_sdk import Fortuna, FORTUNA_TESTNET

async def main():
    # ── testnet ──────────────────────────────────────────────────────
    client = LiteBalancer.from_testnet_config(trust_level=2)
    await client.start_up()
    fortuna = Fortuna.create_from_address(FORTUNA_TESTNET.fortuna, client=client)

    health = await fortuna.get_contract_health()
    print(f"connected to testnet — paused={health.paused} fee_accumulated={health.config.fee_accumulated}")
    await client.close_all()

asyncio.run(main())
```

For mainnet, change exactly two lines (once Fortuna mainnet is deployed —
see *Live deployments* below):

```python
# ── mainnet ──────────────────────────────────────────────────────────
client = LiteBalancer.from_mainnet_config(trust_level=2)              # ← was from_testnet_config
await client.start_up()
fortuna = Fortuna.create_from_address(FORTUNA_MAINNET.fortuna,         # ← was FORTUNA_TESTNET
                                      client=client)
```

That's it. The same wheel, same bytecode, same code paths work on either
network — only the LiteServer config and the deployment constant change.
Pointing at a custom deployment (your own fork, a private chain) is just
`Fortuna.create_from_address("0Q...", client=client)` — no constant required.

Prefer the safe-by-default helper for scripts: `assert_deployment("testnet" |
"mainnet")` returns the populated `FortunaDeployment` or raises an actionable
error if the requested network isn't shipped yet (mainnet today).

## Three surfaces

### 1. Contract wrapper

```python
from fortuna_sdk import Fortuna, new_fortuna

# Existing deploy:
fortuna = Fortuna.create_from_address(addr, client=client)
config = await fortuna.get_config()

# Fresh deploy:
fortuna = new_fortuna(
    owner=owner_addr, forgeton=forgeton_addr, atlas=atlas_addr, client=client
)
# fortuna.address is derived from the bundled code + init data.
await fortuna.send_deploy(wallet, value=200_000_000)  # 0.2 TON
```

Every on-chain receiver has a `send_*` method on the wrapper:
`send_request_randomness`, `send_reclaim_request`, `send_fulfill_randomness`,
`send_pause`, `send_unpause`, `send_update_config`, `send_withdraw_fees`,
`send_sync_atlas`, `send_propose_code_upgrade`, `send_execute_code_upgrade`,
`send_cancel_code_upgrade`, `send_prune_expired_request`, `send_fortuna_opt_in`,
`send_fortuna_opt_out`. Every getter has a `get_*` method:
`get_ownership`, `get_paused`, `get_config`, `get_schema_versions`,
`get_operator`, `get_group_key`, `get_request`, `get_pending_upgrade`,
`get_contract_health`.

### 2. VRF primitives

```python
from fortuna_sdk import compute_alpha, compute_beta, sign_alpha, aggregate_signatures

# Off-chain operator signing:
alpha = compute_alpha(consumer, query_id, seed, creation_lt)
sig = sign_alpha(operator_sk, alpha)

# Off-chain aggregate (threshold ceremony):
agg = aggregate_signatures([sig1, sig2, ...])

# Consumer-side beta verification (after VrfCallback):
beta = compute_beta(agg, consumer, query_id, seed, creation_lt)
assert beta == event_beta_bytes  # byte-identical to on-chain
```

`compute_alpha` / `compute_beta` / `compute_req_key` are byte-identical to the
Tolk on-chain helpers. `sign_alpha` / `aggregate_signatures` use
`py_ecc.bls.G2ProofOfPossession` (min-pk + POP DST), interoperable with the
TS SDK's `@noble/curves/bls12-381` `longSignatures`.

### 3. Event decoding + error explaining

```python
from fortuna_sdk import decode_events, explain_error, FortunaError

# Decode external-out bodies into the typed FortunaEvent union:
for ev in decode_events(external_out_bodies):
    if ev.kind == "RequestFulfilled":
        print(f"req {ev.req_key:x} via {ev.submitter}, beta={ev.beta:x}")

# Explain an exit code with structured metadata:
e = explain_error(161)
# ErrorExplanation(code=161, origin='fortuna', name='InvalidBlsSignature', ...)

# Or catch the typed exception:
try:
    await fortuna.send_fulfill_randomness(...)
except FortunaError as err:
    print(err.code, err.origin, err.hint)
```

## Common task → what to import (cheat sheet)

| Task | Import |
|---|---|
| Send `RequestRandomness` | `from fortuna_sdk import Fortuna, FORTUNA_TESTNET, QueryIdStream` |
| Decode events from a tx | `from fortuna_sdk import decode_events, RequestCreatedEvent, RequestFulfilledEvent` |
| Verify beta client-side | `from fortuna_sdk import compute_beta` |
| Compute alpha for off-chain signing | `from fortuna_sdk import compute_alpha, sign_alpha, aggregate_signatures` |
| Owner ops (pause / config / withdraw) | `from fortuna_sdk import Fortuna` (every receiver has a `send_*`) |
| Code-upgrade timelock | `from fortuna_sdk import Fortuna, MIN_UPGRADE_DELAY_SECONDS` |
| Look up an exit code | `from fortuna_sdk import explain_error, format_error_explanation` |
| Catch SDK-side reverts | `from fortuna_sdk import FortunaError, SchemaDriftError` |
| Schema drift pre-flight | `await fortuna.validate_against_live()` |
| Bundle a tx into success/failure/events | `from fortuna_sdk import summarize_tx_from_parts, format_tx_summary` |
| Live testnet address book | `from fortuna_sdk import FORTUNA_TESTNET, FORTUNA_MAINNET, assert_deployment` |
| Bundled compiled BoC for a fresh deploy | `from fortuna_sdk import load_fortuna_code, fortuna_code_hash, new_fortuna` |

⚠️ **Deploy from Python is partial.** `new_fortuna(...)` builds the init
cell + bundles the BoC, and you can drive `send_deploy` from any
`pytoniq.BaseWallet`. But **admitting Fortuna at Atlas + ForgeTON
requires their TS SDKs** (no Python ports yet) — finish the wire-up
from `../../scripts/deployFortunaScripted.ts` + `wireFortunaScripted.ts`.

⚠️ **Operator off-chain signer is TS-only.** Python ships the crypto
primitives (`sign_alpha`, `aggregate_signatures`), but the production
operator daemon, keystore, metrics, and event drain live in
[`automaton`](../../../automaton/) (TypeScript). Don't roll your own in
Python unless you have a strong reason.

## Surface parity with TypeScript

Same names (snake_case mirror), same shapes. Translate any TS docs/example by
mechanical transliteration:

| TypeScript | Python |
|------------|--------|
| `Fortuna.createFromAddress(addr)` | `Fortuna.create_from_address(addr, client=client)` |
| `Fortuna.createFromConfig(cfg, code)` | `Fortuna.create_from_config(cfg, code, client=client)` |
| `fortuna.sendRequestRandomness(via, opts)` | `await fortuna.send_request_randomness(wallet, **opts)` |
| `fortuna.getRequest(consumer, queryId)` | `await fortuna.get_request(consumer, query_id)` |
| `computeAlpha(consumer, q, seed, lt)` | `compute_alpha(consumer, q, seed, lt)` |
| `decodeEvents(bodies)` | `decode_events(bodies)` |
| `explainError(161)` | `explain_error(161)` |
| `summarizeTx(tx)` | `summarize_tx(tx)` |
| `FORTUNA_TESTNET.fortuna` | `FORTUNA_TESTNET.fortuna` |
| `assertDeployment("testnet")` | `assert_deployment("testnet")` |

See [AGENTS.md](./AGENTS.md) for the full surface map +
[skills/](./skills/) for persona-grouped recipes (consumer / owner / debug).

## Project links

- [Fortuna contract source](../../contracts/fortuna.tolk)
- [TS SDK](../typescript/) — canonical, this is a port
- [`fortuna/CLAUDE.md`](../../CLAUDE.md) — repo-wide AI development guide
- [`fortuna/DESIGN.md`](../../DESIGN.md) — architecture decisions D-001..D-019

🛡️ **Audit posture.** Built on the Fortuna contract that cleared TSA static
analysis with **zero findings** — see
[`AUDIT_REPORT.md`](https://github.com/titon-network/fortuna/blob/main/AUDIT_REPORT.md).
The SDK is a transliteration of the audited TS SDK; same byte-for-byte alpha /
beta / request-key derivation.
