Skip to main content

lightcone_sdk/program/
client.rs

1//! Async client for interacting with the Lightcone Pinocchio program.
2//!
3//! This module provides the main SDK client with account fetching and
4//! transaction building capabilities.
5
6use solana_client::nonblocking::rpc_client::RpcClient;
7use solana_commitment_config::CommitmentConfig;
8use solana_sdk::{
9    hash::Hash,
10    pubkey::Pubkey,
11    signature::Keypair,
12    transaction::Transaction,
13};
14
15use crate::program::accounts::{Exchange, Market, OrderStatus, Position, UserNonce};
16use crate::program::constants::PROGRAM_ID;
17use crate::program::ed25519::{create_cross_ref_ed25519_instructions, create_order_verify_instruction};
18use crate::program::error::{SdkError, SdkResult};
19use crate::program::instructions::*;
20use crate::program::orders::{derive_condition_id, FullOrder};
21use crate::program::pda::{
22    get_all_conditional_mint_pdas, get_exchange_pda, get_market_pda, get_order_status_pda,
23    get_position_pda, get_user_nonce_pda, Pda,
24};
25use crate::program::types::*;
26
27/// Client for interacting with the Lightcone Pinocchio program.
28pub struct LightconePinocchioClient {
29    /// RPC client for Solana
30    pub rpc_client: RpcClient,
31    /// Program ID
32    pub program_id: Pubkey,
33}
34
35impl LightconePinocchioClient {
36    /// Create a new client with default program ID.
37    pub fn new(rpc_url: &str) -> Self {
38        Self {
39            rpc_client: RpcClient::new_with_commitment(
40                rpc_url.to_string(),
41                CommitmentConfig::confirmed(),
42            ),
43            program_id: *PROGRAM_ID,
44        }
45    }
46
47    /// Create a new client with custom program ID.
48    pub fn with_program_id(rpc_url: &str, program_id: Pubkey) -> Self {
49        Self {
50            rpc_client: RpcClient::new_with_commitment(
51                rpc_url.to_string(),
52                CommitmentConfig::confirmed(),
53            ),
54            program_id,
55        }
56    }
57
58    /// Create a new client with existing RpcClient.
59    pub fn from_rpc_client(rpc_client: RpcClient) -> Self {
60        Self {
61            rpc_client,
62            program_id: *PROGRAM_ID,
63        }
64    }
65
66    /// Get PDA derivation helpers.
67    pub fn pda(&self) -> &Pda {
68        &Pda
69    }
70
71    // ========================================================================
72    // Account Fetchers
73    // ========================================================================
74
75    /// Fetch the Exchange account.
76    pub async fn get_exchange(&self) -> SdkResult<Exchange> {
77        let (pda, _) = get_exchange_pda(&self.program_id);
78        let account = self
79            .rpc_client
80            .get_account(&pda)
81            .await
82            .map_err(|e| SdkError::AccountNotFound(format!("Exchange: {}", e)))?;
83        Exchange::deserialize(&account.data)
84    }
85
86    /// Fetch a Market account by ID.
87    pub async fn get_market(&self, market_id: u64) -> SdkResult<Market> {
88        let (pda, _) = get_market_pda(market_id, &self.program_id);
89        self.get_market_by_pubkey(&pda).await
90    }
91
92    /// Fetch a Market account by pubkey.
93    pub async fn get_market_by_pubkey(&self, market: &Pubkey) -> SdkResult<Market> {
94        let account = self
95            .rpc_client
96            .get_account(market)
97            .await
98            .map_err(|e| SdkError::AccountNotFound(format!("Market: {}", e)))?;
99        Market::deserialize(&account.data)
100    }
101
102    /// Fetch a Position account (returns None if not found).
103    pub async fn get_position(
104        &self,
105        owner: &Pubkey,
106        market: &Pubkey,
107    ) -> SdkResult<Option<Position>> {
108        let (pda, _) = get_position_pda(owner, market, &self.program_id);
109        match self.rpc_client.get_account(&pda).await {
110            Ok(account) => Ok(Some(Position::deserialize(&account.data)?)),
111            Err(_) => Ok(None),
112        }
113    }
114
115    /// Fetch an OrderStatus account (returns None if not found).
116    pub async fn get_order_status(&self, order_hash: &[u8; 32]) -> SdkResult<Option<OrderStatus>> {
117        let (pda, _) = get_order_status_pda(order_hash, &self.program_id);
118        match self.rpc_client.get_account(&pda).await {
119            Ok(account) => Ok(Some(OrderStatus::deserialize(&account.data)?)),
120            Err(_) => Ok(None),
121        }
122    }
123
124    /// Fetch a user's current nonce (returns 0 if not initialized).
125    pub async fn get_user_nonce(&self, user: &Pubkey) -> SdkResult<u64> {
126        let (pda, _) = get_user_nonce_pda(user, &self.program_id);
127        match self.rpc_client.get_account(&pda).await {
128            Ok(account) => {
129                let user_nonce = UserNonce::deserialize(&account.data)?;
130                Ok(user_nonce.nonce)
131            }
132            Err(_) => Ok(0),
133        }
134    }
135
136    /// Get the next available nonce for a user (the current stored nonce value).
137    ///
138    /// Orders should be signed with this nonce value.
139    /// Call `increment_nonce` to invalidate orders with the current nonce.
140    pub async fn get_next_nonce(&self, user: &Pubkey) -> SdkResult<u64> {
141        self.get_user_nonce(user).await
142    }
143
144    /// Get the next available market ID.
145    pub async fn get_next_market_id(&self) -> SdkResult<u64> {
146        let exchange = self.get_exchange().await?;
147        Ok(exchange.market_count)
148    }
149
150    // ========================================================================
151    // Transaction Builders
152    // ========================================================================
153
154    /// Get the latest blockhash for transaction building.
155    pub async fn get_latest_blockhash(&self) -> SdkResult<Hash> {
156        self.rpc_client
157            .get_latest_blockhash()
158            .await
159            .map_err(SdkError::Rpc)
160    }
161
162    /// Build Initialize transaction.
163    pub async fn initialize(&self, authority: &Pubkey) -> SdkResult<Transaction> {
164        let ix = build_initialize_ix(authority, &self.program_id);
165        Ok(Transaction::new_with_payer(&[ix], Some(authority)))
166    }
167
168    /// Build CreateMarket transaction.
169    pub async fn create_market(&self, params: CreateMarketParams) -> SdkResult<Transaction> {
170        let market_id = self.get_next_market_id().await?;
171        let ix = build_create_market_ix(&params, market_id, &self.program_id)?;
172        Ok(Transaction::new_with_payer(&[ix], Some(&params.authority)))
173    }
174
175    /// Build AddDepositMint transaction.
176    pub async fn add_deposit_mint(
177        &self,
178        params: AddDepositMintParams,
179        market: &Pubkey,
180        num_outcomes: u8,
181    ) -> SdkResult<Transaction> {
182        let ix = build_add_deposit_mint_ix(&params, market, num_outcomes, &self.program_id)?;
183        Ok(Transaction::new_with_payer(&[ix], Some(&params.payer)))
184    }
185
186    /// Build MintCompleteSet transaction.
187    pub async fn mint_complete_set(
188        &self,
189        params: MintCompleteSetParams,
190        num_outcomes: u8,
191    ) -> SdkResult<Transaction> {
192        let ix = build_mint_complete_set_ix(&params, num_outcomes, &self.program_id);
193        Ok(Transaction::new_with_payer(&[ix], Some(&params.user)))
194    }
195
196    /// Build MergeCompleteSet transaction.
197    pub async fn merge_complete_set(
198        &self,
199        params: MergeCompleteSetParams,
200        num_outcomes: u8,
201    ) -> SdkResult<Transaction> {
202        let ix = build_merge_complete_set_ix(&params, num_outcomes, &self.program_id);
203        Ok(Transaction::new_with_payer(&[ix], Some(&params.user)))
204    }
205
206    /// Build CancelOrder transaction.
207    pub async fn cancel_order(
208        &self,
209        maker: &Pubkey,
210        order: &FullOrder,
211    ) -> SdkResult<Transaction> {
212        let ix = build_cancel_order_ix(maker, order, &self.program_id);
213        Ok(Transaction::new_with_payer(&[ix], Some(maker)))
214    }
215
216    /// Build IncrementNonce transaction.
217    pub async fn increment_nonce(&self, user: &Pubkey) -> SdkResult<Transaction> {
218        let ix = build_increment_nonce_ix(user, &self.program_id);
219        Ok(Transaction::new_with_payer(&[ix], Some(user)))
220    }
221
222    /// Build SettleMarket transaction.
223    pub async fn settle_market(&self, params: SettleMarketParams) -> SdkResult<Transaction> {
224        let ix = build_settle_market_ix(&params, &self.program_id);
225        Ok(Transaction::new_with_payer(&[ix], Some(&params.oracle)))
226    }
227
228    /// Build RedeemWinnings transaction.
229    pub async fn redeem_winnings(
230        &self,
231        params: RedeemWinningsParams,
232        winning_outcome: u8,
233    ) -> SdkResult<Transaction> {
234        let ix = build_redeem_winnings_ix(&params, winning_outcome, &self.program_id);
235        Ok(Transaction::new_with_payer(&[ix], Some(&params.user)))
236    }
237
238    /// Build SetPaused transaction.
239    pub async fn set_paused(
240        &self,
241        authority: &Pubkey,
242        paused: bool,
243    ) -> SdkResult<Transaction> {
244        let ix = build_set_paused_ix(authority, paused, &self.program_id);
245        Ok(Transaction::new_with_payer(&[ix], Some(authority)))
246    }
247
248    /// Build SetOperator transaction.
249    pub async fn set_operator(
250        &self,
251        authority: &Pubkey,
252        new_operator: &Pubkey,
253    ) -> SdkResult<Transaction> {
254        let ix = build_set_operator_ix(authority, new_operator, &self.program_id);
255        Ok(Transaction::new_with_payer(&[ix], Some(authority)))
256    }
257
258    /// Build WithdrawFromPosition transaction.
259    pub async fn withdraw_from_position(
260        &self,
261        params: WithdrawFromPositionParams,
262        is_token_2022: bool,
263    ) -> SdkResult<Transaction> {
264        let ix = build_withdraw_from_position_ix(&params, is_token_2022, &self.program_id);
265        Ok(Transaction::new_with_payer(&[ix], Some(&params.user)))
266    }
267
268    /// Build ActivateMarket transaction.
269    pub async fn activate_market(&self, params: ActivateMarketParams) -> SdkResult<Transaction> {
270        let ix = build_activate_market_ix(&params, &self.program_id);
271        Ok(Transaction::new_with_payer(&[ix], Some(&params.authority)))
272    }
273
274    /// Build MatchOrdersMulti transaction without Ed25519 verify instructions.
275    ///
276    /// Note: This requires Ed25519 verification instructions to be added separately
277    /// before the match instruction.
278    pub async fn match_orders_multi(
279        &self,
280        params: MatchOrdersMultiParams,
281    ) -> SdkResult<Transaction> {
282        let ix = build_match_orders_multi_ix(&params, &self.program_id)?;
283        Ok(Transaction::new_with_payer(&[ix], Some(&params.operator)))
284    }
285
286    /// Build MatchOrdersMulti transaction with Ed25519 verify instructions.
287    ///
288    /// Uses individual Ed25519 verify instructions (one per signature).
289    pub async fn match_orders_multi_with_verify(
290        &self,
291        params: MatchOrdersMultiParams,
292    ) -> SdkResult<Transaction> {
293        let mut instructions = Vec::new();
294
295        // Add taker Ed25519 verify instruction
296        instructions.push(create_order_verify_instruction(&params.taker_order));
297
298        // Add maker Ed25519 verify instructions
299        for maker_order in &params.maker_orders {
300            instructions.push(create_order_verify_instruction(maker_order));
301        }
302
303        // Add match orders instruction
304        let match_ix = build_match_orders_multi_ix(&params, &self.program_id)?;
305        instructions.push(match_ix);
306
307        Ok(Transaction::new_with_payer(
308            &instructions,
309            Some(&params.operator),
310        ))
311    }
312
313    /// Build MatchOrdersMulti transaction with cross-reference Ed25519 verification.
314    ///
315    /// This is the most space-efficient approach - Ed25519 instructions reference
316    /// data in the match instruction instead of duplicating it.
317    pub async fn match_orders_multi_cross_ref(
318        &self,
319        params: MatchOrdersMultiParams,
320    ) -> SdkResult<Transaction> {
321        let num_makers = params.maker_orders.len();
322
323        // Match instruction will be at index (1 + num_makers)
324        let match_ix_index = (1 + num_makers) as u16;
325
326        // Add Ed25519 cross-ref verify instructions
327        let mut instructions = create_cross_ref_ed25519_instructions(num_makers, match_ix_index);
328
329        // Add match orders instruction
330        let match_ix = build_match_orders_multi_ix(&params, &self.program_id)?;
331        instructions.push(match_ix);
332
333        Ok(Transaction::new_with_payer(
334            &instructions,
335            Some(&params.operator),
336        ))
337    }
338
339    // ========================================================================
340    // Order Helpers
341    // ========================================================================
342
343    /// Create an unsigned bid order.
344    pub fn create_bid_order(&self, params: BidOrderParams) -> FullOrder {
345        FullOrder::new_bid(params)
346    }
347
348    /// Create an unsigned ask order.
349    pub fn create_ask_order(&self, params: AskOrderParams) -> FullOrder {
350        FullOrder::new_ask(params)
351    }
352
353    /// Create and sign a bid order.
354    pub fn create_signed_bid_order(&self, params: BidOrderParams, keypair: &Keypair) -> FullOrder {
355        FullOrder::new_bid_signed(params, keypair)
356    }
357
358    /// Create and sign an ask order.
359    pub fn create_signed_ask_order(&self, params: AskOrderParams, keypair: &Keypair) -> FullOrder {
360        FullOrder::new_ask_signed(params, keypair)
361    }
362
363    /// Compute the hash of an order.
364    pub fn hash_order(&self, order: &FullOrder) -> [u8; 32] {
365        order.hash()
366    }
367
368    /// Sign an order with the given keypair.
369    pub fn sign_order(&self, order: &mut FullOrder, keypair: &Keypair) {
370        order.sign(keypair);
371    }
372
373    // ========================================================================
374    // Utility Functions
375    // ========================================================================
376
377    /// Derive the condition ID for a market.
378    pub fn derive_condition_id(
379        &self,
380        oracle: &Pubkey,
381        question_id: &[u8; 32],
382        num_outcomes: u8,
383    ) -> [u8; 32] {
384        derive_condition_id(oracle, question_id, num_outcomes)
385    }
386
387    /// Get all conditional mint pubkeys for a market.
388    pub fn get_conditional_mints(
389        &self,
390        market: &Pubkey,
391        deposit_mint: &Pubkey,
392        num_outcomes: u8,
393    ) -> Vec<Pubkey> {
394        get_all_conditional_mint_pdas(market, deposit_mint, num_outcomes, &self.program_id)
395            .into_iter()
396            .map(|(pubkey, _)| pubkey)
397            .collect()
398    }
399
400    /// Get the Exchange PDA.
401    pub fn get_exchange_pda(&self) -> Pubkey {
402        get_exchange_pda(&self.program_id).0
403    }
404
405    /// Get a Market PDA.
406    pub fn get_market_pda(&self, market_id: u64) -> Pubkey {
407        get_market_pda(market_id, &self.program_id).0
408    }
409
410    /// Get a Position PDA.
411    pub fn get_position_pda(&self, owner: &Pubkey, market: &Pubkey) -> Pubkey {
412        get_position_pda(owner, market, &self.program_id).0
413    }
414
415    /// Get an Order Status PDA.
416    pub fn get_order_status_pda(&self, order_hash: &[u8; 32]) -> Pubkey {
417        get_order_status_pda(order_hash, &self.program_id).0
418    }
419
420    /// Get a User Nonce PDA.
421    pub fn get_user_nonce_pda(&self, user: &Pubkey) -> Pubkey {
422        get_user_nonce_pda(user, &self.program_id).0
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_client_creation() {
432        let client = LightconePinocchioClient::new("https://api.devnet.solana.com");
433        assert_eq!(client.program_id, *PROGRAM_ID);
434    }
435
436    #[test]
437    fn test_client_with_custom_program_id() {
438        let custom_id = Pubkey::new_unique();
439        let client =
440            LightconePinocchioClient::with_program_id("https://api.devnet.solana.com", custom_id);
441        assert_eq!(client.program_id, custom_id);
442    }
443
444    #[test]
445    fn test_pda_helpers() {
446        let client = LightconePinocchioClient::new("https://api.devnet.solana.com");
447
448        let exchange_pda = client.get_exchange_pda();
449        assert_ne!(exchange_pda, Pubkey::default());
450
451        let market_pda = client.get_market_pda(0);
452        assert_ne!(market_pda, Pubkey::default());
453
454        let owner = Pubkey::new_unique();
455        let market = Pubkey::new_unique();
456        let position_pda = client.get_position_pda(&owner, &market);
457        assert_ne!(position_pda, Pubkey::default());
458    }
459
460    #[test]
461    fn test_create_bid_order() {
462        let client = LightconePinocchioClient::new("https://api.devnet.solana.com");
463
464        let params = BidOrderParams {
465            nonce: 1,
466            maker: Pubkey::new_unique(),
467            market: Pubkey::new_unique(),
468            base_mint: Pubkey::new_unique(),
469            quote_mint: Pubkey::new_unique(),
470            maker_amount: 1000,
471            taker_amount: 500,
472            expiration: 0,
473        };
474
475        let order = client.create_bid_order(params.clone());
476        assert_eq!(order.nonce, params.nonce);
477        assert_eq!(order.maker, params.maker);
478        assert_eq!(order.maker_amount, params.maker_amount);
479    }
480
481    #[test]
482    fn test_condition_id_derivation() {
483        let client = LightconePinocchioClient::new("https://api.devnet.solana.com");
484
485        let oracle = Pubkey::new_unique();
486        let question_id = [1u8; 32];
487        let num_outcomes = 3;
488
489        let condition_id1 = client.derive_condition_id(&oracle, &question_id, num_outcomes);
490        let condition_id2 = client.derive_condition_id(&oracle, &question_id, num_outcomes);
491
492        assert_eq!(condition_id1, condition_id2);
493    }
494
495    #[test]
496    fn test_get_conditional_mints() {
497        let client = LightconePinocchioClient::new("https://api.devnet.solana.com");
498
499        let market = Pubkey::new_unique();
500        let deposit_mint = Pubkey::new_unique();
501
502        let mints = client.get_conditional_mints(&market, &deposit_mint, 3);
503        assert_eq!(mints.len(), 3);
504
505        // All mints should be unique
506        assert_ne!(mints[0], mints[1]);
507        assert_ne!(mints[1], mints[2]);
508        assert_ne!(mints[0], mints[2]);
509    }
510}