lightcone 0.3.0

Rust SDK for the Lightcone Protocol — unified native + WASM client
Documentation

Lightcone SDK v2

Unified Rust SDK for the Lightcone protocol. Supports both native and WASM targets under a single crate with compile-time feature dispatch.

Why v2?

The v1 SDK (deps/sdk/rust/) was built as a native-first Rust library with three independent modules (program, api, websocket). It worked well for CLI tooling and server-side use, but introduced significant friction when the Dioxus fullstack app needed to use it on both native (server) and WASM (browser) targets simultaneously.

Problems with v1

  • Not WASM-compatible. The API client used tokio::sync::RwLock, tokio::time::sleep, and reqwest features (.timeout(), .is_connect(), cookies) that don't compile to wasm32-unknown-unknown. The WebSocket client was tokio-tungstenite-only.
  • No domain types. v1 only exposed raw wire types (REST response structs). The app had to define its own domain types (Market, Order, OrderBookPair, etc.) in src/domain/, implement all TryFrom conversions, validation logic, and business methods. This duplicated work that belongs in the SDK.
  • No WebSocket message types. The app defined its own MessageIn, MessageOut, Kind, SubscribeParams, and Subscription types in src/service/ws/. These should be SDK-provided so other consumers get the same typed WS interface.
  • Flat client API. All endpoints were top-level methods on a single LightconeApiClient struct, making it hard to manage per-domain caching or organize related operations.
  • All-or-nothing retry. Retry was a global setting. Non-idempotent POSTs (order submission, cancellation) were retried the same as GETs, risking duplicate actions.
  • No auth abstraction. Auth was a standalone function that returned a raw JWT string. No credential management, no platform-aware token handling, no logout flow.

What v2 adds

  • Unified native + WASM support. Compile-time cfg dispatch for platform-specific code (async locks, timers, HTTP features, WebSocket transport). One crate, two targets, zero runtime overhead.
  • Rich domain types with validation. Market, Order, OrderBookPair, Outcome, ConditionalToken, DepositAsset, TokenBalance, Portfolio, etc. — all migrated from the app with their TryFrom conversions and business logic.
  • Wire types as a public secondary API. Raw serde structs for REST and WS are public under domain::*/wire for consumers who need raw access (server functions, debugging), but domain types are the primary interface.
  • Typed WebSocket messages. MessageIn, MessageOut, Kind enum, SubscribeParams, UnsubscribeParams, and Subscription trait — all SDK-provided with full channel coverage (book, trades, user, price_history, ticker, market).
  • Nested sub-client API. client.markets().get_by_slug(...), client.orderbooks().decimals(...), client.orders().submit(...) — organized by domain for clean ergonomics. The SDK is stateless for HTTP data; caching is the consumer's responsibility.
  • Granular retry policies. RetryPolicy::Idempotent for GETs, RetryPolicy::None for non-idempotent POSTs, RetryPolicy::Custom for anything else. Backoff + jitter, 429 Retry-After support.
  • Secure auth with platform-aware token handling. HTTP-only cookies on WASM (SDK never touches the token), internal private storage on native (never exposed via public API). Logout calls the backend endpoint to clear cookies server-side.
  • App-owned WS state containers. OrderbookSnapshot, UserOpenOrders, TradeHistory, PriceHistoryState — standalone types with update methods that the app wraps in its own reactive state (e.g. Dioxus Signal). Avoids the RwLock vs reactive signal mismatch.
  • On-chain program module carried over from v1. Instructions, orders, PDAs, accounts — unchanged.

Coexistence with v1

Both SDKs can be imported simultaneously for incremental migration:

use lightcone_sdk::prelude::*;     // v1 — existing code
use lightcone::prelude::*;  // v2 — new code

The v2 crate is named lightcone in Cargo.toml and imported as a non-optional dependency. Migrate domain-by-domain, then drop v1.

Architecture

The SDK is organized in five layers:

Layer Module Purpose
1 shared, domain, program Core types, domain models, on-chain program logic. Always available, WASM-safe.
2 auth Message generation + platform-dependent signing.
3 http LightconeHttp — low-level HTTP client with per-endpoint retry policies.
4 ws WebSocket — compile-time dispatch: tokio-tungstenite (native) / web-sys (WASM).
5 client LightconeClient — high-level nested sub-client API with caching.

Module Layout

