lightcone 0.5.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.4.1", features = ["native"] }

For browser/WASM targets:

[dependencies]
lightcone = { version = "0.4.1", 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

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(&keypair.pubkey().to_string(), 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.

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().

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

Market Discovery & Data

Example Description
markets Featured markets, paginated listing, fetch by pubkey, search
orderbook Fetch orderbook depth (bids/asks) and decimal precision metadata
trades Recent trade history with cursor-based pagination
price_history Historical candlestick data (OHLCV) at various resolutions
positions User positions across all markets and per-market

Placing Orders

Example Description
submit_order Limit order via client.orders().limit_order() with human-readable price/size, auto-scaling, and fill tracking

Cancelling Orders

Example Description
cancel_order Cancel a single order by hash and cancel all orders in an orderbook
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, and withdraw from global

WebSocket Streaming

Example Description
ws_book_and_trades Live orderbook depth with OrderbookSnapshot 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 { .. }).