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