src/
  lib.rs               # Public re-exports + prelude
  client.rs            # LightconeClient, LightconeClientBuilder, sub-client accessors
  error.rs             # SdkError, HttpError, WsError, AuthError
  network.rs           # DEFAULT_API_URL, DEFAULT_WS_URL
  shared/
    mod.rs             # OrderBookId, PubkeyStr, Side, Resolution, SubmitOrderRequest
    scaling.rs         # Price/size → raw lamport conversion
    price.rs           # Decimal formatting utilities
    fmt/               # Decimal display, number abbreviation (num.rs, decimal.rs)
    serde_util.rs      # Serde helpers
  domain/
    mod.rs
    market/
      mod.rs           # Market, Status, ValidationError
      client.rs        # Markets sub-client (get, get_by_slug, search, featured, cache)
      outcome.rs       # Outcome, OutcomeValidationError (sub-entity of market)
      tokens.rs        # Token trait, ConditionalToken, DepositAsset, ValidatedTokens, TokenMetadata + TryFrom
      wire.rs          # MarketResponse, OutcomeResponse, DepositAssetResponse, etc.
      convert.rs       # TryFrom<MarketResponse> for Market
    orderbook/
      mod.rs           # OrderBookPair, OrderBookValidationError, OutcomeImpact
      client.rs        # Orderbooks sub-client (get, decimals, cache)
      ticker.rs        # TickerData (best bid/ask/mid)
      wire.rs          # OrderbookResponse, DecimalsResponse, BookOrder, etc.
      state.rs         # OrderbookSnapshot with apply(), bids(), asks(), best_bid()
      convert.rs       # TryFrom<(OrderbookResponse, &[ConditionalToken])> for OrderBookPair
    order/
      mod.rs           # Order, OrderType, OrderStatus
      client.rs        # Orders sub-client (submit, cancel, cancel_all, get_user_orders)
      state.rs         # UserOpenOrders (app-owned state container)
      wire.rs          # OrderUpdate, UserSnapshot, UserUpdate, AuthUpdate + WS balance types
      convert.rs       # From<OrderUpdate/UserSnapshotOrder> for Order
    trade/
      mod.rs           # Trade
      client.rs        # Trades sub-client (get)
      state.rs         # TradeHistory (rolling buffer, app-owned)
      wire.rs          # TradeResponse, WsTrade, TradesResponse
      convert.rs       # From<TradeResponse/WsTrade> for Trade
    price_history/
      mod.rs           # LineData
      client.rs        # PriceHistoryClient sub-client (get)
      state.rs         # PriceHistoryState (app-owned state container)
      wire.rs          # PriceHistory snapshot/update (WS)
    position/
      mod.rs           # Portfolio, Position, PositionOutcome, WalletHolding, TokenBalance types
      client.rs        # Positions sub-client (get, get_for_market)
      wire.rs          # PositionsResponse, PositionResponse (REST)
    admin/
      mod.rs           # AdminEnvelope (domain-level signed envelope)
      client.rs        # Admin sub-client (upsert_metadata)
      wire.rs          # UnifiedMetadataRequest/Response, *MetadataPayload types
  program/             # On-chain: instructions, orders, PDAs, accounts (from v1)
  auth/
    mod.rs             # generate_signin_message, AuthCredentials, LoginRequest/Response
    client.rs          # Auth sub-client (login_with_message, logout, credentials)
    native.rs          # KeypairAuth — sign_login_message (native-auth feature)
  http/
    mod.rs
    client.rs          # LightconeHttp (internal)
    retry.rs           # RetryPolicy, RetryConfig
  ws/
    mod.rs             # MessageIn, MessageOut, Kind, WsEvent, WsConfig
    subscriptions.rs   # SubscribeParams, UnsubscribeParams, Subscription trait
    native.rs          # tokio-tungstenite (ws-native feature) [TODO: stub]
    wasm.rs            # web-sys WebSocket (ws-wasm feature) — full implementation

Features

Feature What it enables
http (default) HTTP client (Layers 1-3). Works on both native and WASM.
native-auth Keypair-based authentication (Layer 2 native).
ws-native WebSocket via tokio-tungstenite (Layer 4 native).
ws-wasm WebSocket via web-sys::WebSocket (Layer 4 WASM).
solana-rpc solana-client for on-chain reads (native only).
native Bundle: http + native-auth + ws-native + solana-rpc. For CLI/server use.
wasm Bundle: http + ws-wasm. For browser use.

Quick Start

use lightcone::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = LightconeClient::builder()
        .base_url("https://tapi.lightcone.xyz")
        .build()?;

    // Nested sub-client API
    let featured = client.markets().featured().await?;
    let market = client.markets().get_by_slug("some-market").await?;
    let orderbook = client.orderbooks().get("orderbook_id", Some(10)).await?;
    let decimals = client.orderbooks().decimals("orderbook_id").await?;

    Ok(())
}

