Skip to main content

lightcone_sdk/program/
builder.rs

1//! Fluent builder for creating and signing orders.
2
3use rust_decimal::Decimal;
4use solana_pubkey::Pubkey;
5
6#[cfg(feature = "client")]
7use solana_keypair::Keypair;
8
9use crate::program::orders::FullOrder;
10use crate::program::types::OrderSide;
11use crate::shared::scaling::{scale_price_size, OrderbookDecimals, ScalingError};
12use crate::shared::SubmitOrderRequest;
13
14/// Builder for creating orders with a fluent API.
15///
16/// Provides a convenient way to construct, sign, and convert orders
17/// for API submission in a single chain of method calls.
18///
19/// # Example
20///
21/// ```rust,ignore
22/// use lightcone_sdk::prelude::*;
23///
24/// let request = OrderBuilder::new()
25///     .maker(maker_pubkey)
26///     .market(market_pubkey)
27///     .base_mint(yes_token)
28///     .quote_mint(usdc)
29///     .bid()
30///     .nonce(5)
31///     .maker_amount(1_000_000)
32///     .taker_amount(500_000)
33///     .build_and_sign(&keypair)
34///     .to_submit_request("orderbook_id");
35/// ```
36#[derive(Debug, Clone, Default)]
37pub struct OrderBuilder {
38    nonce: Option<u64>,
39    maker: Option<Pubkey>,
40    market: Option<Pubkey>,
41    base_mint: Option<Pubkey>,
42    quote_mint: Option<Pubkey>,
43    side: Option<OrderSide>,
44    maker_amount: Option<u64>,
45    taker_amount: Option<u64>,
46    expiration: i64,
47    price_raw: Option<String>,
48    size_raw: Option<String>,
49}
50
51impl OrderBuilder {
52    /// Create a new order builder.
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Set the nonce (required).
58    ///
59    /// The nonce must be >= the user's on-chain nonce for the order to be valid.
60    pub fn nonce(mut self, nonce: u64) -> Self {
61        self.nonce = Some(nonce);
62        self
63    }
64
65    /// Set the maker pubkey (required).
66    ///
67    /// This is the public key of the order creator.
68    pub fn maker(mut self, maker: Pubkey) -> Self {
69        self.maker = Some(maker);
70        self
71    }
72
73    /// Set the market pubkey (required).
74    pub fn market(mut self, market: Pubkey) -> Self {
75        self.market = Some(market);
76        self
77    }
78
79    /// Set the base mint / token being bought or sold (required).
80    pub fn base_mint(mut self, base_mint: Pubkey) -> Self {
81        self.base_mint = Some(base_mint);
82        self
83    }
84
85    /// Set the quote mint / payment token (required).
86    pub fn quote_mint(mut self, quote_mint: Pubkey) -> Self {
87        self.quote_mint = Some(quote_mint);
88        self
89    }
90
91    /// Set as a bid order (buy base with quote).
92    pub fn bid(mut self) -> Self {
93        self.side = Some(OrderSide::Bid);
94        self
95    }
96
97    /// Set as an ask order (sell base for quote).
98    pub fn ask(mut self) -> Self {
99        self.side = Some(OrderSide::Ask);
100        self
101    }
102
103    /// Set the side directly.
104    pub fn side(mut self, side: OrderSide) -> Self {
105        self.side = Some(side);
106        self
107    }
108
109    /// Set the amount the maker gives.
110    pub fn maker_amount(mut self, amount: u64) -> Self {
111        self.maker_amount = Some(amount);
112        self
113    }
114
115    /// Set the amount the maker wants to receive.
116    pub fn taker_amount(mut self, amount: u64) -> Self {
117        self.taker_amount = Some(amount);
118        self
119    }
120
121    /// Set expiration timestamp (0 = no expiration).
122    pub fn expiration(mut self, expiration: i64) -> Self {
123        self.expiration = expiration;
124        self
125    }
126
127    /// Build an unsigned FullOrder.
128    ///
129    /// The returned order has an all-zero signature and must be signed
130    /// before submission.
131    ///
132    /// # Panics
133    ///
134    /// Panics if required fields are missing.
135    pub fn build(self) -> FullOrder {
136        FullOrder {
137            nonce: self.nonce.expect("nonce is required"),
138            maker: self.maker.expect("maker is required"),
139            market: self.market.expect("market is required"),
140            base_mint: self.base_mint.expect("base_mint is required"),
141            quote_mint: self.quote_mint.expect("quote_mint is required"),
142            side: self.side.expect("side is required (call .bid() or .ask())"),
143            maker_amount: self.maker_amount.expect("maker_amount is required"),
144            taker_amount: self.taker_amount.expect("taker_amount is required"),
145            expiration: self.expiration,
146            signature: [0u8; 64],
147        }
148    }
149
150    /// Build and sign the order with the given keypair.
151    ///
152    /// Returns a signed FullOrder ready for API submission.
153    ///
154    /// # Panics
155    ///
156    /// Panics if required fields are missing.
157    #[cfg(feature = "client")]
158    pub fn build_and_sign(self, keypair: &Keypair) -> FullOrder {
159        let mut order = self.build();
160        order.sign(keypair);
161        order
162    }
163
164    /// Build, sign, and convert directly to a SubmitOrderRequest.
165    ///
166    /// # Arguments
167    ///
168    /// * `keypair` - Keypair to sign the order with
169    /// * `orderbook_id` - Target orderbook ID
170    ///
171    /// # Panics
172    ///
173    /// Panics if required fields are missing.
174    #[cfg(feature = "client")]
175    pub fn to_submit_request(
176        self,
177        keypair: &Keypair,
178        orderbook_id: impl Into<String>,
179    ) -> SubmitOrderRequest {
180        self.build_and_sign(keypair).to_submit_request(orderbook_id)
181    }
182
183    // =========================================================================
184    // Auto-scaling: price/size -> maker_amount/taker_amount
185    // =========================================================================
186
187    /// Set price as a human-readable string (e.g., "0.65" quote per base).
188    pub fn price(mut self, price: &str) -> Self {
189        self.price_raw = Some(price.to_string());
190        self
191    }
192
193    /// Set size as a human-readable string (e.g., "100" base tokens).
194    pub fn size(mut self, size: &str) -> Self {
195        self.size_raw = Some(size.to_string());
196        self
197    }
198
199    /// Convert price/size strings into maker_amount/taker_amount using orderbook decimals.
200    ///
201    /// Call this after `.price()`, `.size()`, and `.bid()`/`.ask()`, then use
202    /// any existing build method (`build()`, `build_and_sign()`, `to_submit_request()`).
203    pub fn apply_scaling(mut self, decimals: &OrderbookDecimals) -> Result<Self, ScalingError> {
204        let price_str = self
205            .price_raw
206            .as_deref()
207            .expect("price() is required for apply_scaling");
208        let size_str = self
209            .size_raw
210            .as_deref()
211            .expect("size() is required for apply_scaling");
212
213        let price: Decimal =
214            price_str
215                .parse()
216                .map_err(|e: rust_decimal::Error| ScalingError::InvalidDecimal {
217                    input: price_str.to_string(),
218                    reason: e.to_string(),
219                })?;
220
221        let size: Decimal =
222            size_str
223                .parse()
224                .map_err(|e: rust_decimal::Error| ScalingError::InvalidDecimal {
225                    input: size_str.to_string(),
226                    reason: e.to_string(),
227                })?;
228
229        let side = self
230            .side
231            .expect("side is required (call .bid() or .ask()) for apply_scaling");
232
233        let scaled = scale_price_size(price, size, side, decimals)?;
234        self.maker_amount = Some(scaled.maker_amount);
235        self.taker_amount = Some(scaled.taker_amount);
236        Ok(self)
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    #[cfg(feature = "client")]
244    use solana_signer::Signer;
245
246    #[test]
247    #[cfg(feature = "client")]
248    fn test_order_builder_basic() {
249        let keypair = Keypair::new();
250        let maker = keypair.pubkey();
251        let market = Pubkey::new_unique();
252        let base_mint = Pubkey::new_unique();
253        let quote_mint = Pubkey::new_unique();
254
255        let order = OrderBuilder::new()
256            .nonce(1)
257            .maker(maker)
258            .market(market)
259            .base_mint(base_mint)
260            .quote_mint(quote_mint)
261            .bid()
262            .maker_amount(1_000_000)
263            .taker_amount(500_000)
264            .build_and_sign(&keypair);
265
266        assert_eq!(order.nonce, 1);
267        assert_eq!(order.maker, maker);
268        assert_eq!(order.market, market);
269        assert_eq!(order.base_mint, base_mint);
270        assert_eq!(order.quote_mint, quote_mint);
271        assert_eq!(order.side, OrderSide::Bid);
272        assert_eq!(order.maker_amount, 1_000_000);
273        assert_eq!(order.taker_amount, 500_000);
274        assert!(order.is_signed());
275    }
276
277    #[test]
278    #[cfg(feature = "client")]
279    fn test_order_builder_to_submit_request() {
280        let keypair = Keypair::new();
281        let maker = keypair.pubkey();
282        let market = Pubkey::new_unique();
283        let base_mint = Pubkey::new_unique();
284        let quote_mint = Pubkey::new_unique();
285
286        let request = OrderBuilder::new()
287            .nonce(1)
288            .maker(maker)
289            .market(market)
290            .base_mint(base_mint)
291            .quote_mint(quote_mint)
292            .ask()
293            .maker_amount(500_000)
294            .taker_amount(1_000_000)
295            .to_submit_request(&keypair, "test_orderbook");
296
297        assert_eq!(request.maker, maker.to_string());
298        assert_eq!(request.nonce, 1);
299        assert_eq!(request.market_pubkey, market.to_string());
300        assert_eq!(request.base_token, base_mint.to_string());
301        assert_eq!(request.quote_token, quote_mint.to_string());
302        assert_eq!(request.side, 1); // Ask
303        assert_eq!(request.maker_amount, 500_000);
304        assert_eq!(request.taker_amount, 1_000_000);
305        assert_eq!(request.orderbook_id, "test_orderbook");
306        assert_eq!(request.signature.len(), 128); // 64 bytes = 128 hex chars
307    }
308
309    #[test]
310    fn test_order_builder_unsigned() {
311        #[cfg(feature = "client")]
312        let keypair = Keypair::new();
313        #[cfg(feature = "client")]
314        let maker = keypair.pubkey();
315        #[cfg(not(feature = "client"))]
316        let maker = Pubkey::new_unique();
317
318        let order = OrderBuilder::new()
319            .nonce(1)
320            .maker(maker)
321            .market(Pubkey::new_unique())
322            .base_mint(Pubkey::new_unique())
323            .quote_mint(Pubkey::new_unique())
324            .bid()
325            .maker_amount(1_000_000)
326            .taker_amount(500_000)
327            .build();
328
329        assert!(!order.is_signed());
330    }
331
332    #[test]
333    #[cfg(feature = "client")]
334    #[should_panic(expected = "nonce is required")]
335    fn test_order_builder_missing_nonce() {
336        let keypair = Keypair::new();
337        OrderBuilder::new()
338            .maker(keypair.pubkey())
339            .market(Pubkey::new_unique())
340            .base_mint(Pubkey::new_unique())
341            .quote_mint(Pubkey::new_unique())
342            .bid()
343            .maker_amount(1_000_000)
344            .taker_amount(500_000)
345            .build_and_sign(&keypair);
346    }
347
348    #[test]
349    #[cfg(feature = "client")]
350    #[should_panic(expected = "side is required")]
351    fn test_order_builder_missing_side() {
352        let keypair = Keypair::new();
353        OrderBuilder::new()
354            .nonce(1)
355            .maker(keypair.pubkey())
356            .market(Pubkey::new_unique())
357            .base_mint(Pubkey::new_unique())
358            .quote_mint(Pubkey::new_unique())
359            .maker_amount(1_000_000)
360            .taker_amount(500_000)
361            .build_and_sign(&keypair);
362    }
363}