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