Retry Strategy

  • GET requests: RetryPolicy::Idempotent by default — retries on transport failures + 502/503/504, backs off on 429.
  • POST /orders/submit, /orders/cancel, /orders/cancel-all: RetryPolicy::None by default.
  • POST /auth/login: RetryPolicy::None.
  • All retry policies are overridable per-call.

Rule of thumb: Retry transport failures and status checks; don't retry "actions" unless you have idempotency guarantees.

Domain Types vs Wire Types

The SDK exposes two levels of types:

  • Domain types (lightcone::domain::*) — Rich, validated types with business logic methods. These are the primary public API. Example: Market, Order, OrderBookPair.
  • Wire types (lightcone::domain::*/wire) — Raw serde structs matching backend REST responses and WS messages. Also public for consumers who need raw access (forwarding data, debugging, server functions).

Shared newtypes like OrderBookId, PubkeyStr, Side, and Resolution are used directly in wire types because they are serialization-transparent — they deserialize identically to the raw format the backend sends.

State Management

HTTP: Stateless (consumer owns caching)

The SDK is intentionally stateless for HTTP data. Sub-clients are thin, typed wrappers over the REST API — they fetch, convert wire → domain types, and return. No market cache, no slug index, no TTLs.

Why? Different consumers want radically different caching strategies:

  • Dioxus server functions use static LazyLock<Mutex<Cache>> with 1-hour TTLs
  • CLI tools may want no caching at all
  • Admin dashboards may want short TTLs with manual invalidation

The SDK shouldn't pick one strategy. The consumer knows best.

The only exception: orderbooks().decimals() — orderbook decimals are effectively immutable. This is a pure memoization, not a caching policy.

WS-Driven Live State (app-owned)

The SDK does NOT store WS-driven state internally. Instead it provides standalone state container types with update methods:

  • OrderbookSnapshotapply() for book snapshots/deltas, best_bid(), best_ask(), mid_price()
  • UserOpenOrdersupsert(), remove() for order updates
  • TradeHistory — rolling buffer with push()
  • PriceHistoryStateapply_snapshot(), apply_update()

The app instantiates these, wraps them in its own reactive state (e.g. Dioxus Signal), and calls SDK update methods when WS events arrive. This avoids the RwLock vs reactive signal mismatch.

Authentication — CRITICAL Security Model

This section is mandatory reading for any SDK implementation in any language.

Wasm/Browser

  • Token lives ONLY in the HTTP-only cookie set by the backend.
  • The SDK never reads, stores, or exposes the token programmatically.
  • Authenticated requests work because the browser auto-includes cookies.
  • Browser SDKs MUST use HTTP-only cookies — never store tokens in localStorage, sessionStorage, or any JS/WASM-accessible location.

Native/CLI

  • The SDK stores the token internally (private field) and injects it as a Cookie: auth_token=<token> header, matching the backend's cookie-only auth model.
  • Token is NEVER exposed via public API — no .token() accessor.
  • AuthCredentials only exposes: user_id, wallet_address, expires_at, is_authenticated().
  • We manually inject the cookie rather than using reqwest's cookie_store(true) because the backend hardcodes Domain=.lightcone.xyz on the Set-Cookie, which would break local development (requests to localhost wouldn't match the domain).

Nonce-Based Sign-In (Native)

Login uses a server-issued, single-use nonce to prevent signature replay attacks. The full flow:

  1. Fetch a nonce from the server (GET /api/auth/nonce).
  2. Build the sign-in message embedding the nonce.
  3. Sign the message with the local keypair.
  4. Submit the signed message to log in.
use lightcone::auth::native::sign_login_message;
use solana_keypair::Keypair;

// 1. Fetch a single-use nonce (5-minute TTL, consumed on login)
let nonce = client.auth().get_nonce().await?;

// 2-3. Build message + sign with keypair
let signed = sign_login_message(&keypair, &nonce);

// 4. Authenticate
let user = client.auth().login_with_message(
    &signed.message,
    &signed.signature_bs58,
    &signed.pubkey_bytes,
    None, // use_embedded_wallet
).await?;

The nonce is validated and deleted server-side on login -- each nonce can only be used once. If you need to retry a failed login, fetch a new nonce.

Logout

On both platforms, client.auth().logout():

  1. Calls POST /api/auth/logout to clear the HTTP-only cookie server-side.
  2. On native: clears the internal token.
  3. Clears all sub-client HTTP caches.

Client-side clearing alone is insufficient for cookie-based auth — the backend must be told to invalidate.

Backend API Alignment

The SDK aligns with lightcone-backend API routes:

