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}