Metadata-Version: 2.4
Name: risex
Version: 0.10.0
Summary: Asynchronous Python SDK for the RISEx perpetuals DEX (trading, market data, EIP-712 signing, WebSocket feeds).
Project-URL: Homepage, https://github.com/Dmkls/risex
Project-URL: Repository, https://github.com/Dmkls/risex
Project-URL: Issues, https://github.com/Dmkls/risex/issues
Author-email: Nikita <plentyplesa@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: async,crypto,dex,eip712,ethereum,perp,perpetuals,risex,trading,websocket
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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: Topic :: Office/Business :: Financial :: Investment
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: aiohttp>=3.9.0
Requires-Dist: eth-abi>=5.0.0
Requires-Dist: eth-account>=0.11.0
Requires-Dist: eth-utils>=4.0.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: build>=1.0.0; extra == 'dev'
Requires-Dist: mypy>=1.8.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Requires-Dist: twine>=5.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# RISEx Python SDK 🚀

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Asyncio](https://img.shields.io/badge/asyncio-supported-green.svg)](https://docs.python.org/3/library/asyncio.html)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

The **RISEx Python SDK** is an asynchronous library for the [RISEx perpetuals DEX](https://rise.trade/). It is aimed at trading bots, market makers, and analytics, and handles the awkward parts for you: EIP-712 signing, session-signer registration, and the exact request/payload formats the exchange expects.

> Note: RISEx settles actions on-chain — each signed action (order, cancel, leverage, TPSL) is a blockchain transaction whose response includes a receipt and block number. It is fast and supports concurrent submission, but it is not a sub-millisecond CEX matching engine; size your latency expectations accordingly.

---

## ✨ Features

- 🎯 **One-call trading**: `open_long("BTC", 100, leverage=3, tp=..., sl=...)` — USD sizing, symbol markets, TP/SL attached in the same call.
- 🔁 **Built-in retry**: transient timeouts, disconnects, 5xx and 429s are retried with backoff; signed orders are never blindly resent.
- ⚡ **Asynchronous**: built on `aiohttp` + `asyncio`; signed actions accept a pre-fetched `nonce` so you can submit concurrently (see [Concurrency](#-concurrency--nonces)).
- 🔐 **EIP-712 signing built in**: builds, hashes and signs every action locally with `eth-account` / `eth-abi` — payloads are verified byte-for-byte against the live API.
- 🔑 **Automatic session signer**: `create()` registers a throwaway session key so your main private key never signs individual orders.
- 🛡️ **Typed models**: market data and account reads are `pydantic` models with IDE autocompletion. (State-changing calls return the raw response `dict` from the exchange.)
- 📊 **Full coverage**: market data, account reads, orders, leverage/margin, and the complete TPSL lifecycle — all verified against mainnet (`scripts/live_smoke_test.py`).

Full technical reference — every method, wire format, retry rule and EIP-712
structure — lives in [`docs/`](docs/README.md).

---

## 📦 Installation

```bash
pip install risex
```

Or from source:

```bash
git clone https://github.com/Dmkls/risex.git
cd risex
pip install -e .
```

---

## ✅ Verify it works (live smoke test)

A self-contained script exercises **every** public method against the real
mainnet API and prints a `PASS`/`FAIL` line per step plus a summary (exit code 0
if everything works):

```bash
# Safe: read-only endpoints only, no key needed
python scripts/live_smoke_test.py --read-only

# Full check: needs a funded test wallet. Opens AND closes a real position,
# and runs the full TPSL lifecycle. Always cleans up after itself.
export RISEX_PRIVATE_KEY=0x...
python scripts/live_smoke_test.py
```

> ⚠️ The full run places **real** orders and opens/closes a **real** position
> (tiny size, ~1.5x the market minimum). Use a dedicated test wallet with a small
> balance — it costs a little in fees/spread.

---

## 🚀 Quickstart

RISEx requires wallets to register a "signer" to place orders without signing metamask transactions every time. The SDK abstracts this entirely.

```python
import asyncio
import os
from risex import RiseXClient

async def main():
    # Never hard-code a key. Read it from the environment (or a .env you load).
    private_key = os.environ["RISEX_PRIVATE_KEY"]

    # create() registers a temporary session signer for you automatically.
    client = await RiseXClient.create(private_key)
    print(f"Connected to RISEx as {client.account_address}")

    # Fetch your cross-margin balance (returns a float, in USDC)
    balance = await client.info.get_cross_margin_balance(client.account_address)
    print(f"Current Margin Balance: {balance} USDC")

    await client.close()

if __name__ == "__main__":
    asyncio.run(main())
```

> **Tip:** both clients are async context managers, so you can let `async with`
> close the HTTP session for you:
> ```python
> async with await RiseXClient.create(private_key) as client:
>     balance = await client.info.get_cross_margin_balance(client.account_address)
> ```

---

## 🎯 One-call trading (high-level API)

The simplest way to trade. Everything is sized in **USD**, markets are addressed
by **symbol**, and entry + leverage + TP/SL happen in a single call:

```python
async with await RiseXClient.create(private_key) as client:
    # Open a $100 LONG on BTC at 3x, with take-profit and stop-loss attached:
    await client.open_long("BTC", 100, leverage=3, tp=95_000, sl=55_000)

    # Or a short (tp/sl/leverage are all optional):
    await client.open_short("ETH", 50)

    # Read your own account without passing an address around:
    balance   = await client.get_balance()          # cross-margin USDC
    positions = await client.get_positions()        # all open positions
    pos       = await client.get_position("BTC")    # one market (None if flat)
    orders    = await client.get_open_orders()      # resting orders
    trades    = await client.get_trade_history()    # recent fills

    # Flatten one market — or everything:
    await client.close_position("BTC")
    await client.close_all_positions()
```

`tp`/`sl` are USD trigger prices, placed as TPSL trigger orders for the same
size right after the entry (TPSL needs the one-time
`approve_permit_single_budget()` — see Authentication below). If the entry
succeeds but the TP/SL placement fails, the raised error says so explicitly, so
you never silently lose protection.

---

## 🛡️ Reliability: built-in retries, timeouts & proxies

The HTTP layer retries transient failures automatically, so one flaky request
doesn't kill your bot:

- **429 rate limits** — retried for every request, honoring `Retry-After`.
- **Timeouts, disconnects, 5xx** — retried with exponential backoff + jitter for
  all **reads** (GET) and for idempotent setup calls (signer registration, terms).
- **Connect failures** (DNS, refused, proxy down) — retried for everything: the
  request never reached the server, so a resend can't double-execute.
- **Order placement is never blindly resent**: if a signed action times out, the
  server may still have processed it, so the SDK raises a clear
  `RiseXConnectionError` telling you to check open orders/positions first.

Defaults: 4 retries, 10s per-attempt timeout — both tunable on every factory:

```python
client = await RiseXClient.create(pk, timeout_ms=15_000, max_retries=6)
info   = await RiseXInfoClient.create(max_retries=2)
```

**Proxies.** Pass `proxy="http://user:pass@host:port"` to any factory, or just
set the standard `HTTPS_PROXY` environment variable (the SDK honors it). The
API sits behind Cloudflare, which occasionally tarpits datacenter/flagged IPs
for non-browser clients — if you see persistent timeouts while the website
works, route the SDK through a clean proxy.

---

## 🔑 Authentication: main key vs. session signer ("API key")

RISEx never signs individual orders with your main wallet. Instead your main
wallet registers a **session signer** — a throwaway keypair (think "API key") —
and every order/cancel/leverage/TPSL action is signed by that signer. The main
private key is only needed for **one-time setup**: registering the signer and
`approve_permit_single_budget`.

So there are two ways to authenticate:

**1. Main private key** — `RiseXClient.create(private_key)`. Generates and
registers a session signer for you automatically. Use this for first-time setup,
or if you're fine keeping the main key on the trading machine.

**2. Address + session-signer key (no main key)** —
`RiseXClient.from_signer(account_address, signer_key)`. The bot authenticates
with just your **public address** and the **signer key**, so the main private
key never touches the trading machine. This is the recommended setup for bots.

```python
# --- one-time, on a trusted machine: register a signer and save it as your API key ---
client = await RiseXClient.create(main_private_key)
api_key = client.signer_key          # <-- save this (it's the session signer's private key)
await client.approve_permit_single_budget()   # one-time, enables TPSL triggers
await client.close()

# --- afterwards, on the bot: address + API key only, NO main key ---
async with await RiseXClient.from_signer(account_address, api_key) as client:
    await client.market_buy_usd("BTC", 100)    # signed by the session key
```

> The signer key can **trade** but is registered with an expiry (≈30 days), after
> which you re-register with `create(main_key)`. `approve_permit_single_budget`
> and signer registration are unavailable on a `from_signer` client (they need
> the main key) and raise a clear `RiseXError`. Treat the signer key as a secret.

**Managing signers.** List the signers registered to an account and revoke ones
you no longer use:

```python
for s in await info.get_signers(account):          # signer, label, expiration, status
    print(s.signer, s.status)

await client.revoke_signer(old_signer_address)     # needs the main key; status -> "Revoked"
```

---

## 📐 Units: steps & ticks

Order endpoints do **not** take USD. Size is an integer count of **steps** and
price is an integer count of **ticks**, where each market defines the increment:

```
asset_size = size_steps * market.config.step_size
price_usd  = price_ticks * market.config.step_price
```

> 💡 **Prefer the USD helpers.** If you just want to trade in dollars, skip this
> whole section and use `market_buy_usd` / `limit_sell_usd` etc. (see
> [Placing Orders](#placing-orders)) — they do the conversion for you. The
> manual path below is for when you need exact step/tick control.

The lowest-level helpers convert explicitly. You no longer have to fetch and
filter the market list yourself — pass a market **id or symbol** to
`info.get_market(...)`:

```python
from risex import RiseXInfoClient, usd_to_steps, price_to_ticks, steps_to_size, ticks_to_price

info = await RiseXInfoClient.create()
market = await info.get_market("BTC")          # by symbol; or get_market(1) by id

size_steps  = usd_to_steps(100, market)        # $100 of notional -> steps (floored)
price_ticks = price_to_ticks(85_000, market)   # $85,000 -> ticks (rounded)

# ...and back the other way:
size_in_btc = steps_to_size(size_steps, market)
price_in_usd = ticks_to_price(price_ticks, market)
```

On the trading client there are one-call shortcuts that resolve the market for
you: `await client.to_steps("BTC", 100)` and `await client.to_ticks("BTC", 85_000)`.

> ⚠️ `size_steps` / `price_ticks` returned inside `OpenOrder` are the backend's
> internal **encoded** form — they do **not** equal the values you submitted and
> won't round-trip through these helpers. Use them only as opaque fields; read
> real sizes/prices from positions, fills, or the orderbook.

---

## 📖 Public Data: `RiseXInfoClient`

The `RiseXInfoClient` handles all public API endpoints. It does **not** require a private key.

### Initialization
```python
from risex import RiseXInfoClient
info = await RiseXInfoClient.create()
```

> 🔢 **Numeric fields are `Decimal`, not strings.** Prices, sizes and amounts
> across the typed models — market (`last_price`, `mark_price`, `step_size`,
> `step_price`, `max_leverage`, `min_order_size`), position (`size`,
> `quote_amount`), orderbook level (`price`, `quantity`) and trade
> (`price`, `size`, `fee`, `realized_pnl`, `avg_price`, `leverage`) — are parsed
> as `Decimal`. You can compare and compute on them directly
> (`pos.size > 0`, `market.mark_price * 2`, `f"{lvl.price:.2f}"`) with no casting.
> (Identifiers/timestamps like `trade.time` stay strings.)

### 1. Market Data

#### `get_markets()`
Returns a list of all available trading pairs (markets) on RISEx. Results are
cached briefly (`markets_cache_ttl`, default 5s) so repeated lookups and USD
sizing don't hit the network every call; pass `refresh=True` to force a refetch.
```python
markets = await info.get_markets()
for market in markets:
    print(f"[{market.market_id}] {market.display_name} - Mark Price: {market.mark_price}")
```

#### `get_market(market)`
Resolve a single market by numeric id **or** by symbol (case-insensitive,
matched against the display name / base token). Raises `RiseXValidationError`
listing the available symbols if nothing matches.
```python
btc = await info.get_market("BTC")     # same as get_market(1) or get_market("BTC-PERP")
print(btc.market_id, btc.config.step_size)
```

#### `get_orderbook(market_id: int, limit: int = 20)`
Fetches the current orderbook snapshot (`limit` price levels per side).
```python
ob = await info.get_orderbook(market_id=1, limit=10)  # BTC/USDC
print(f"Best Bid: {ob.bids[0].price}, Best Ask: {ob.asks[0].price}")
# each level also carries .order_count; the snapshot has .total_bids / .total_asks
```

### 2. Account Information

#### `get_cross_margin_balance(account_address: str)`
Get the available USDC margin balance for an account.
```python
balance = await info.get_cross_margin_balance("0xYourAddress")
```

#### `get_position(account_address: str, market_id: int)`
Get the open position for an account in a market. Returns `None` if flat.
```python
position = await info.get_position("0xYourAddress", market_id=1)
if position and position.size != 0:   # size is a Decimal — no float() cast needed
    print(f"Size: {position.size}, Side: {position.side}")  # side: 0 = long, 1 = short
else:
    print("No open position")
```

#### `get_all_positions(account_address: str)`
Get all open positions across all markets for a specific account. Each `Position`
carries its `market_id` and is normalized to the same human form as
`get_position` (decimal `size`/`quote_amount`, int `side`). The backend reports
this endpoint in on-chain 18-decimal fixed-point with a string side; the SDK
converts it for you. Extra per-position fields (e.g. `leverage`,
`avg_entry_price`) are scaled and available via `position.model_extra`.
```python
for p in await info.get_all_positions("0xYourAddress"):
    side = "long" if p.side == 0 else "short"
    print(f"market {p.market_id}: {side} {p.size} @ {p.model_extra.get('avg_entry_price')}")
```

#### `get_open_orders(account_address: str)`
Retrieve all currently resting limit orders for an account.
```python
orders = await info.get_open_orders("0xYourAddress")
for order in orders:
    # Use order_id (hex) to cancel; size_steps/price_ticks here are encoded — see the units note above.
    print(f"Order {order.order_id} (resting_order_id={order.resting_order_id})")
```

#### `get_trade_history(account_address: str, limit: int = 50, market_id: int | None = None)`
Get the account's most recent fills (optionally filtered to one market). Each
`TradeRecord` carries `side` ("BUY"/"SELL"), `price`, `size`, `fee`, `time`
(unix **nanoseconds**, as a string), plus `realized_pnl`, `avg_price`,
`liquidity_indicator` ("MAKER"/"TAKER"), and `leverage`.
```python
trades = await info.get_trade_history("0xYourAddress", limit=50)
for t in trades:
    print(f"{t.side} {t.size} @ {t.price}  pnl={t.realized_pnl} ({t.liquidity_indicator})")
```

---

## ⚔️ Trading: `RiseXClient`

The `RiseXClient` is required for any state-modifying actions (trading, changing leverage). It inherits all methods from the `RiseXInfoClient` under `client.info.*`.

### Account Management

#### `update_leverage(market_id: int, leverage: int)`
Updates the account's leverage multiplier for a specific market.
```python
# Set leverage to 10x on BTC/USDC
await client.update_leverage(market_id=1, leverage=10)
```

#### `update_margin_mode(market_id: int, margin_mode: MarginMode | int)`
Change the margin mode. `MarginMode.CROSS == 0`, `MarginMode.ISOLATED == 1`.
```python
from risex import MarginMode

await client.update_margin_mode(market_id=1, margin_mode=MarginMode.ISOLATED)
```
> Setting the mode that's already active reverts on-chain with `MarginModeUnchanged` — catch `RiseXAPIError` if that's expected.

#### `update_isolated_margin(market_id: int, amount: int)`
Add (`amount > 0`) or remove (`amount < 0`) isolated margin for a market. Only
valid in **isolated** mode — in cross mode it reverts with `…BalanceInCross`.
```python
await client.update_isolated_margin(market_id=1, amount=5_000_000)  # add margin
```

#### `deposit(amount: str)`
Notify the backend of a deposit. The actual USDC transfer must be made on-chain
to the router contract separately; this only registers it.
```python
await client.deposit("100")
```

### Placing Orders

> 🎯 **Every method below accepts a market `id` or a `symbol`** ("BTC", "ETH-PERP",
> …) wherever it takes `market_id`. So `client.market_buy("BTC", …)` and
> `client.update_leverage("ETH", 10)` both work — no need to remember numeric ids.

#### USD helpers (the easy way) 💵
Trade in dollars and let the SDK convert to steps/ticks for you. Size is a USD
notional; `price` is a USD price.
```python
# Market buy $100 of BTC
await client.market_buy_usd("BTC", 100)

# Rest a $250 limit sell of ETH at $4,000 (post-only maker)
await client.limit_sell_usd("ETH", 250, 4_000)

# Also: market_sell_usd(market, usd), limit_buy_usd(market, usd, price)
```

#### Lower-level: `size_steps` / `price_ticks`
If you need exact step/tick control, the methods below take integer
`size_steps` and `price_ticks` — see [Units: steps & ticks](#-units-steps--ticks)
for converting from USD.

#### `limit_buy` / `limit_sell`
Place a limit order at a specific price.
```python
# Place a limit buy order of size 0.01 at price 85000
res = await client.limit_buy(
    market_id=1,
    size_steps=1000,     # Amount of tokens (scaled by market step size)
    price_ticks=850000,  # Price (scaled by market tick size)
    post_only=True       # Ensure maker fee
)
print(f"Order placed: {res.order_id}")
```

#### `market_buy` / `market_sell`
Execute an immediate market order.
```python
# Market buy
res = await client.market_buy(market_id=1, size_steps=500)
```

### Risk Management (TP/SL)

> **Side convention:** the order side is expressed with `Side.LONG` (maps to a
> BUY trigger) and `Side.SHORT` (maps to a SELL trigger). To reduce a LONG
> position you submit a SELL, i.e. `Side.SHORT`.

#### `approve_permit_single_budget(budget=MAX_UINT96, expiry_seconds=1yr)`
One-time setup so the exchange operator can submit your TPSL triggers on your
behalf. Signed by your main wallet; returns the approval tx plus fresh
access/refresh tokens. Call it once before placing trigger orders.
```python
await client.approve_permit_single_budget()
```

#### `place_stop_loss(market_id, side, size, stop_price, limit_price)`
Place a Stop-Loss order to protect an open position.
```python
from risex.enums import Side

# If you are LONG on BTC (bought at 90,000), place a SELL stop-loss at 88,000
await client.place_stop_loss(
    market_id=1,
    side=Side.SHORT,   # SELL, to reduce a LONG
    size="0.1",
    stop_price="88000",
    limit_price="87900"
)
```

#### `place_take_profit(market_id, side, size, stop_price, limit_price)`
Place a Take-Profit as a TPSL **trigger** order (taker fee when it fires).
```python
# Take profit for a LONG BTC position at 95,000
await client.place_take_profit(
    market_id=1,
    side=Side.SHORT,   # SELL, to reduce a LONG
    size="0.1",
    stop_price="95000",
    limit_price="95100"
)
```

#### `take_profit_limit(market_id, side, size_steps, price_ticks, post_only=True)`
Take profit as a resting **maker** `reduce_only` limit order — **lower fees** than a
TPSL trigger. Prefer this when you can wait for the price to come to you.
```python
# Maker take-profit on a LONG: rest a SELL reduce_only limit at the target
await client.take_profit_limit(
    market_id=1,
    side=Side.SHORT,       # SELL, to reduce a LONG
    size_steps=1000,
    price_ticks=950000,
)
```

#### `close_position(market_id)`
Automatically calculates your current position size and places a market order in the opposite direction to flatten your exposure entirely.
```python
# Close all open exposure on BTC/USDC
await client.close_position(market_id=1)
```

### Order Cancellation

#### `cancel_order(market_id, order_id, resting_order_id=None)`
Cancel a specific resting order. Pass the hex `order_id` from `place_order` /
`get_open_orders`; the SDK looks up the numeric `resting_order_id` it needs to
sign (pass it explicitly to skip that extra lookup).
```python
res = await client.limit_buy(market_id=1, size_steps=1000, price_ticks=850000)
await client.cancel_order(market_id=1, order_id=res.order_id)
```

#### `cancel_all_orders(market_id)`
Cancel all open orders on a specific market in a single transaction. A market
**must** be specified (the backend has no all-markets cancel — call it per market).
```python
# Cancel all orders on BTC/USDC
await client.cancel_all_orders(market_id=1)
```

---

## 📡 Real-time data: WebSocket

`RiseXWebSocketClient` streams live data over `wss://api.rise.trade/ws/` (built on
`aiohttp` — no extra dependency). It auto-reconnects and replays your
subscriptions, and delivers messages either by `async for` iteration or
per-channel callbacks.

| Channel | Auth | Subscribe with | Payload |
|---|---|---|---|
| `system` | no | `subscribe_system()` | maintenance status |
| `orderbook` | no | `subscribe_orderbook([1,2])` | bids/asks per `market_id` |
| `trades` | no | `subscribe_trades([1])` | fills on the market |
| `oracle` | no | `subscribe_oracle()` | index/mark prices (all markets, 1e18-scaled) |
| `positions` | no¹ | `subscribe_positions([addr],[1])` | position updates for the address |
| `orders` | no¹ | `subscribe_orders([addr],[1])` | order lifecycle for the address |
| `account`, `fills` | **yes²** | `subscribe_account()`, `subscribe_fills()` | balance/equity / your fills |

¹ `positions`/`orders` are filtered by maker address and need no login (same as the
REST by-address reads). ² `account`/`fills` require an `auth_v2` sign-in — one call,
see [below](#authenticating-account--fills).

```python
from risex import RiseXWebSocketClient

async with RiseXWebSocketClient() as ws:
    await ws.subscribe_orderbook([1])          # BTC/USDC book
    await ws.subscribe_trades([1])
    await ws.subscribe_positions([my_address], [1])

    async for msg in ws:                        # msg is a WSMessage
        if msg.is_ack or msg.is_error:
            continue
        if msg.channel == "orderbook":
            best_ask = msg.data["asks"][0]
            print(f"[{msg.market_id}] best ask {best_ask['price']} x {best_ask['quantity']}")
        elif msg.channel == "positions":
            print("position update:", msg.data)
```

Each message is a `WSMessage` with `channel`, `type` (`"snapshot"`/`"update"`/…),
`market_id`, `block_number`, and `data` (the channel payload — a dict or list).
Prefer callbacks for a long-running bot:

```python
ws = RiseXWebSocketClient()
ws.on("trades", lambda m: print("trade", m.data))
ws.on("orders", on_my_order)        # sync or async handlers
await ws.connect()
await ws.subscribe_trades([1])
await ws.subscribe_orders([my_address], [1])
await ws.run_forever()
```

### Authenticating `account` / `fills`

These two channels need an `auth_v2` sign-in: the SDK fetches a single-use nonce,
signs it (EIP-712 `RegisterV2`) with your session signer, and re-authenticates
automatically on reconnect. With a `RiseXClient` it's one call:

```python
ws = await client.connect_websocket()   # already authenticated with the session signer
await ws.subscribe_fills()
await ws.subscribe_account([client.account_address])
async for msg in ws:
    if msg.channel == "fills":
        print("fill:", msg.data)        # side, price, size, fee, liquidity_indicator, ...
```

Or authenticate a standalone `RiseXWebSocketClient` yourself with an info client
and a signer key (your "API key"):

```python
info = await RiseXInfoClient.create()
ws = await RiseXWebSocketClient().connect()
await ws.authenticate(account_address, signer_key, info)
await ws.subscribe_fills()
```

`info.get_signers(account)` lists the registered signers (label, expiry, status)
if you need to pick or check one. Without `authenticate()`,
`subscribe_account`/`subscribe_fills` return an `authentication required` error (a
`WSMessage` with `is_error`); every other channel works without it.

---

## 🔀 Concurrency & nonces

By default each signed action (`place_order`, `cancel_order`, `cancel_all_orders`)
fetches the account nonce first, which adds a round-trip. To submit several
actions concurrently — or to skip that fetch — pass a pre-fetched `NonceState`
via the `nonce` argument:

```python
nonce = await client.get_nonce_state()
await asyncio.gather(
    client.place_order(market_id=1, side=Side.LONG, size_steps=100, nonce=nonce),
    client.place_order(market_id=2, side=Side.SHORT, size_steps=50, nonce=nonce),
)
```

> Each signed action consumes a nonce slot on-chain, so when firing many at once
> you are responsible for advancing/spacing nonces to avoid collisions.

### Logging

The SDK logs requests (and, at `DEBUG`, payloads) under the `risex.http` and
`risex.client` loggers — they're silent unless you enable them:

```python
import logging
logging.getLogger("risex").setLevel(logging.DEBUG)
```

---

## 🚨 Error Handling

All API errors and smart contract reverts throw a `RiseXAPIError` exception, which includes the HTTP status code, request path, and decoded error message from the EVM.

```python
from risex.exceptions import RiseXAPIError

try:
    await client.market_buy(market_id=1, size_steps=100000000)
except RiseXAPIError as e:
    print(f"Failed! Status: {e.status}")
    print(f"Message: {e.message}") # e.g. 'CancelOrder reverted: SignerNotAuthorized'
```

---

## 📄 License

This SDK is distributed under the MIT License. See `LICENSE` for more information.

> **Disclaimer**: This SDK interacts with live decentralized financial infrastructure. High-frequency trading carries significant financial risk. The authors are not responsible for any financial losses or unexpected smart contract behavior resulting from the use of this software. Always test thoroughly using small amounts before deploying to production.
