lightcone 0.7.1

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

Lightcone SDK

Rust SDK for the Lightcone impact market protocol on Solana.

Table of Contents

Installation

Add to your Cargo.toml:

[dependencies]
lightcone = { version = "0.7.0", features = ["native"] }

For browser/WASM targets:

[dependencies]
lightcone = { version = "0.7.0", features = ["wasm"] }

Feature Flags

Feature What it enables Use case
native http + native-auth + ws-native + solana-rpc Market makers, bots, CLI tools
wasm http + ws-wasm Browser applications
trigger_orders Stop-limit & take-profit-limit order types, envelope, state Under development — not yet available. For internal use only.

Quick Start

use lightcone::prelude::*;
use lightcone::auth::native::sign_login_message;
use solana_keypair::Keypair;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Defaults to Prod. Use .env(LightconeEnv::Staging) for staging.
    let client = LightconeClient::builder()
        .deposit_source(DepositSource::Market)
        .build()?;
    let keypair = Keypair::new();

    // 1. Authenticate
    let nonce = client.auth().get_nonce().await?;
    let signed = sign_login_message(&keypair, &nonce);
    let user = client.auth().login_with_message(
        &signed.message,
        &signed.signature_bs58,
        &signed.pubkey_bytes,
        None,
    ).await?;

    // 2. Find a market
    let market = client.markets().get(None, Some(1)).await?.markets.into_iter().next().unwrap();
    let orderbook = &market.orderbook_pairs[0];

    // 3. Deposit collateral to the global pool
    let deposit_mint = market.deposit_assets[0].pubkey().to_pubkey()?;
    let deposit_ix = client.positions().deposit().await
        .user(keypair.pubkey())
        .mint(deposit_mint)
        .amount(1_000_000)
        .build_ix()
        .await?;

    // 4. Build, sign, and submit a limit order
    let request = client.orders().limit_order().await
        .maker(keypair.pubkey())
        .bid()
        .price("0.55")
        .size("100")
        .sign(&keypair, &orderbook)?;

    let response = client.orders().submit(&request).await?;
    println!("Order submitted: {:?}", response);

    // 5. Withdraw from the global pool
    let withdraw_ix = client.positions().withdraw().await
        .user(keypair.pubkey())
        .mint(deposit_mint)
        .amount(1_000_000)
        .build_ix()
        .await?;

    // 6. Stream real-time updates
    let mut ws = client.ws_native();
    ws.connect().await?;
    ws.subscribe(SubscribeParams::Books {
        orderbook_ids: vec![orderbook.orderbook_id.clone()],
    })?;

    Ok(())
}

Start Trading

use lightcone::prelude::*;
use solana_keypair::read_keypair_file;
use solana_signer::Signer;

// Defaults to Prod. Use .env(LightconeEnv::Staging) for staging.
let client = LightconeClient::builder()
    .deposit_source(DepositSource::Market)
    .build()?;
let keypair = read_keypair_file("~/.config/solana/id.json")?;

Step 1: Find a Market

let market = client.markets().get(None, Some(1)).await?.markets.into_iter().next().unwrap();
let orderbook = market
    .orderbook_pairs
    .iter()
    .find(|pair| pair.active)
    .or_else(|| market.orderbook_pairs.first())
    .expect("market has no orderbooks");

Step 2: Deposit Collateral

let deposit_mint = market.deposit_assets[0].pubkey().to_pubkey()?;
let deposit_ix = client.positions().deposit().await
    .user(keypair.pubkey())
    .mint(deposit_mint)
    .amount(1_000_000)
    .build_ix()
    .await?;

Step 3: Place an Order

let request = client.orders().limit_order().await
    .maker(keypair.pubkey())
    .bid()
    .price("0.55")
    .size("1")
    .sign(&keypair, &orderbook)?;
let order = client.orders().submit(&request).await?;

Step 4: Monitor

let open = client
    .orders()
    .get_user_orders(Some(50), None)
    .await?;
let mut ws = client.ws_native();
ws.connect().await?;
ws.subscribe(SubscribeParams::Books {
    orderbook_ids: vec![orderbook.orderbook_id.clone()],
})?;
ws.subscribe(SubscribeParams::User {
    wallet_address: keypair.pubkey().into(),
})?;

Step 5: Cancel an Order

let cancel = CancelBody::signed(order.order_hash.clone(), keypair.pubkey().into(), &keypair);
client.orders().cancel(&cancel).await?;

Step 6: Exit a Position

// sign_and_submit builds the tx, signs it using the client's signing strategy, and submits
let tx_hash = client.positions().merge()
    .user(keypair.pubkey())
    .market(&market)
    .mint(deposit_mint)
    .amount(1_000_000)
    .sign_and_submit()
    .await?;

Step 7: Withdraw

let withdraw_ix = client.positions().withdraw().await
    .user(keypair.pubkey())
    .mint(deposit_mint)
    .amount(1_000_000)
    .build_ix()
    .await?;

Authentication

