# 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:
| 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.