Skip to main content

lightcone_sdk/program/
orders.rs

1//! Order types, serialization, hashing, and signing.
2//!
3//! This module provides the full and compact order structures with
4//! Keccak256 hashing and Ed25519 signing functionality.
5
6use sha3::{Digest, Keccak256};
7use solana_pubkey::Pubkey;
8use solana_signature::Signature;
9
10#[cfg(feature = "client")]
11use solana_keypair::Keypair;
12#[cfg(feature = "client")]
13use solana_signer::Signer;
14
15use crate::program::constants::{COMPACT_ORDER_SIZE, FULL_ORDER_SIZE};
16use crate::program::error::{SdkError, SdkResult};
17use crate::program::types::{AskOrderParams, BidOrderParams, OrderSide};
18use crate::shared::SubmitOrderRequest;
19
20// ============================================================================
21// Full Order (225 bytes)
22// ============================================================================
23
24/// Full order structure with signature.
25///
26/// Layout (225 bytes):
27/// - [0..8]     nonce (8 bytes)
28/// - [8..40]    maker (32 bytes)
29/// - [40..72]   market (32 bytes)
30/// - [72..104]  base_mint (32 bytes)
31/// - [104..136] quote_mint (32 bytes)
32/// - [136]      side (1 byte)
33/// - [137..145] maker_amount (8 bytes)
34/// - [145..153] taker_amount (8 bytes)
35/// - [153..161] expiration (8 bytes)
36/// - [161..225] signature (64 bytes)
37#[derive(Debug, Clone)]
38pub struct FullOrder {
39    /// Unique order ID and replay protection
40    pub nonce: u64,
41    /// Order maker's pubkey
42    pub maker: Pubkey,
43    /// Market pubkey
44    pub market: Pubkey,
45    /// Base mint (token being bought/sold)
46    pub base_mint: Pubkey,
47    /// Quote mint (token used for payment)
48    pub quote_mint: Pubkey,
49    /// Order side (0 = Bid, 1 = Ask)
50    pub side: OrderSide,
51    /// Amount maker gives
52    pub maker_amount: u64,
53    /// Amount maker receives
54    pub taker_amount: u64,
55    /// Expiration timestamp (0 = no expiration)
56    pub expiration: i64,
57    /// Ed25519 signature
58    pub signature: [u8; 64],
59}
60
61impl FullOrder {
62    /// Order size in bytes
63    pub const LEN: usize = FULL_ORDER_SIZE;
64
65    /// Create a new bid order (maker buys base, gives quote)
66    pub fn new_bid(params: BidOrderParams) -> Self {
67        Self {
68            nonce: params.nonce,
69            maker: params.maker,
70            market: params.market,
71            base_mint: params.base_mint,
72            quote_mint: params.quote_mint,
73            side: OrderSide::Bid,
74            maker_amount: params.maker_amount,
75            taker_amount: params.taker_amount,
76            expiration: params.expiration,
77            signature: [0u8; 64],
78        }
79    }
80
81    /// Create a new ask order (maker sells base, receives quote)
82    pub fn new_ask(params: AskOrderParams) -> Self {
83        Self {
84            nonce: params.nonce,
85            maker: params.maker,
86            market: params.market,
87            base_mint: params.base_mint,
88            quote_mint: params.quote_mint,
89            side: OrderSide::Ask,
90            maker_amount: params.maker_amount,
91            taker_amount: params.taker_amount,
92            expiration: params.expiration,
93            signature: [0u8; 64],
94        }
95    }
96
97    /// Compute the Keccak256 hash of the order (excludes signature).
98    ///
99    /// Hash layout (161 bytes):
100    /// - nonce (8)
101    /// - maker (32)
102    /// - market (32)
103    /// - base_mint (32)
104    /// - quote_mint (32)
105    /// - side (1)
106    /// - maker_amount (8)
107    /// - taker_amount (8)
108    /// - expiration (8)
109    pub fn hash(&self) -> [u8; 32] {
110        let mut hasher = Keccak256::new();
111
112        hasher.update(self.nonce.to_le_bytes());
113        hasher.update(self.maker.as_ref());
114        hasher.update(self.market.as_ref());
115        hasher.update(self.base_mint.as_ref());
116        hasher.update(self.quote_mint.as_ref());
117        hasher.update([self.side as u8]);
118        hasher.update(self.maker_amount.to_le_bytes());
119        hasher.update(self.taker_amount.to_le_bytes());
120        hasher.update(self.expiration.to_le_bytes());
121
122        hasher.finalize().into()
123    }
124
125    /// Sign the order with the given keypair.
126    #[cfg(feature = "client")]
127    pub fn sign(&mut self, keypair: &Keypair) {
128        let hash = self.hash();
129        let sig = keypair.sign_message(&hash);
130        self.signature.copy_from_slice(sig.as_ref());
131    }
132
133    /// Create and sign an order in one step.
134    #[cfg(feature = "client")]
135    pub fn new_bid_signed(params: BidOrderParams, keypair: &Keypair) -> Self {
136        let mut order = Self::new_bid(params);
137        order.sign(keypair);
138        order
139    }
140
141    /// Create and sign an ask order in one step.
142    #[cfg(feature = "client")]
143    pub fn new_ask_signed(params: AskOrderParams, keypair: &Keypair) -> Self {
144        let mut order = Self::new_ask(params);
145        order.sign(keypair);
146        order
147    }
148
149    /// Verify a hash signature against the maker's pubkey.
150    pub fn verify_signature(&self) -> SdkResult<()> {
151        let hash: [u8; 32] = self.hash();
152
153        let sig = Signature::try_from(self.signature.as_slice())
154            .map_err(|_| SdkError::InvalidSignature)?;
155
156        if !sig.verify(self.maker.as_ref(), &hash) {
157            return Err(SdkError::SignatureVerificationFailed);
158        }
159        Ok(())
160    }
161
162    /// Apply a signature to the order.
163    pub fn apply_signature(&mut self, signature: &[u8]) -> SdkResult<()> {
164        self.signature = signature
165            .try_into()
166            .map_err(|_| SdkError::InvalidSignature)?;
167        Ok(())
168    }
169
170    /// Serialize to bytes (225 bytes).
171    pub fn serialize(&self) -> [u8; FULL_ORDER_SIZE] {
172        let mut data = [0u8; FULL_ORDER_SIZE];
173
174        data[0..8].copy_from_slice(&self.nonce.to_le_bytes());
175        data[8..40].copy_from_slice(self.maker.as_ref());
176        data[40..72].copy_from_slice(self.market.as_ref());
177        data[72..104].copy_from_slice(self.base_mint.as_ref());
178        data[104..136].copy_from_slice(self.quote_mint.as_ref());
179        data[136] = self.side as u8;
180        data[137..145].copy_from_slice(&self.maker_amount.to_le_bytes());
181        data[145..153].copy_from_slice(&self.taker_amount.to_le_bytes());
182        data[153..161].copy_from_slice(&self.expiration.to_le_bytes());
183        data[161..225].copy_from_slice(&self.signature);
184
185        data
186    }
187
188    /// Deserialize from bytes.
189    pub fn deserialize(data: &[u8]) -> SdkResult<Self> {
190        if data.len() < FULL_ORDER_SIZE {
191            return Err(SdkError::InvalidDataLength {
192                expected: FULL_ORDER_SIZE,
193                actual: data.len(),
194            });
195        }
196
197        let mut nonce_bytes = [0u8; 8];
198        nonce_bytes.copy_from_slice(&data[0..8]);
199
200        let mut maker_bytes = [0u8; 32];
201        maker_bytes.copy_from_slice(&data[8..40]);
202
203        let mut market_bytes = [0u8; 32];
204        market_bytes.copy_from_slice(&data[40..72]);
205
206        let mut base_mint_bytes = [0u8; 32];
207        base_mint_bytes.copy_from_slice(&data[72..104]);
208
209        let mut quote_mint_bytes = [0u8; 32];
210        quote_mint_bytes.copy_from_slice(&data[104..136]);
211
212        let mut maker_amount_bytes = [0u8; 8];
213        maker_amount_bytes.copy_from_slice(&data[137..145]);
214
215        let mut taker_amount_bytes = [0u8; 8];
216        taker_amount_bytes.copy_from_slice(&data[145..153]);
217
218        let mut expiration_bytes = [0u8; 8];
219        expiration_bytes.copy_from_slice(&data[153..161]);
220
221        let mut signature = [0u8; 64];
222        signature.copy_from_slice(&data[161..225]);
223
224        Ok(Self {
225            nonce: u64::from_le_bytes(nonce_bytes),
226            maker: Pubkey::new_from_array(maker_bytes),
227            market: Pubkey::new_from_array(market_bytes),
228            base_mint: Pubkey::new_from_array(base_mint_bytes),
229            quote_mint: Pubkey::new_from_array(quote_mint_bytes),
230            side: OrderSide::try_from(data[136])?,
231            maker_amount: u64::from_le_bytes(maker_amount_bytes),
232            taker_amount: u64::from_le_bytes(taker_amount_bytes),
233            expiration: i64::from_le_bytes(expiration_bytes),
234            signature,
235        })
236    }
237
238    /// Convert to compact order format.
239    pub fn to_compact(&self) -> CompactOrder {
240        CompactOrder {
241            nonce: self.nonce,
242            maker: self.maker,
243            side: self.side,
244            maker_amount: self.maker_amount,
245            taker_amount: self.taker_amount,
246            expiration: self.expiration,
247        }
248    }
249
250    /// Get the signature as a hex string (128 chars).
251    pub fn signature_hex(&self) -> String {
252        hex::encode(self.signature)
253    }
254
255    /// Get the order hash as a hex string (64 chars).
256    pub fn hash_hex(&self) -> String {
257        hex::encode(self.hash())
258    }
259
260    /// Check if the order has been signed.
261    pub fn is_signed(&self) -> bool {
262        self.signature != [0u8; 64]
263    }
264
265    // =========================================================================
266    // API Bridge Methods
267    // =========================================================================
268
269    /// Convert a signed order to an API SubmitOrderRequest.
270    ///
271    /// This bridges on-chain order creation with REST API submission.
272    ///
273    /// # Arguments
274    ///
275    /// * `orderbook_id` - Target orderbook (get from market API or use `derive_orderbook_id()`)
276    ///
277    /// # Panics
278    ///
279    /// Panics if the order has not been signed (signature is all zeros).
280    ///
281    /// # Example
282    ///
283    /// ```rust,ignore
284    /// let mut order = FullOrder::new_bid(params);
285    /// order.sign(&keypair);
286    ///
287    /// let request = order.to_submit_request(order.derive_orderbook_id());
288    /// let response = api_client.submit_order(request).await?;
289    /// ```
290    pub fn to_submit_request(&self, orderbook_id: impl Into<String>) -> SubmitOrderRequest {
291        assert!(
292            self.signature != [0u8; 64],
293            "Order must be signed before converting to submit request"
294        );
295
296        SubmitOrderRequest {
297            maker: self.maker.to_string(),
298            nonce: self.nonce,
299            market_pubkey: self.market.to_string(),
300            base_token: self.base_mint.to_string(),
301            quote_token: self.quote_mint.to_string(),
302            side: self.side as u32,
303            maker_amount: self.maker_amount,
304            taker_amount: self.taker_amount,
305            expiration: self.expiration,
306            signature: hex::encode(self.signature),
307            orderbook_id: orderbook_id.into(),
308        }
309    }
310
311    /// Derive the orderbook ID for this order.
312    ///
313    /// Format: `{base_token[0:8]}_{quote_token[0:8]}`
314    pub fn derive_orderbook_id(&self) -> String {
315        crate::shared::derive_orderbook_id(
316            &self.base_mint.to_string(),
317            &self.quote_mint.to_string(),
318        )
319    }
320}
321
322// ============================================================================
323// Compact Order (65 bytes)
324// ============================================================================
325
326/// Compact order format for transaction size optimization.
327///
328/// Excludes market, base_mint, quote_mint which are passed via accounts.
329///
330/// Layout (65 bytes):
331/// - [0..8]   nonce (8 bytes)
332/// - [8..40]  maker (32 bytes)
333/// - [40]     side (1 byte)
334/// - [41..49] maker_amount (8 bytes)
335/// - [49..57] taker_amount (8 bytes)
336/// - [57..65] expiration (8 bytes)
337#[derive(Debug, Clone)]
338pub struct CompactOrder {
339    /// Unique order ID and replay protection
340    pub nonce: u64,
341    /// Order maker's pubkey
342    pub maker: Pubkey,
343    /// Order side (0 = Bid, 1 = Ask)
344    pub side: OrderSide,
345    /// Amount maker gives
346    pub maker_amount: u64,
347    /// Amount maker receives
348    pub taker_amount: u64,
349    /// Expiration timestamp (0 = no expiration)
350    pub expiration: i64,
351}
352
353impl CompactOrder {
354    /// Order size in bytes
355    pub const LEN: usize = COMPACT_ORDER_SIZE;
356
357    /// Serialize to bytes (65 bytes).
358    pub fn serialize(&self) -> [u8; COMPACT_ORDER_SIZE] {
359        let mut data = [0u8; COMPACT_ORDER_SIZE];
360
361        data[0..8].copy_from_slice(&self.nonce.to_le_bytes());
362        data[8..40].copy_from_slice(self.maker.as_ref());
363        data[40] = self.side as u8;
364        data[41..49].copy_from_slice(&self.maker_amount.to_le_bytes());
365        data[49..57].copy_from_slice(&self.taker_amount.to_le_bytes());
366        data[57..65].copy_from_slice(&self.expiration.to_le_bytes());
367
368        data
369    }
370
371    /// Deserialize from bytes.
372    pub fn deserialize(data: &[u8]) -> SdkResult<Self> {
373        if data.len() < COMPACT_ORDER_SIZE {
374            return Err(SdkError::InvalidDataLength {
375                expected: COMPACT_ORDER_SIZE,
376                actual: data.len(),
377            });
378        }
379
380        let mut nonce_bytes = [0u8; 8];
381        nonce_bytes.copy_from_slice(&data[0..8]);
382
383        let mut maker_bytes = [0u8; 32];
384        maker_bytes.copy_from_slice(&data[8..40]);
385
386        let mut maker_amount_bytes = [0u8; 8];
387        maker_amount_bytes.copy_from_slice(&data[41..49]);
388
389        let mut taker_amount_bytes = [0u8; 8];
390        taker_amount_bytes.copy_from_slice(&data[49..57]);
391
392        let mut expiration_bytes = [0u8; 8];
393        expiration_bytes.copy_from_slice(&data[57..65]);
394
395        Ok(Self {
396            nonce: u64::from_le_bytes(nonce_bytes),
397            maker: Pubkey::new_from_array(maker_bytes),
398            side: OrderSide::try_from(data[40])?,
399            maker_amount: u64::from_le_bytes(maker_amount_bytes),
400            taker_amount: u64::from_le_bytes(taker_amount_bytes),
401            expiration: i64::from_le_bytes(expiration_bytes),
402        })
403    }
404
405    /// Expand to full order using pubkeys from accounts.
406    pub fn to_full_order(
407        &self,
408        market: Pubkey,
409        base_mint: Pubkey,
410        quote_mint: Pubkey,
411        signature: [u8; 64],
412    ) -> FullOrder {
413        FullOrder {
414            nonce: self.nonce,
415            maker: self.maker,
416            market,
417            base_mint,
418            quote_mint,
419            side: self.side,
420            maker_amount: self.maker_amount,
421            taker_amount: self.taker_amount,
422            expiration: self.expiration,
423            signature,
424        }
425    }
426}
427
428// ============================================================================
429// Order Validation Helpers
430// ============================================================================
431
432/// Check if an order is expired.
433pub fn is_order_expired(order: &FullOrder, current_time: i64) -> bool {
434    order.expiration != 0 && current_time >= order.expiration
435}
436
437/// Check if two orders can cross (prices are compatible).
438///
439/// Returns true if the buyer's price >= seller's price.
440pub fn orders_can_cross(buy_order: &FullOrder, sell_order: &FullOrder) -> bool {
441    if buy_order.side != OrderSide::Bid || sell_order.side != OrderSide::Ask {
442        return false;
443    }
444
445    if buy_order.maker_amount == 0
446        || buy_order.taker_amount == 0
447        || sell_order.maker_amount == 0
448        || sell_order.taker_amount == 0
449    {
450        return false;
451    }
452
453    // Buyer gives quote, receives base
454    // Seller gives base, receives quote
455    // Cross condition: buyer's price >= seller's price
456    // buyer_price = buyer.maker_amount / buyer.taker_amount (quote per base)
457    // seller_price = seller.taker_amount / seller.maker_amount (quote per base)
458    // Cross: buyer.maker_amount / buyer.taker_amount >= seller.taker_amount / seller.maker_amount
459    // Rearrange: buyer.maker_amount * seller.maker_amount >= buyer.taker_amount * seller.taker_amount
460
461    let buyer_cross = (buy_order.maker_amount as u128) * (sell_order.maker_amount as u128);
462    let seller_cross = (buy_order.taker_amount as u128) * (sell_order.taker_amount as u128);
463
464    buyer_cross >= seller_cross
465}
466
467/// Calculate the taker fill amount given a maker fill amount.
468pub fn calculate_taker_fill(maker_order: &FullOrder, maker_fill_amount: u64) -> SdkResult<u64> {
469    if maker_order.maker_amount == 0 {
470        return Err(SdkError::Overflow);
471    }
472
473    let result = (maker_fill_amount as u128)
474        .checked_mul(maker_order.taker_amount as u128)
475        .ok_or(SdkError::Overflow)?
476        .checked_div(maker_order.maker_amount as u128)
477        .ok_or(SdkError::Overflow)?;
478
479    if result > u64::MAX as u128 {
480        return Err(SdkError::Overflow);
481    }
482
483    Ok(result as u64)
484}
485
486/// Derive condition ID from oracle, question_id, and num_outcomes.
487pub fn derive_condition_id(oracle: &Pubkey, question_id: &[u8; 32], num_outcomes: u8) -> [u8; 32] {
488    let mut hasher = Keccak256::new();
489    hasher.update(oracle.as_ref());
490    hasher.update(question_id);
491    hasher.update([num_outcomes]);
492    hasher.finalize().into()
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn test_order_serialization_roundtrip() {
501        let order = FullOrder {
502            nonce: 12345,
503            maker: Pubkey::new_unique(),
504            market: Pubkey::new_unique(),
505            base_mint: Pubkey::new_unique(),
506            quote_mint: Pubkey::new_unique(),
507            side: OrderSide::Bid,
508            maker_amount: 1000000,
509            taker_amount: 500000,
510            expiration: 1234567890,
511            signature: [0u8; 64],
512        };
513
514        let serialized = order.serialize();
515        let deserialized = FullOrder::deserialize(&serialized).unwrap();
516
517        assert_eq!(order.nonce, deserialized.nonce);
518        assert_eq!(order.maker, deserialized.maker);
519        assert_eq!(order.market, deserialized.market);
520        assert_eq!(order.base_mint, deserialized.base_mint);
521        assert_eq!(order.quote_mint, deserialized.quote_mint);
522        assert_eq!(order.side, deserialized.side);
523        assert_eq!(order.maker_amount, deserialized.maker_amount);
524        assert_eq!(order.taker_amount, deserialized.taker_amount);
525        assert_eq!(order.expiration, deserialized.expiration);
526    }
527
528    #[test]
529    fn test_compact_order_serialization_roundtrip() {
530        let order = CompactOrder {
531            nonce: 12345,
532            maker: Pubkey::new_unique(),
533            side: OrderSide::Ask,
534            maker_amount: 1000000,
535            taker_amount: 500000,
536            expiration: 1234567890,
537        };
538
539        let serialized = order.serialize();
540        let deserialized = CompactOrder::deserialize(&serialized).unwrap();
541
542        assert_eq!(order.nonce, deserialized.nonce);
543        assert_eq!(order.maker, deserialized.maker);
544        assert_eq!(order.side, deserialized.side);
545        assert_eq!(order.maker_amount, deserialized.maker_amount);
546        assert_eq!(order.taker_amount, deserialized.taker_amount);
547        assert_eq!(order.expiration, deserialized.expiration);
548    }
549
550    #[test]
551    fn test_order_hash_consistency() {
552        let order = FullOrder {
553            nonce: 1,
554            maker: Pubkey::new_from_array([1u8; 32]),
555            market: Pubkey::new_from_array([2u8; 32]),
556            base_mint: Pubkey::new_from_array([3u8; 32]),
557            quote_mint: Pubkey::new_from_array([4u8; 32]),
558            side: OrderSide::Bid,
559            maker_amount: 100,
560            taker_amount: 50,
561            expiration: 0,
562            signature: [0u8; 64],
563        };
564
565        let hash1 = order.hash();
566        let hash2 = order.hash();
567        assert_eq!(hash1, hash2);
568    }
569
570    #[test]
571    fn test_orders_can_cross() {
572        let buy_order = FullOrder {
573            nonce: 1,
574            maker: Pubkey::new_unique(),
575            market: Pubkey::new_unique(),
576            base_mint: Pubkey::new_unique(),
577            quote_mint: Pubkey::new_unique(),
578            side: OrderSide::Bid,
579            maker_amount: 100, // 100 quote
580            taker_amount: 50,  // for 50 base (price = 2 quote/base)
581            expiration: 0,
582            signature: [0u8; 64],
583        };
584
585        let sell_order = FullOrder {
586            nonce: 2,
587            maker: Pubkey::new_unique(),
588            market: buy_order.market,
589            base_mint: buy_order.base_mint,
590            quote_mint: buy_order.quote_mint,
591            side: OrderSide::Ask,
592            maker_amount: 50, // 50 base
593            taker_amount: 90, // for 90 quote (price = 1.8 quote/base)
594            expiration: 0,
595            signature: [0u8; 64],
596        };
597
598        // Buyer pays 2 quote/base, seller wants 1.8 quote/base - should cross
599        assert!(orders_can_cross(&buy_order, &sell_order));
600    }
601
602    #[test]
603    fn test_orders_cannot_cross() {
604        let buy_order = FullOrder {
605            nonce: 1,
606            maker: Pubkey::new_unique(),
607            market: Pubkey::new_unique(),
608            base_mint: Pubkey::new_unique(),
609            quote_mint: Pubkey::new_unique(),
610            side: OrderSide::Bid,
611            maker_amount: 50, // 50 quote
612            taker_amount: 50, // for 50 base (price = 1 quote/base)
613            expiration: 0,
614            signature: [0u8; 64],
615        };
616
617        let sell_order = FullOrder {
618            nonce: 2,
619            maker: Pubkey::new_unique(),
620            market: buy_order.market,
621            base_mint: buy_order.base_mint,
622            quote_mint: buy_order.quote_mint,
623            side: OrderSide::Ask,
624            maker_amount: 50,  // 50 base
625            taker_amount: 100, // for 100 quote (price = 2 quote/base)
626            expiration: 0,
627            signature: [0u8; 64],
628        };
629
630        // Buyer pays 1 quote/base, seller wants 2 quote/base - should not cross
631        assert!(!orders_can_cross(&buy_order, &sell_order));
632    }
633
634    #[test]
635    fn test_calculate_taker_fill() {
636        let maker_order = FullOrder {
637            nonce: 1,
638            maker: Pubkey::new_unique(),
639            market: Pubkey::new_unique(),
640            base_mint: Pubkey::new_unique(),
641            quote_mint: Pubkey::new_unique(),
642            side: OrderSide::Ask,
643            maker_amount: 100, // gives 100 base
644            taker_amount: 200, // wants 200 quote
645            expiration: 0,
646            signature: [0u8; 64],
647        };
648
649        // If filling 50 maker_amount, taker should get 50 * 200 / 100 = 100
650        let taker_fill = calculate_taker_fill(&maker_order, 50).unwrap();
651        assert_eq!(taker_fill, 100);
652    }
653
654    #[test]
655    #[cfg(feature = "client")]
656    fn test_to_submit_request() {
657        use solana_keypair::Keypair;
658        use solana_signer::Signer;
659
660        let keypair = Keypair::new();
661        let maker = keypair.pubkey();
662        let market = Pubkey::new_unique();
663        let base_mint = Pubkey::new_unique();
664        let quote_mint = Pubkey::new_unique();
665
666        let mut order = FullOrder {
667            nonce: 42,
668            maker,
669            market,
670            base_mint,
671            quote_mint,
672            side: OrderSide::Bid,
673            maker_amount: 1_000_000,
674            taker_amount: 500_000,
675            expiration: 1234567890,
676            signature: [0u8; 64],
677        };
678
679        order.sign(&keypair);
680
681        let request = order.to_submit_request("test_orderbook");
682
683        assert_eq!(request.maker, maker.to_string());
684        assert_eq!(request.nonce, 42);
685        assert_eq!(request.market_pubkey, market.to_string());
686        assert_eq!(request.base_token, base_mint.to_string());
687        assert_eq!(request.quote_token, quote_mint.to_string());
688        assert_eq!(request.side, 0); // Bid
689        assert_eq!(request.maker_amount, 1_000_000);
690        assert_eq!(request.taker_amount, 500_000);
691        assert_eq!(request.expiration, 1234567890);
692        assert_eq!(request.orderbook_id, "test_orderbook");
693        assert_eq!(request.signature.len(), 128); // 64 bytes = 128 hex chars
694    }
695
696    #[test]
697    fn test_derive_orderbook_id() {
698        let order = FullOrder {
699            nonce: 1,
700            maker: Pubkey::new_from_array([1u8; 32]),
701            market: Pubkey::new_from_array([2u8; 32]),
702            base_mint: Pubkey::new_from_array([3u8; 32]),
703            quote_mint: Pubkey::new_from_array([4u8; 32]),
704            side: OrderSide::Bid,
705            maker_amount: 100,
706            taker_amount: 50,
707            expiration: 0,
708            signature: [0u8; 64],
709        };
710
711        let orderbook_id = order.derive_orderbook_id();
712        // The orderbook ID should be first 8 chars of each pubkey string
713        let base_str = order.base_mint.to_string();
714        let quote_str = order.quote_mint.to_string();
715        let expected = format!("{}_{}", &base_str[..8], &quote_str[..8]);
716        assert_eq!(orderbook_id, expected);
717    }
718
719    #[test]
720    #[cfg(feature = "client")]
721    fn test_is_signed() {
722        use solana_keypair::Keypair;
723        use solana_signer::Signer;
724
725        let keypair = Keypair::new();
726        let mut order = FullOrder {
727            nonce: 1,
728            maker: keypair.pubkey(),
729            market: Pubkey::new_unique(),
730            base_mint: Pubkey::new_unique(),
731            quote_mint: Pubkey::new_unique(),
732            side: OrderSide::Bid,
733            maker_amount: 100,
734            taker_amount: 50,
735            expiration: 0,
736            signature: [0u8; 64],
737        };
738
739        assert!(!order.is_signed());
740
741        order.sign(&keypair);
742
743        assert!(order.is_signed());
744    }
745
746    #[test]
747    #[cfg(feature = "client")]
748    fn test_signature_and_hash_hex() {
749        use solana_keypair::Keypair;
750        use solana_signer::Signer;
751
752        let keypair = Keypair::new();
753        let mut order = FullOrder {
754            nonce: 1,
755            maker: keypair.pubkey(),
756            market: Pubkey::new_unique(),
757            base_mint: Pubkey::new_unique(),
758            quote_mint: Pubkey::new_unique(),
759            side: OrderSide::Bid,
760            maker_amount: 100,
761            taker_amount: 50,
762            expiration: 0,
763            signature: [0u8; 64],
764        };
765
766        order.sign(&keypair);
767
768        let sig_hex = order.signature_hex();
769        let hash_hex = order.hash_hex();
770
771        // Signature should be 128 hex chars (64 bytes)
772        assert_eq!(sig_hex.len(), 128);
773        // Hash should be 64 hex chars (32 bytes)
774        assert_eq!(hash_hex.len(), 64);
775
776        // Verify they are valid hex
777        assert!(hex::decode(&sig_hex).is_ok());
778        assert!(hex::decode(&hash_hex).is_ok());
779    }
780
781    #[test]
782    #[should_panic(expected = "Order must be signed before converting to submit request")]
783    fn test_to_submit_request_panics_unsigned() {
784        let order = FullOrder {
785            nonce: 1,
786            maker: Pubkey::new_unique(),
787            market: Pubkey::new_unique(),
788            base_mint: Pubkey::new_unique(),
789            quote_mint: Pubkey::new_unique(),
790            side: OrderSide::Bid,
791            maker_amount: 100,
792            taker_amount: 50,
793            expiration: 0,
794            signature: [0u8; 64],
795        };
796
797        order.to_submit_request("test_orderbook");
798    }
799}