Authentication is only required for user-specific endpoints. Authentication is session-based using ED25519 signed messages. The flow is: request a nonce, sign it with your wallet, and exchange it for a session token.

Cookie handling

After client.auth().login_with_message(...) succeeds, the SDK stores the session token internally and attaches it as Cookie: lightcone-token=… on every authenticated request. The behaviour depends on the build target:

  • Native builds: token lives in a process-wide Arc<RwLock<Option<String>>> on the LightconeClient. Every authed call reads from it.
  • WASM builds: requests use credentials: "include" and the browser supplies the cookie automatically — the SDK's internal store is unused.

Server-side cookie forwarding (_with_cookies variants)

Naming note. The _with_cookies suffix does not mean other methods are unauthed — most SDK methods that talk to authed endpoints (Positions::positions, Metrics::user, etc.) read auth from the SDK's process-wide token store / browser cookie automatically; that's the typical client-side path. The _with_cookies(auth_token: &str) siblings exist for server-side rendering (SSR) where the per-request browser cookie can't propagate to the shared client. Those callers extract the token from the incoming request and pass it explicitly. Same wire contract, different credentials path.

When the SDK runs on a server (SSR, server functions, an axum handler, etc.) and the user's auth_token cookie arrives on an incoming HTTP request, the SDK's process-wide token store is the wrong place to route it through — the store is shared across all users of that server process.

For these cases, authed methods that need per-call forwarding ship a _with_cookies(auth_token) sibling that injects the cookie just for that one call:

// Inside an axum / dioxus server function, after extracting the
// auth_token cookie from the incoming request:
let balances = client
    .positions()
    .deposit_token_balances_with_cookies(&auth_token)
    .await?;

let positions = client
    .positions()
    .positions_with_cookies(&auth_token)
    .await?;

On WASM these methods are equivalent to their non-_with_cookies counterparts because the browser is already attaching the cookie via credentials mode.

If you maintain a non-Rust SDK (TypeScript, Python) and need to support an SSR consumer, mirror the same pattern: the wire contract is unchanged — only the per-call Cookie: lightcone-token=<token> header attachment differs.

Environment Configuration

The SDK defaults to the production environment. Use LightconeEnv to target a different deployment:

// Production (default — no .env() call needed)
let client = LightconeClient::builder().build()?;

// Staging
let client = LightconeClient::builder()
    .env(LightconeEnv::Staging)
    .build()?;

// Local development
let client = LightconeClient::builder()
    .env(LightconeEnv::Local)
    .build()?;

Each environment configures the API URL, WebSocket URL, Solana RPC URL, and on-chain program ID automatically. Individual URL overrides (.base_url(), .ws_url(), .rpc_url()) take precedence when called after .env().

LightconeEnv::Local targets https://api.local.lightcone.xyz for REST and wss://ws.local.lightcone.xyz/ws for WebSocket connections.

RPC Failover

The SDK supports automatic failover to a backup Solana RPC endpoint:

let client = LightconeClient::builder()
    .rpc_url("https://my-primary-rpc.example.com")
    .backup_rpc_url("https://my-backup-rpc.example.com")
    .build()?;

When the active RPC returns infrastructure errors (connection failures, timeouts, HTTP 502/503/504), the SDK:

  1. Retries once after 100 ms on the same endpoint
  2. If still failing, flips to the other endpoint and retries
  3. All subsequent calls use the new endpoint automatically

After 120 seconds the SDK probes the primary again. If it fails, the fast retry logic switches back to the backup immediately.

Application-level errors (transaction failures, JSON-RPC errors, 4xx responses) never trigger failover — they indicate a problem with the request, not the RPC node.

Examples

All examples are runnable with cargo run --example <name> --features native. Examples default to the production environment and read the wallet keypair from ~/.config/solana/id.json.

Setup & Authentication

Example Description
login Full auth lifecycle: sign message, login, check session, logout
with_auth Per-call auth-token forwarding for SSR / server-function consumers — logs in, captures the token via client.auth_token(), clears the SDK's internal store, and exercises every _with_cookies variant

Market Discovery & Data

Example Description
markets Featured markets, paginated listing, fetch by pubkey, search, platform deposit assets via global_deposit_assets()
market_deposit_assets List the deposit assets and conditional mints for a specific market
orderbook Fetch orderbook depth (bids/asks) and decimal precision metadata
trades Recent trade history with cursor-based pagination (per-orderbook and market-wide)
price_history Historical candlestick data (OHLCV) at various resolutions
positions User positions across all markets and per-market
metrics_all Exercise every endpoint on client.metrics() — platform, markets, categories, orderbook, leaderboard, history

Admin & Testnet

Example Description
faucet_claim Request testnet SOL + deposit tokens via client.claim()

Admin API methods (client.admin()) live in the SDK but are not exercised by an example because they require an admin keypair the CI runner doesn't have. See domain/admin/ADMIN.md for usage.

Placing Orders

