lightspeed_sdk/
client.rs

1// src/client.rs
2#![allow(deprecated)]
3
4use crate::{LightspeedConfig, LightspeedError, Priority, TransactionResult};
5use solana_sdk::{
6    instruction::Instruction,
7    pubkey::Pubkey,
8    signature::Signature,
9    system_instruction,
10    transaction::Transaction,
11    signer::Signer,
12};
13use std::str::FromStr;
14use std::sync::Arc;
15use tokio::sync::Mutex;
16use url::Url;
17use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
18
19/// Lightspeed tip recipient address
20/// 
21/// All tip transactions are sent to this address to enable prioritized processing.
22pub const LIGHTSPEED_TIP_ADDRESS: &str = "53PhM3UTdMQWu5t81wcd35AHGc5xpmHoRjem7GQPvXjA";
23
24/// Minimum tip amount in lamports (0.0001 SOL)
25/// 
26/// Transactions with tips below this amount will be rejected to ensure
27/// meaningful prioritization.
28pub const MIN_TIP_LAMPORTS: u64 = 100_000;
29
30/// Lightspeed RPC client for prioritized transaction processing
31/// 
32/// The client handles authentication, tip management, and connection maintenance
33/// for interacting with the Lightspeed service. API keys are securely transmitted
34/// via the Authorization header on all requests.
35/// 
36/// ## Example
37/// 
38/// ```rust
39/// use lightspeed_sdk::{LightspeedClientBuilder, Priority};
40/// use solana_sdk::{signature::Keypair, signer::Signer};
41/// 
42/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
43/// let client = LightspeedClientBuilder::new("your-api-key")
44///     .svs_rpc_url("https://basic.rpc.solanavibestation.com") 
45///     .build()?;
46/// 
47/// // Send a transaction with automatic tip injection
48/// let payer = Keypair::new();
49/// // ... create instructions ...
50/// # Ok(())
51/// # }
52/// ```
53pub struct LightspeedClient {
54    pub(crate) config: LightspeedConfig,
55    http_client: reqwest::Client,
56    endpoint: Url,
57    tip_pubkey: Pubkey,
58    keep_alive_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
59}
60
61impl LightspeedClient {
62    /// Creates a new Lightspeed client with the provided configuration
63    /// 
64    /// ## Arguments
65    /// 
66    /// * `config` - Client configuration including API key, tier, and settings
67    /// 
68    /// ## Errors
69    /// 
70    /// Returns an error if:
71    /// - The API key is empty
72    /// - The endpoint URL is invalid
73    /// - HTTP client initialization fails
74    pub fn new(config: LightspeedConfig) -> Result<Self, LightspeedError> {
75        if config.api_key.is_empty() {
76            return Err(LightspeedError::InvalidApiKey);
77        }
78
79        // Get the endpoint URL from config (handles both custom and SVS URLs)
80        let endpoint = config.get_endpoint()?;
81
82        let tip_pubkey = Pubkey::from_str(LIGHTSPEED_TIP_ADDRESS)
83            .expect("Invalid tip address constant");
84
85        // Configure HTTP client with authentication headers
86        let mut headers = reqwest::header::HeaderMap::new();
87        headers.insert(
88            reqwest::header::AUTHORIZATION,
89            reqwest::header::HeaderValue::from_str(&config.api_key)
90                .map_err(|_| LightspeedError::InvalidApiKey)?
91        );
92        headers.insert(
93            reqwest::header::CONTENT_TYPE,
94            reqwest::header::HeaderValue::from_static("application/json")
95        );
96        headers.insert(
97            reqwest::header::USER_AGENT,
98            reqwest::header::HeaderValue::from_static("lightspeed-sdk-rust/0.1.0")
99        );
100
101        let http_client = reqwest::Client::builder()
102            .default_headers(headers)
103            .timeout(config.timeout)
104            .build()?;
105
106        if config.debug {
107            log::debug!("Lightspeed endpoint configured: {}", endpoint);
108        }
109
110        Ok(Self {
111            config,
112            http_client,
113            endpoint,
114            tip_pubkey,
115            keep_alive_handle: Arc::new(Mutex::new(None)),
116        })
117    }
118    
119    /// Starts automatic keep-alive to maintain connection health
120    /// 
121    /// Spawns a background task that periodically sends keep-alive requests
122    /// to prevent connection timeouts. The interval is configured via
123    /// `LightspeedClientBuilder::keep_alive_interval()`.
124    /// 
125    /// ## Example
126    /// 
127    /// ```rust
128    /// # async fn example(client: lightspeed_sdk::LightspeedClient) -> Result<(), Box<dyn std::error::Error>> {
129    /// client.start_keep_alive().await?;
130    /// // Connection will be maintained automatically
131    /// # Ok(())
132    /// # }
133    /// ```
134    /// 
135    /// ## Errors
136    /// 
137    /// Returns `LightspeedError::KeepAliveAlreadyRunning` if keep-alive is already active.
138    pub async fn start_keep_alive(&self) -> Result<(), LightspeedError> {
139        let mut handle_guard = self.keep_alive_handle.lock().await;
140        
141        if handle_guard.is_some() {
142            if self.config.debug {
143                log::debug!("Keep-alive already running");
144            }
145            return Err(LightspeedError::KeepAliveAlreadyRunning);
146        }
147        
148        let client = self.clone_for_keep_alive();
149        let interval_duration = self.config.keep_alive_interval;
150
151        let task = tokio::spawn(async move {
152            let mut interval = tokio::time::interval(interval_duration);
153            loop {
154                interval.tick().await;
155                if let Err(e) = client.keep_alive().await {
156                    log::warn!("Keep-alive failed: {:?}", e);
157                }
158            }
159        });
160
161        *handle_guard = Some(task);
162        
163        if self.config.debug {
164            log::debug!("Keep-alive task started with interval {:?}", interval_duration);
165        }
166        
167        Ok(())
168    }
169
170    /// Sends a transaction with automatic tip injection using the default priority
171    /// 
172    /// A tip instruction is automatically appended to your transaction to ensure
173    /// prioritized processing. The tip amount is determined by the client's
174    /// default priority setting.
175    /// 
176    /// ## Arguments
177    /// 
178    /// * `instructions` - Transaction instructions to execute
179    /// * `payer` - Account paying for transaction fees and tip
180    /// * `signers` - All required transaction signers
181    /// * `recent_blockhash` - Recent blockhash from the cluster
182    /// 
183    /// ## Returns
184    /// 
185    /// Returns a `TransactionResult` containing the signature and tip amount.
186    /// 
187    /// ## Example
188    /// 
189    /// ```rust
190    /// # async fn example(client: lightspeed_sdk::LightspeedClient) -> Result<(), Box<dyn std::error::Error>> {
191    /// # use solana_sdk::{signature::Keypair, signer::Signer, system_instruction, hash::Hash};
192    /// let payer = Keypair::new();
193    /// let recipient = solana_sdk::pubkey::Pubkey::new_unique();
194    /// 
195    /// let instruction = system_instruction::transfer(
196    ///     &payer.pubkey(),
197    ///     &recipient,
198    ///     1_000_000,
199    /// );
200    /// 
201    /// let result = client.send_transaction(
202    ///     vec![instruction],
203    ///     &payer.pubkey(),
204    ///     &[&payer],
205    ///     Hash::default(), // Use real blockhash in production
206    /// ).await?;
207    /// 
208    /// println!("Transaction: {}", result.signature);
209    /// println!("Tip paid: {} lamports", result.tip_amount);
210    /// # Ok(())
211    /// # }
212    /// ```
213    pub async fn send_transaction<T: Signer>(
214        &self,
215        instructions: Vec<Instruction>,
216        payer: &Pubkey,
217        signers: &[&T],
218        recent_blockhash: solana_sdk::hash::Hash,
219    ) -> Result<TransactionResult, LightspeedError> {
220        self.send_transaction_with_priority(
221            instructions,
222            payer,
223            signers,
224            recent_blockhash,
225            self.config.default_priority
226        ).await
227    }
228
229    /// Sends a transaction with a specific priority level
230    /// 
231    /// Similar to `send_transaction` but allows overriding the default priority
232    /// for this specific transaction.
233    /// 
234    /// ## Arguments
235    /// 
236    /// * `instructions` - Transaction instructions to execute
237    /// * `payer` - Account paying for transaction fees and tip
238    /// * `signers` - All required transaction signers
239    /// * `recent_blockhash` - Recent blockhash from the cluster
240    /// * `priority` - Priority level for this transaction
241    /// 
242    /// ## Priority Levels
243    /// 
244    /// - `Priority::Minimum` - 0.0001 SOL tip
245    /// - `Priority::Standard` - 0.001 SOL tip
246    /// - `Priority::Rush` - 0.005 SOL tip
247    /// - `Priority::Custom(lamports)` - Custom tip amount
248    pub async fn send_transaction_with_priority<T: Signer>(
249        &self,
250        mut instructions: Vec<Instruction>,
251        payer: &Pubkey,
252        signers: &[&T],
253        recent_blockhash: solana_sdk::hash::Hash,
254        priority: Priority,
255    ) -> Result<TransactionResult, LightspeedError> {
256        let tip_amount = priority.to_lamports();
257
258        if tip_amount < MIN_TIP_LAMPORTS {
259            return Err(LightspeedError::TipBelowMinimum(tip_amount, MIN_TIP_LAMPORTS));
260        }
261
262        // Append tip instruction
263        let tip_instruction = system_instruction::transfer(
264            payer,
265            &self.tip_pubkey,
266            tip_amount,
267        );
268        instructions.push(tip_instruction);
269        
270        // Build and sign transaction
271        let mut transaction = Transaction::new_with_payer(
272            &instructions,
273            Some(payer),
274        );
275        transaction.sign(signers, recent_blockhash);
276
277        // Submit to Lightspeed
278        let signature = self.send_transaction_internal(&transaction).await?;
279
280        Ok(TransactionResult {
281            signature,
282            tip_amount,
283        })
284    }
285
286    /// Sends a transaction with a custom tip amount
287    /// 
288    /// Provides direct control over the tip amount in lamports.
289    /// 
290    /// ## Arguments
291    /// 
292    /// * `instructions` - Transaction instructions to execute
293    /// * `payer` - Account paying for transaction fees and tip
294    /// * `signers` - All required transaction signers
295    /// * `recent_blockhash` - Recent blockhash from the cluster
296    /// * `tip_lamports` - Tip amount in lamports
297    pub async fn send_transaction_with_tip<T: Signer>(
298        &self,
299        instructions: Vec<Instruction>,
300        payer: &Pubkey,
301        signers: &[&T],
302        recent_blockhash: solana_sdk::hash::Hash,
303        tip_lamports: u64,
304    ) -> Result<TransactionResult, LightspeedError> {
305        self.send_transaction_with_priority(
306            instructions,
307            payer,
308            signers,
309            recent_blockhash,
310            Priority::Custom(tip_lamports),
311        ).await
312    }
313
314    /// Creates a tip instruction using the default priority
315    /// 
316    /// Use this when manually constructing transactions that need tip instructions.
317    /// 
318    /// ## Arguments
319    /// 
320    /// * `payer` - Account that will pay the tip
321    pub fn create_tip_instruction(&self, payer: &Pubkey) -> Instruction {
322        let tip_amount = self.config.default_priority.to_lamports();
323        system_instruction::transfer(
324            payer,
325            &self.tip_pubkey,
326            tip_amount,
327        )
328    }
329
330    /// Creates a tip instruction with a specific priority
331    /// 
332    /// ## Arguments
333    /// 
334    /// * `payer` - Account that will pay the tip
335    /// * `priority` - Priority level determining tip amount
336    pub fn create_tip_instruction_with_priority(&self, payer: &Pubkey, priority: Priority) -> Instruction {
337        let tip_amount = priority.to_lamports();
338        system_instruction::transfer(
339            payer,
340            &self.tip_pubkey,
341            tip_amount,
342        )
343    }
344
345    /// Creates a tip instruction with a custom amount
346    /// 
347    /// ## Arguments
348    /// 
349    /// * `payer` - Account that will pay the tip
350    /// * `tip_lamports` - Tip amount in lamports
351    pub fn create_tip_instruction_with_tip(&self, payer: &Pubkey, tip_lamports: u64) -> Instruction {
352        system_instruction::transfer(
353            payer,
354            &self.tip_pubkey,
355            tip_lamports,
356        )
357    }
358
359    /// Sends a pre-built transaction through Lightspeed
360    /// 
361    /// The transaction should already include a tip instruction. This method
362    /// provides direct control for advanced use cases.
363    /// 
364    /// ## Arguments
365    /// 
366    /// * `transaction` - Signed transaction including tip instruction
367    /// 
368    /// ## Example
369    /// 
370    /// ```rust
371    /// # async fn example(client: lightspeed_sdk::LightspeedClient) -> Result<(), Box<dyn std::error::Error>> {
372    /// # use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction, hash::Hash};
373    /// let payer = Keypair::new();
374    /// 
375    /// // Build transaction with tip
376    /// let tip = client.create_tip_instruction(&payer.pubkey());
377    /// let mut tx = Transaction::new_with_payer(
378    ///     &[tip],
379    ///     Some(&payer.pubkey()),
380    /// );
381    /// tx.sign(&[&payer], Hash::default());
382    /// 
383    /// // Send through Lightspeed
384    /// let signature = client.send_prebuilt_transaction(&tx).await?;
385    /// # Ok(())
386    /// # }
387    /// ```
388    pub async fn send_prebuilt_transaction(
389        &self,
390        transaction: &Transaction,
391    ) -> Result<Signature, LightspeedError> {
392        if self.config.debug {
393            log::debug!("Sending transaction through Lightspeed");
394        }
395        self.send_transaction_internal(transaction).await
396    }
397
398    /// Updates the tip recipient address
399    /// 
400    /// ## Arguments
401    /// 
402    /// * `new_tip_address` - New tip address as a base58 string
403    /// 
404    /// ## Errors
405    /// 
406    /// Returns an error if the address is not a valid Solana public key.
407    pub fn set_tip_address(&mut self, new_tip_address: &str) -> Result<(), LightspeedError> {
408        let new_pubkey = Pubkey::from_str(new_tip_address)
409            .map_err(|_| LightspeedError::InvalidTipAddress(
410                new_tip_address.to_string()
411            ))?;
412        
413        self.tip_pubkey = new_pubkey;
414        
415        if self.config.debug {
416            log::debug!("Updated tip address to: {}", new_tip_address);
417        }
418        
419        Ok(())
420    }
421    
422    /// Returns the current tip recipient address
423    pub fn get_tip_address(&self) -> Pubkey {
424        self.tip_pubkey
425    }
426
427    /// Internal transaction submission handler
428    async fn send_transaction_internal(
429        &self,
430        transaction: &Transaction,
431    ) -> Result<Signature, LightspeedError> {
432        // Serialize transaction
433        let tx_bytes = bincode::serialize(&transaction)
434            .map_err(|e| LightspeedError::TransactionFailed(e.to_string()))?;
435        
436        // Encode as base64
437        let encoded = BASE64.encode(&tx_bytes);
438        
439        let request = serde_json::json!({
440            "jsonrpc": "2.0",
441            "id": 1,
442            "method": "sendTransaction",
443            "params": [
444                encoded,
445                {
446                    "skipPreflight": true,
447                    "encoding": "base64"
448                }
449            ]
450        });
451
452        if self.config.debug {
453            log::debug!("Sending transaction to endpoint: {}", self.endpoint);
454        }
455
456        let response = self.http_client
457            .post(self.endpoint.clone())
458            .json(&request)
459            .send()
460            .await?;
461
462        let response_text = response.text().await?;
463        
464        if self.config.debug {
465            log::debug!("Raw response: {}", response_text);
466        }
467
468        // Parse response
469        let result: serde_json::Value = serde_json::from_str(&response_text)
470            .map_err(|e| {
471                if self.config.debug {
472                    log::error!("Failed to parse response as JSON: {}", response_text);
473                }
474                LightspeedError::TransactionFailed(format!("Invalid JSON response: {}", e))
475            })?;
476        
477        if let Some(error) = result.get("error") {
478            return Err(LightspeedError::TransactionFailed(
479                error.to_string()
480            ));
481        }
482
483        let sig_str = result["result"]
484            .as_str()
485            .ok_or_else(|| LightspeedError::TransactionFailed("Invalid response".to_string()))?;
486
487        Signature::from_str(sig_str)
488            .map_err(|_| LightspeedError::TransactionFailed("Invalid signature".to_string()))
489    }
490
491    /// Sends a keep-alive request to maintain connection
492    /// 
493    /// This is called automatically when keep-alive is enabled via `start_keep_alive()`.
494    /// Can also be called manually if needed.
495    pub async fn keep_alive(&self) -> Result<(), LightspeedError> {
496        let request = serde_json::json!({
497            "jsonrpc": "2.0",
498            "id": 1,
499            "method": "getHealth"
500        });
501
502        if self.config.debug {
503            log::debug!("Sending keep-alive");
504        }
505
506        self.http_client
507            .post(self.endpoint.clone())
508            .json(&request)
509            .send()
510            .await?;
511
512        Ok(())
513    }
514
515    /// Creates a lightweight clone for the keep-alive task
516    fn clone_for_keep_alive(&self) -> Self {
517        Self {
518            config: self.config.clone(),
519            http_client: self.http_client.clone(),
520            endpoint: self.endpoint.clone(),
521            tip_pubkey: self.tip_pubkey,
522            keep_alive_handle: Arc::new(Mutex::new(None)),
523        }
524    }
525
526    /// Stops the automatic keep-alive task
527    /// 
528    /// ## Returns
529    /// 
530    /// Returns `true` if a keep-alive task was running and has been stopped,
531    /// `false` if no task was running.
532    /// 
533    /// ## Example
534    /// 
535    /// ```rust
536    /// # async fn example(client: lightspeed_sdk::LightspeedClient) -> Result<(), Box<dyn std::error::Error>> {
537    /// client.start_keep_alive().await?;
538    /// // ... do work ...
539    /// let was_running = client.stop_keep_alive().await;
540    /// assert!(was_running);
541    /// # Ok(())
542    /// # }
543    /// ```
544    pub async fn stop_keep_alive(&self) -> bool {
545        let mut handle_guard = self.keep_alive_handle.lock().await;
546        
547        if let Some(handle) = handle_guard.take() {
548            handle.abort();
549            if self.config.debug {
550                log::debug!("Keep-alive task stopped");
551            }
552            true
553        } else {
554            false
555        }
556    }
557}