monaco-sdk 1.0.0

Typed Rust client for the Monaco REST API — generated from the OpenAPI specification
Documentation
use alloy::hex;
use alloy::signers::local::PrivateKeySigner;
use ed25519_dalek::SigningKey;
use std::sync::Once;

const DEFAULT_BASE_URL: &str = "http://localhost:8080";

static INIT: Once = Once::new();

fn load_env() {
    INIT.call_once(|| {
        let _ = dotenvy::dotenv();
    });
}

pub fn base_url() -> String {
    load_env();
    std::env::var("API_BASE_URL").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string())
}

pub fn client() -> monaco_sdk::Client {
    monaco_sdk::Client::new(&base_url())
}

/// Build a client for authenticated calls.
///
/// NOTE: under the noncustodial session-key scheme, every authenticated request
/// must carry per-request `X-Monaco-*` ed25519 signature headers. The generated
/// progenitor client does not yet support per-request signing middleware, so
/// this helper currently returns a plain client. Authenticated release smoke tests
/// therefore require the rust-sdk request-signing layer (tracked as a
/// follow-up) and a live gateway to pass.
pub fn authed_client(_session_private_key_hex: &str) -> monaco_sdk::Client {
    monaco_sdk::Client::new(&base_url())
}

pub async fn authenticate() -> (String, String, PrivateKeySigner) {
    let signer = PrivateKeySigner::random();
    authenticate_with_signer(signer).await
}

/// Authenticate with the funded wallet from `PRIVATE_KEY` env var.
/// Required for tests that create orders (need balance).
pub async fn authenticate_funded() -> (String, String, PrivateKeySigner) {
    load_env();
    let key = std::env::var("PRIVATE_KEY")
        .expect("PRIVATE_KEY env var required for order tests — set it in .env at repo root");
    let signer: PrivateKeySigner = key.parse().unwrap();
    authenticate_with_signer(signer).await
}

/// Authenticate with a fresh wallet and fund it from the faucet. Returns the
/// session private key (hex), address, signer, and the mint response — a new
/// wallet is created on every call so callers are not tripped up by the 24h
/// per-address faucet rate limit.
pub async fn authenticate_and_faucet() -> (
    String,
    String,
    PrivateKeySigner,
    monaco_sdk::types::MintTokensResponse,
) {
    let (session_key, address, signer) = authenticate().await;
    let authed = authed_client(&session_key);
    let mint = authed
        .mint_tokens()
        .await
        .expect("faucet mint_tokens failed")
        .into_inner();
    (session_key, address, signer, mint)
}

/// Runs the wallet challenge → verify flow, registering a fresh ed25519 session
/// public key. Returns (`session_private_key_hex`, address, `wallet_signer`).
async fn authenticate_with_signer(signer: PrivateKeySigner) -> (String, String, PrivateKeySigner) {
    let c = client();
    let address = format!("{:?}", signer.address());

    // Locally-generated ed25519 session keypair — the server only sees the public key.
    let session_key = SigningKey::from_bytes(&rand::random::<[u8; 32]>());
    let session_public_key = hex::encode(session_key.verifying_key().to_bytes());

    let challenge = c
        .create_challenge(&monaco_sdk::types::ChallengeRequest {
            address: address.parse().unwrap(),
            chain_id: None,
            client_id: None,
            session_public_key: session_public_key.parse().unwrap(),
        })
        .await
        .unwrap()
        .into_inner();

    let message = challenge.message.unwrap();
    let nonce = challenge.nonce.unwrap();

    let signature = alloy::signers::Signer::sign_message(&signer, message.as_bytes())
        .await
        .unwrap();
    let sig_hex = format!("0x{}", hex::encode(signature.as_bytes()));

    c.verify_signature(&monaco_sdk::types::VerifyRequest {
        address: address.parse().unwrap(),
        chain_id: None,
        client_id: None,
        nonce: nonce.parse().unwrap(),
        referral_code: None,
        signature: sig_hex.parse().unwrap(),
        session_public_key: session_public_key.parse().unwrap(),
    })
    .await
    .unwrap()
    .into_inner();

    (hex::encode(session_key.to_bytes()), address, signer)
}

pub async fn first_pair_id() -> String {
    let pairs = client()
        .list_trading_pairs(None, None, Some(true), None, None, None, None)
        .await
        .unwrap()
        .into_inner();

    pairs
        .trading_pairs
        .unwrap()
        .into_iter()
        .next()
        .unwrap()
        .id
        .unwrap()
        .to_string()
}