Metadata-Version: 2.4
Name: tronado
Version: 0.1.0
Summary: Production-grade, version-aware Python SDK for the Tronado Public API (v5).
Project-URL: Homepage, https://t.me/TronadoBusiness
Project-URL: Documentation, https://documenter.getpostman.com/view/48018954/2sBXwwm7St
Project-URL: Support, https://t.me/TronadoSupp
Author: Tronado SDK contributors
License: MIT
License-File: LICENSE
Keywords: api-client,crypto,payments,sdk,tron,tronado,trx
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.5
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: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: tox>=4.0; extra == 'dev'
Description-Content-Type: text/markdown

# Tronado Python SDK

A production-grade, **version-aware** Python SDK for the [Tronado Public API](https://documenter.getpostman.com/view/48018954/2sBXwwm7St).
Tronado lets a business accept TRON (TRX) payments: you create an order, the customer
pays through Tronado's in-app/web payment UI, and Tronado notifies your server with a
signed webhook.

- **Sync & async** clients on a single `httpx` core
- **Typed** Pydantic v2 request/response models (amounts are `Decimal`, never `float`)
- **Idempotency-aware retries** — order creation is never silently retried
- **Webhook verification** for the documented `X-Tronado-Sig` HMAC-SHA512 scheme
- **Designed for API versioning** — v5 today, future versions plug in cleanly

> Currently implements API **v5** (the recommended version).

---

## Installation

```bash
pip install tronado
```

From source (this repository):

```bash
pip install -e ".[dev]"   # includes test/lint tooling
```

Requires Python **3.9+**. Runtime dependencies: `httpx` and `pydantic` (v2).

---

## Authentication

Every outbound endpoint requires your API key in the **`x-api-key`** header (this is the
documented scheme — there is no `Authorization: Bearer`). Request a key from
[Tronado support](https://t.me/TronadoSupp).

```python
from tronado import TronadoClient

client = TronadoClient(api_key="YOUR_API_KEY")
```

Or via environment variable (no argument needed):

```bash
export TRONADO_API_KEY="YOUR_API_KEY"
# optional: export TRONADO_BASE_URL="https://bot.tronado.cloud"
```

---

## Configuration

| Option | Default | Description |
|---|---|---|
| `api_key` | `TRONADO_API_KEY` env | API key for the `x-api-key` header |
| `base_url` | `https://bot.tronado.cloud` | API base URL (`TRONADO_BASE_URL` honoured) |
| `timeout` | `30.0` | Per-request timeout (seconds) |
| `max_retries` | `3` | Max retries for **idempotent** operations |
| `backoff_factor` | `0.5` | Exponential-backoff base multiplier (seconds) |
| `default_version` | `"v5"` | Version used by the `order` / `price` shortcuts |
| `default_headers` | `{}` | Extra headers added to every request |
| `user_agent` | `tronado-python/<ver>` | `User-Agent` header |
| `http_client` | `None` | Inject your own `httpx.Client`/`AsyncClient` |

```python
client = TronadoClient(
    api_key="YOUR_API_KEY",
    timeout=15.0,
    max_retries=5,
    backoff_factor=0.25,
    default_headers={"X-Trace-Id": "abc123"},
)
```

You can also pass a fully built `TronadoConfig`:

```python
from tronado import TronadoConfig, TronadoClient

config = TronadoConfig(api_key="YOUR_API_KEY", timeout=10)
client = TronadoClient(config=config)
```

---

## Quickstart (sync)

```python
from decimal import Decimal
from tronado import TronadoClient

with TronadoClient(api_key="YOUR_API_KEY") as tron:
    # 1. Tronado's TRX price differs from exchanges — always price first.
    price = tron.price.tron.get_price_to_toman()
    print(price.tron_price_toman, "Toman per TRX")

    # 2. Create the order and get a payment link.
    order = tron.order.get_order_token(
        payment_id="inv-1001",                       # your unique id
        wallet_address="TXYZ12345abcdef...",         # destination wallet
        tron_amount=Decimal("12.123456"),            # invoice amount in TRX
        callback_url="https://your-domain.com/payment/callback",
        wage_from_business_percentage=0,             # who absorbs the fee (0–100)
    )
    print("Pay here:", order.full_payment_url)

    # 3. Later, check status (by Tronado OrderId / TrndOrderID_x / TXID).
    status = tron.order.get_status(id="TrndOrderID_55")
    print("Paid?" , status.is_payment_accepted)
```

## Quickstart (async)

```python
import asyncio
from tronado import AsyncTronadoClient

async def main() -> None:
    async with AsyncTronadoClient(api_key="YOUR_API_KEY") as tron:
        price = await tron.price.tron.get_price_to_toman()
        order = await tron.order.get_order_token(
            payment_id="inv-1002",
            wallet_address="TXYZ...",
            tron_amount="8.5",
            callback_url="https://your-domain.com/payment/callback",
        )
        print(order.full_payment_url)

asyncio.run(main())
```

---

## Endpoint reference

The async client exposes the same methods with `await`.

### Order — `client.order`

| Method | Endpoint | Returns |
|---|---|---|
| `get_order_token(payment_id, wallet_address, tron_amount, callback_url, wage_from_business_percentage=0)` | `POST /api/v5/GetOrderToken` | `OrderTokenData` |
| `get_status(id)` | `POST /Order/GetStatus` | `OrderStatus` (raises `OrderNotFoundError` if absent) |
| `get_status_by_payment_id(id)` | `POST /Order/GetStatusByPaymentID` | `OrderStatus` |

### Price — `client.price`

| Method | Endpoint | Returns |
|---|---|---|
| `price.tron.get_price_to_toman()` | `POST /Tron/GetPriceToToman` | `TronPrice` |
| `price.tron.get_price_with_wage_to_toman(request_code, wallet_address, tron_amount)` | `POST /Tron/GetPriceWithWageToToman` | `PriceWithWage` |
| `price.toman.convert_to_tron_wage_subtracted(toman, wallet)` | `POST /Toman/ConvertToTronWageSubtracted` | `TronConversion` |
| `price.toman.get_price_to_toman()` | `POST /Toman/GetPriceToToman` | `DollarPrice` |
| `price.dollar.convert_to_tron_wage_subtracted(dollar, wallet)` | `POST /Dollar/ConvertToTronWageSubtracted` | `TronConversion` |
| `price.dollar.get_price_to_toman()` | `POST /Dollar/GetPriceToToman` | `DollarPrice` |

> Amount arguments (`tron_amount`, `dollar`) accept `Decimal`, `int`, `float`, or `str`.
> They are coerced via `str` to avoid binary-float rounding, and sent on the wire as JSON
> numbers.

---

## Handling webhooks (IPN)

Tronado POSTs a JSON callback to your `CallbackUrl` **on every order status change** and
signs it with `X-Tronado-Sig = HMAC_SHA512(raw_body, your_ipn_signing_key)` (lowercase
hex). Get your `IpnSigningKey` from support. **Always verify against the raw body** — do
not re-serialize parsed JSON.

```python
from tronado.webhook import construct_event
from tronado.exceptions import InvalidSignatureError

IPN_SIGNING_KEY = "YOUR_IPN_SIGNING_KEY"

def handle_webhook(raw_body: bytes, signature_header: str) -> int:
    try:
        event = construct_event(raw_body, signature_header, IPN_SIGNING_KEY)
    except InvalidSignatureError:
        return 401  # reject

    # De-duplicate: the same (payment_id, status) may arrive more than once.
    if already_processed(event.dedup_key):
        return 200

    if event.is_payment_accepted:               # IsPaid == True or status 30
        # Charge the user with what they actually paid (includes wage), per the docs.
        credit_user(event.payment_id, event.user_paid_toman_amount)

    return 200  # return 2xx to acknowledge, else Tronado retries
```

FastAPI example (reads the **raw** body before parsing):

```python
from fastapi import FastAPI, Request, Response
from tronado.webhook import construct_event
from tronado.exceptions import InvalidSignatureError, TronadoWebhookError

app = FastAPI()

@app.post("/payment/callback")
async def callback(request: Request) -> Response:
    raw = await request.body()
    sig = request.headers.get("X-Tronado-Sig", "")
    try:
        event = construct_event(raw, sig, "YOUR_IPN_SIGNING_KEY")
    except (InvalidSignatureError, TronadoWebhookError):
        return Response(status_code=401)
    # ... process event ...
    return Response(status_code=200)
```

If you only need to verify or parse separately:

```python
from tronado.webhook import verify_signature, parse_callback

if verify_signature(raw, sig, signing_key):
    event = parse_callback(raw)
```

---

## Error handling

All errors derive from `TronadoError`:

```python
from tronado.exceptions import (
    TronadoError, TronadoAuthenticationError, TronadoRateLimitError,
    OrderNotFoundError, TronadoValidationError, TronadoServerError,
    TronadoTimeoutError, TronadoConnectionError,
)

try:
    status = client.order.get_status(id="maybe-missing")
except OrderNotFoundError:
    status = None
except TronadoAuthenticationError:
    ...   # bad/missing API key (HTTP 401 or envelope Code == -1)
except TronadoRateLimitError:
    ...   # 429 / per-user limits
except (TronadoTimeoutError, TronadoConnectionError):
    ...   # network problems
except TronadoServerError:
    ...   # 5xx (already retried for idempotent calls)
except TronadoError:
    ...   # catch-all
```

`TronadoAPIError` (and its subclasses) carry `.status_code`, `.code`, `.message`, and
`.response` for diagnostics.

### Retries & idempotency

- Idempotent reads (price/status) are retried on timeouts, connection errors, `429`, and
  `5xx`, using exponential backoff with jitter (and `Retry-After` when present).
- **`get_order_token` is never auto-retried** — it creates a transaction, so retrying on
  an ambiguous failure could create a duplicate order. Handle its failures explicitly.

---

## Versioning

The Tronado API versions endpoints in the URL path (`/api/v{version}/...`). In v5 only
`GetOrderToken` is version-pathed; the price/status endpoints live at unversioned roots.
The SDK models this with per-version **operation descriptors**, so:

```python
client.v5.order.get_order_token(...)     # explicit version pin
client.version("v5").price.tron.get_price_to_toman()
client.order.get_order_token(...)        # uses default_version
```

When Tronado ships a new version, it becomes a new `tronado/versions/vN/` package and a
one-line registry entry — the transport, models, and error handling are reused unchanged.

```python
from tronado import available_versions
print(available_versions())   # ('v5',)
```

---

## The fee (wage) model

The default wage is 20% (configurable per business; minimum is the greater of 9,000 Toman
or $0.10). `wage_from_business_percentage` on `get_order_token` controls who absorbs it:

- `0` (default): the whole fee is added on top of the user's payment; you receive the full
  invoice TRX.
- `100`: the whole fee is taken from your share; the user pays roughly the base value and
  your received `TronAmount` is the net after fee.
- Values in between split the fee proportionally.

To charge your user correctly, use `user_paid_toman_amount` from the **v5 webhook** (what
the user actually paid) rather than the TRX you receive.

---

## Development

```bash
pip install -e ".[dev]"
pytest                 # run the test-suite (sync + async)
ruff check .           # lint
mypy src               # type-check
```

Run the suite across every supported interpreter with **tox** (included in the `dev`
extra installed above):

```bash
tox                    # py39–py313 + a lint/type-check env
```

CI (GitHub Actions, [`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs ruff and
mypy once, then the test-suite on Python **3.9, 3.10, 3.11, 3.12, and 3.13**.

See [`examples/`](examples/) for runnable scripts and [`docs/DESIGN.md`](docs/DESIGN.md)
for the architecture and the full endpoint matrix.

## License

MIT — see [LICENSE](LICENSE).

Support: [t.me/TronadoSupp](https://t.me/TronadoSupp)