SDK Method Backend Route HTTP Method
markets().get(cursor, limit) /api/markets GET
markets().get_by_slug(slug) /api/markets/by-slug/{slug} GET
markets().get_by_pubkey(pubkey) /api/markets/{market_pubkey} GET
markets().search(q, limit) /api/markets/search/by-query/{query} GET
markets().featured() /api/markets/search/featured GET
orderbooks().get(id, depth) /api/orderbook/{id} GET
orderbooks().decimals(id) /api/orderbooks/{id}/decimals GET
orders().submit(req) /api/orders/submit POST
orders().cancel(req) /api/orders/cancel POST
orders().cancel_all(req) /api/orders/cancel-all POST
orders().get_user_orders(req) /api/users/orders POST
positions().get(pubkey) /api/users/{pubkey}/positions GET
positions().get_for_market(pubkey, market) /api/users/{pubkey}/markets/{market_pubkey}/positions GET
trades().get(id, limit, before) /api/trades GET
price_history().get(...) /api/price-history GET
admin().upsert_metadata(env) /api/admin/metadata POST
auth().get_nonce() /api/auth/nonce GET
auth().login_with_message(...) /api/auth/login_or_register_with_message POST
auth().check_session() /api/auth/me GET
auth().logout() /api/auth/logout POST
auth().disconnect_x() /api/auth/disconnect_x POST
auth().connect_x(...) /api/auth/connect_x POST
privy().sign_and_send_tx(...) /api/privy/sign_and_send_tx POST
privy().sign_and_send_order(...) /api/privy/sign_and_send_order POST
privy().export_wallet(...) /api/privy/wallet/export POST

Canonical Shared Types

The SDK defines the canonical shared types for auth and user profiles:

  • LinkedAccount, LinkedAccountType, ChainType — linked identity types
  • EmbeddedWallet — Privy-managed embedded wallet
  • User — full user profile (returned by login_with_message and check_session)
  • AuthCredentials — session state (user_id, wallet_address, expires_at)

These types are the source of truth. Consumers (Dioxus app, CLI tools) should use SDK types directly rather than defining local equivalents.

OAuth Authentication

OAuth login (Google, X/Twitter) is a browser redirect flow handled entirely by the Lightcone backend -- it is not an SDK method call. The client navigates the browser to the appropriate backend URL, where the full OAuth exchange takes place. On completion, the backend sets an auth_token HTTP-only cookie and redirects back to the frontend.

Flow URL
Login with Google GET {backend}/api/auth/oauth/google
Login with X GET {backend}/api/auth/oauth/x
Link X account GET {backend}/api/auth/oauth/link/x (requires existing session)

Because OAuth requires pre-registered redirect URIs, these endpoints only function on domains configured in the provider's developer console (e.g., the Lightcone domains). After the redirect completes, call check_session() to hydrate the authenticated user profile.

Native and CLI clients authenticate via get_nonce() + login_with_message() with a Solana wallet signature and do not use the OAuth flow. See Nonce-Based Sign-In (Native) above.

Privy Embedded Wallet

The client.privy() sub-client wraps the backend's Privy RPC endpoints for embedded wallet operations. All methods require an active authenticated session.

Embedded wallets are provisioned during login by passing use_embedded_wallet: true to login_with_message(). This works on any platform -- WASM, native, or CLI. Once provisioned, the wallet is tied to the user's account and all Privy sub-client methods are available to any authenticated client.

Native and CLI clients that sign transactions directly with their own keypair typically do not need an embedded wallet, but provisioning and using one is fully supported.

// Sign and send a Solana transaction via embedded wallet
let result = client.privy().sign_and_send_tx("wallet_id", "base64_tx").await?;

// Sign an order hash and submit to the exchange engine
let result = client.privy().sign_and_send_order("wallet_id", order).await?;

// Export embedded wallet private key (HPKE encrypted)
let export = client.privy().export_wallet("wallet_id", "decode_pubkey_base64").await?;

The backend handles all Privy API interaction -- the SDK never touches Privy directly.

WebSocket Channels

Channel Subscribe Events
book SubscribeParams::Books { orderbook_ids } Kind::BookUpdate — snapshot + delta
trades SubscribeParams::Trades { orderbook_ids } Kind::Trade
user SubscribeParams::User Kind::User — snapshot, order_update, balance_update
price_history SubscribeParams::PriceHistory { orderbook_id, resolution } Kind::PriceHistory — snapshot + update
ticker SubscribeParams::Ticker { orderbook_ids } Kind::Ticker — best bid/ask/mid
market SubscribeParams::Market { market_pubkey } Kind::Market — settled, created, opened, paused, orderbook_created

WS book_update uses conditional_pair: "market_pubkey:orderbook_id" (combined string) — wire type parses/splits this.