# Monaco SDK
Typed Rust client for the Monaco DEX REST API, auto-generated at build time from the vendored OpenAPI spec via [progenitor](https://docs.rs/progenitor). The `build.rs` reads `rust-sdk/openapi/openapi.yaml`, generates typed Rust code, and includes it via `include!`.
## Code Generation
```mermaid
graph LR
YAML[openapi/openapi.yaml] --> BS[build.rs]
BS --> Gen[progenitor::Generator]
Gen --> OUT["$OUT_DIR/api.rs"]
OUT --> Lib["src/lib.rs<br/>include!()"]
Lib --> Client[monaco_sdk::Client]
```
## Quick Start
```toml
[dependencies]
monaco-sdk = "0.5"
tokio = { version = "1", features = ["full"] }
```
```rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = monaco_sdk::Client::new("https://staging.apimonaco.xyz");
let pairs = client.list_trading_pairs(None, Some(true), None, None, None, None).await?.into_inner();
println!("{:?}", pairs.data);
Ok(())
}
```
## Authentication
Monaco uses **noncustodial session-key auth**. The client generates a random
ed25519 keypair locally and registers the public key through the
challenge → wallet-sign → verify flow; the wallet signature authorizes that
session public key. Subsequent authenticated requests are signed with the
session **private** key — the server never holds a credential that can
impersonate the user.
```rust
use monaco_sdk::types::{ChallengeRequest, VerifyRequest};
let client = monaco_sdk::Client::new("https://staging.apimonaco.xyz");
// 1. Generate an ed25519 session keypair locally (e.g. via ed25519-dalek) and
// hex-encode the public key.
let session_public_key = "<64-char-lowercase-hex-ed25519-pubkey>".to_string();
// 2. Request a challenge (binds the session public key into the signed message)
let challenge = client
.create_challenge(&ChallengeRequest {
address: "0xYourAddress".parse().unwrap(),
chain_id: None,
client_id: None,
session_public_key: session_public_key.parse().unwrap(),
})
.await?
.into_inner();
// 3. Sign the challenge message with your wallet and verify
let verify = client
.verify_signature(&VerifyRequest {
address: "0xYourAddress".parse().unwrap(),
chain_id: None,
client_id: None,
nonce: challenge.nonce.unwrap().parse().unwrap(),
signature: "<wallet-signed-message>".parse().unwrap(),
session_public_key: session_public_key.parse().unwrap(),
})
.await?
.into_inner();
// `verify` returns { expires_at, user } — no tokens.
```
Authenticated requests must carry per-request signature headers
(`X-Monaco-PublicKey`, `X-Monaco-Timestamp`, `X-Monaco-Signature`) over
`METHOD\nPATH_WITH_QUERY\nTIMESTAMP_MS\nSHA256_BODY_HEX`, signed with the
session private key. A built-in request-signing layer for this generated client
is a pending follow-up; until then, inject those headers via your own reqwest
layer when calling authenticated endpoints.
## API Coverage
The client exposes every operation in the canonical Monaco REST API. The
vendored spec is resynced from that canonical spec by `make docs-gen` (see
[Gotchas](#gotchas)), so this surface tracks the API automatically.
| Category | Methods |
|----------|---------|
| **Market data** | `list_trading_pairs`, `get_trading_pair_by_id`, `get_market_metadata`, `get_candles`, `get_screener`, `get_market_stats`, `get_open_interest` |
| **Perp markets** | `get_perp_market_config`, `get_perp_market_summary`, `get_mark_price`, `get_index_price`, `get_funding_state`, `list_funding_history` |
| **Orderbook** | `get_orderbook_snapshot` |
| **Trades** | `get_trades`, `get_trade_by_id`, `get_user_trades` |
| **Orders** | `create_order`, `replace_order`, `cancel_order`, `get_orders`, `get_order_by_id` |
| **Conditional orders** | `cancel_conditional_order`, `list_conditional_orders` |
| **Batch orders** | `batch_create_orders`, `batch_replace_orders`, `batch_cancel_orders`, `batch_cancel_all`, `batch_cancel_all_by_pair` |
| **Accounts** | `get_user_profile`, `get_user_balances`, `get_user_balance_by_asset`, `get_user_movements`, `list_funding_payments`, `get_portfolio_stats`, `get_portfolio_chart` |
| **Sub-accounts** | `list_sub_accounts_with_balances`, `create_sub_account_limit`, `get_sub_account_limits`, `update_sub_account_limit`, `delete_sub_account_limit` |
| **Margin accounts** | `list_margin_accounts`, `get_margin_account_summary`, `get_parent_margin_account_summary`, `get_margin_account_movements`, `get_parent_margin_account_movements`, `get_available_collateral`, `transfer_collateral_to_margin_account`, `transfer_collateral_from_margin_account`, `transfer_collateral_to_parent_margin_account`, `transfer_collateral_from_parent_margin_account`, `transfer_collateral_to_risk_bucket`, `simulate_order_risk`, `simulate_parent_margin_order_risk`, `simulate_risk_bucket_order_risk` |
| **Positions** | `list_positions`, `get_position`, `close_position`, `batch_close_all_positions`, `get_position_risk`, `add_position_margin`, `reduce_position_margin`, `list_position_history`, `attach_position_tp_sl`, `get_position_pnl_history` |
| **Delegated agents** | `upsert_delegated_agent`, `list_delegated_agents`, `list_delegated_agent_owners`, `revoke_delegated_agent`, `create_delegated_session` |
| **Applications** | `get_application_config`, `get_application_stats`, `list_application_orders`, `list_application_users`, `list_application_movements`, `list_application_balances` |
| **Auth** | `create_challenge`, `verify_signature`, `refresh_session`, `revoke_session`, `authenticate_backend` |
| **Fees** | `simulate_fees`, `get_my_fee_tier` |
| **Other** | `submit_whitelist`, `mint_tokens`, `health_check` |
All request/response types are in `monaco_sdk::types`.
## Gotchas
- **Generated code**: The entire client is generated at build time. The vendored `openapi/openapi.yaml` is **not** hand-edited — it is regenerated from the canonical spec (`docs/src/proto-openapi/api/openapi.yaml`) by `make docs-gen`, which runs `openapi-postprocess`'s `sync-rust-sdk-spec` transform. CI fails if the committed copy drifts from a fresh regeneration, so a proto/endpoint change reaches this SDK automatically.
- **No runtime codegen**: The spec is included via `include!` from `OUT_DIR`, so builds require the OpenAPI YAML to be present.
## Build & Test
By default `cargo nextest run -p monaco-sdk` runs only the static `packaging_regressions`
checks — the rest of the suite requires a live Monaco backend and is gated
behind `#[ignore]`.
```bash
# Static checks only (no backend needed)
cargo nextest run -p monaco-sdk
# Full live suite against an environment
API_BASE_URL=https://staging.apimonaco.xyz \
PRIVATE_KEY=0x...your_funded_test_key \
cargo nextest run -p monaco-sdk --run-ignored all --test-threads=1
```
The live tests are grouped by responsibility and each file can be targeted
individually (`--test auth`, `--test faucet`, etc.):
| File | Covers |
|------|--------|
| `public.rs` | Unauthenticated market data: `list_trading_pairs`, `get_orderbook_snapshot`, `get_candles`, `get_trades`, `health_check`, … |
| `auth.rs` | Challenge / verify / refresh round-trip, plus 401 rejection on an unauthenticated profile call |
| `accounts.rs` | `get_user_balances`, `get_user_movements`, `simulate_fees` |
| `faucet.rs` | `mint_tokens` contract (minted entries carry asset IDs, amounts, tx hashes) and rate-limit slot accounting |
| `sub_accounts.rs` | `list_sub_accounts_with_balances` + 4xx envelope on an unknown sub-account |
| `orders.rs` | Full order lifecycle (create / replace / cancel), batch create/cancel |
| `release_smoke.rs` | Ignored runtime smoke journeys: fresh wallet onboarding (challenge → verify → faucet → simulate fees) and funded wallet order lifecycle |
For published-crate smoke testing, the sibling
[`sdk-release-verify`](../sdk-release-verify/) harness consumes
`monaco-grpc-sdk` from crates.io so it exercises the crate an external
`cargo add` user would see.