Example Description
submit_order Deposit the quote amount into the global pool, then place a limit order via client.orders().limit_order() with human-readable price/size, auto-scaling, and fill tracking. Companion cancel_order cancels it and withdraws to stay net-neutral

Cancelling Orders

Example Description
cancel_order Cancel a single order by hash, cancel all orders in an orderbook, and withdraw the released collateral from the global pool
user_orders Fetch open orders for an authenticated user

On-Chain Operations

Example Description
read_onchain Read exchange state, market state, user nonce, and PDA derivations via RPC
onchain_transactions Build, sign, and submit mint/merge complete set and increment nonce on-chain
global_deposit_withdrawal Init position tokens, deposit to global pool, move capital into a market, extend an existing ALT, withdraw from global, and merge back to keep the run net-neutral

WebSocket Streaming

Example Description
ws_book_and_trades Live orderbook depth with OrderbookState state + rolling TradeHistory buffer
ws_ticker_and_prices Best bid/ask ticker + price history candles with PriceHistoryState
ws_user_and_market Authenticated user stream (orders, balances) + market lifecycle events

Error Handling

All SDK operations return Result<T, SdkError>:

Variant When
SdkError::Http(HttpError) REST request failures
SdkError::Ws(WsError) WebSocket connection/protocol errors
SdkError::Auth(AuthError) Authentication failures
SdkError::Validation(String) Domain type conversion failures
SdkError::Serde(serde_json::Error) Serialization errors
SdkError::MissingMarketContext(string) Market context not provided for operation requiring DepositSource::Market
SdkError::Signing(String) Signing operation failures
SdkError::UserCancelled User cancelled wallet signing prompt
SdkError::ApiRejected(ApiRejectedDetails) Backend rejected the request (see API Rejections)
SdkError::Program(program::SdkError) On-chain program errors (RPC, account parsing)
SdkError::Other(String) Catch-all

API Rejections

When the backend rejects a request (insufficient balance, expired order, etc.), the SDK returns SdkError::ApiRejected(details) where details is an ApiRejectedDetails containing:

Field Type Description
reason String Human-readable error message
rejection_code Option<RejectionCode> Machine-readable rejection code (see below)
error_code Option<String> API-level error code (e.g. "NOT_FOUND", "INVALID_ARGUMENT")
error_log_id Option<String> Backend support correlation ID (LCERR_*)
request_id Option<String> SDK-generated x-request-id for cross-service tracing

Display formats all present fields as a multi-line report. Use .to_string() for logging or clipboard.

RejectionCode

Machine-readable rejection codes with a human-readable .label() method. Unrecognized codes from the backend are captured as Unknown(String) for forward compatibility.

Variant Label When
InsufficientBalance "Insufficient Balance" Not enough funds to fill the order
Expired "Expired" Order expiration time has passed
NonceMismatch "Nonce Mismatch" Order nonce doesn't match current user nonce
SelfTrade "Self Trade" Order would match against the maker's own order
MarketInactive "Market Inactive" Market is not accepting orders
BelowMinOrderSize "Below Min Order Size" Order size is below the minimum
InvalidNonce "Invalid Nonce" Nonce is invalid
BroadcastFailure "Broadcast Failure" Failed to broadcast to the network
OrderNotFound "Order Not Found" Order does not exist
NotOrderMaker "Not Order Maker" Caller is not the order maker
OrderAlreadyFilled "Order Already Filled" Order has already been fully filled
OrderAlreadyCancelled "Order Already Cancelled" Order was already cancelled
Unknown(String) (raw code) Unrecognized code (forward compatible)
match client.orders().submit(&request).await {
    Ok(response) => println!("Order placed: {}", response.order_hash),
    Err(SdkError::ApiRejected(details)) => {
        if let Some(code) = &details.rejection_code {
            println!("Rejected ({}): {}", code.label(), details.reason);
        }
        if let Some(log_id) = &details.error_log_id {
            println!("Support code: {}", log_id);
        }
    }
    Err(other) => eprintln!("Error: {}", other),
}

Request Correlation

The SDK generates a UUID v4 x-request-id header on every HTTP request. On rejection, this ID is attached to ApiRejectedDetails.request_id for cross-service tracing. The same ID is sent to the backend for correlation in logs and error events.

HttpError variants:

Variant Meaning
Reqwest(reqwest::Error) Network/transport failure
ServerError { status, body } Non-2xx response from the backend
RateLimited { retry_after_ms } 429 - back off and retry
Unauthorized 401 - session expired or missing
NotFound(String) 404 - resource not found
BadRequest(String) 400 - invalid request
Timeout Request timed out
MaxRetriesExceeded { attempts, last_error } All retry attempts exhausted

Retry Strategy

  • GET requests: RetryPolicy::Idempotent - retries on transport failures and 502/503/504, backs off on 429 with exponential backoff + jitter.
  • POST requests (order submit, cancel, auth): RetryPolicy::None - no automatic retry. Non-idempotent actions are never retried to prevent duplicate side effects.
  • Customizable per-call with RetryPolicy::Custom(RetryConfig { .. }).