lightcone-sdk 0.1.7

Rust SDK for the Lightcone Protcol
Documentation
# PR Walkthrough: Decimal Scaling (Auto-fetched from Backend)

## Problem

Previously, SDK users had to manually calculate raw lamport amounts (`maker_amount` / `taker_amount`) when placing orders. This meant everyone had to know the token decimals, do the math themselves, and pass in raw `u64` values like `65_000_000`. Error-prone and annoying.

## Solution

This commit adds a decimal scaling system that lets users pass human-readable strings (`"0.65"`, `"100"`) and have the SDK convert them to raw lamport amounts automatically. The decimals metadata is fetched from the backend and cached permanently (it never changes for a given orderbook).

---

### 2. `src/shared/scaling.rs` — New file (core math module)

This is the heart of the change. A pure, synchronous module — no async, no network calls.

**Key types:**

- `OrderbookDecimals` — Holds `base_decimals`, `quote_decimals`, and `price_decimals` for an orderbook. Passed around as the config for all scaling operations.
- `ScaledAmounts` — The output: `{ maker_amount: u64, taker_amount: u64 }`.
- `ScalingError` — Enum covering all failure modes: non-positive inputs, overflow, fractional lamports, zero amounts, and invalid decimal strings.

**Core function: `scale_price_size(price, size, side, decimals)`**

Conversion math:

```
base_lamports  = size  * 10^base_decimals
quote_lamports = price * size * 10^quote_decimals
```

Then assigned based on side:

| Side | maker_amount (gives) | taker_amount (receives) |
|------|---------------------|------------------------|
| BID  | quote_lamports      | base_lamports          |
| ASK  | base_lamports       | quote_lamports         |

All arithmetic uses `rust_decimal::Decimal` for exact decimal math (no floating-point). The function validates:
- Price and size are positive
- No overflow during multiplication
- Results are whole numbers (no fractional lamports)
- Results are non-zero
- Results fit in `u64`

**Tests (11 unit tests):** Covers bids, asks, different decimal configurations (6/6, 6/9), zero/negative inputs, fractional lamport rejection, overflow, and edge cases (minimum 1-lamport amounts).

---

### 3. `src/api/types/decimals.rs` — New file (API response type)

A simple deserialization struct for the `GET /api/orderbooks/{id}/decimals` endpoint:

```rust
pub struct DecimalsResponse {
    pub orderbook_id: String,
    pub base_decimals: u8,
    pub quote_decimals: u8,
    pub price_decimals: u8,
}
```

---

### 4. `src/api/types/mod.rs` — Wire up new type

Adds `pub mod decimals;` and `pub use decimals::*;` so `DecimalsResponse` is available through the types barrel export.

---

### 5. `src/api/client.rs` — Decimals cache + fetch methods

**Structural changes:**
- Added `decimals_cache: Arc<RwLock<HashMap<String, OrderbookDecimals>>>` field to `LightconeApiClient`. This is an async-safe in-memory cache keyed by orderbook ID.
- The builder initializes it as an empty map.

**New methods:**

`get_orderbook_decimals(orderbook_id)` — Read-through cache:
1. Acquires a read lock; returns immediately on cache hit.
2. On miss, fetches `GET /api/orderbooks/{id}/decimals`, converts the response to an `OrderbookDecimals`, acquires a write lock, and inserts into the cache.
3. Decimals never change for an orderbook, so the cache never needs invalidation.

`prefetch_decimals(orderbook_ids)` — Convenience method to warm the cache for multiple orderbooks upfront. Iterates and calls `get_orderbook_decimals` for each. Fails on first error.

---

### 6. `src/program/builder.rs``OrderBuilder` gets scaling support

**New fields on `OrderBuilder`:**
- `price_raw: Option<String>`
- `size_raw: Option<String>`

**New builder methods:**

- `.price("0.65")` — Stores the human-readable price string.
- `.size("100")` — Stores the human-readable size string.
- `.apply_scaling(&decimals)` — The key method. Parses the stored price/size strings into `Decimal`, calls `scale_price_size()`, and sets `maker_amount` / `taker_amount` on the builder. Returns `Result<Self, ScalingError>` so it chains with `?`.

After calling `apply_scaling()`, the builder has its amounts filled in, and you use any existing build method as normal.

---

### 7. `src/program/error.rs` — New `SdkError` variant

Adds a `Scaling(ScalingError)` variant to `SdkError` with a `#[from]` derive, so scaling errors can be propagated with `?` from any function returning `SdkError`.

---

### 8. `src/shared/mod.rs` — Re-exports

Adds `pub mod scaling;` and re-exports `scale_price_size`, `OrderbookDecimals`, `ScaledAmounts`, and `ScalingError` from `shared`.

---

### 9. `src/lib.rs` — Prelude exports

Adds to the prelude:
- `DecimalsResponse` (API type)
- `scale_price_size`, `OrderbookDecimals`, `ScaledAmounts`, `ScalingError` (shared scaling)

So users who `use lightcone_sdk::prelude::*` get everything they need.

---

### 10. `Cargo.lock` — Auto-updated

Lock file updated to reflect the version bump.

---

## Usage

**With auto-scaling (new):**

```rust
let decimals = api_client.get_orderbook_decimals("orderbook_id").await?;

let request = OrderBuilder::new()
    .maker(keypair.pubkey())
    .market(market_pubkey)
    .base_mint(yes_token)
    .quote_mint(usdc)
    .bid()
    .nonce(5)
    .price("0.65")
    .size("100")
    .apply_scaling(&decimals)?
    .to_submit_request(&keypair, "orderbook_id");
```

**With raw amounts (unchanged):**

```rust
let request = OrderBuilder::new()
    .maker(keypair.pubkey())
    .market(market_pubkey)
    .base_mint(yes_token)
    .quote_mint(usdc)
    .bid()
    .nonce(5)
    .maker_amount(65_000_000)
    .taker_amount(100_000_000)
    .to_submit_request(&keypair, "orderbook_id");
```

Both paths are fully supported. The raw path is completely untouched.

---

## Design Decisions

1. **`apply_scaling()` returns `Self`, not a built order.** This keeps it composable — you call it mid-chain, then pick whichever build method you need (`build()`, `build_and_sign()`, `to_submit_request()`). One scaling method instead of three duplicated variants.

2. **`rust_decimal::Decimal` for all math.** No floating-point anywhere in the pipeline. Exact decimal arithmetic avoids rounding bugs that would silently produce wrong lamport amounts.

3. **Cache is `Arc<RwLock<HashMap>>`.** Read-heavy, write-rare pattern. The read lock is non-blocking for concurrent order placement. Decimals are immutable per orderbook, so there's no cache invalidation logic needed.

4. **Scaling errors are distinct from API errors.** `ScalingError` is its own enum (not shoved into `ApiError`). It can be converted into `SdkError` via the `#[from]` derive if needed.