1use super::{
21 Balance, ChainAdapter, ChainId, GasPrice, GasPrices, RpcClient, SignedTx, TxHash, TxParams,
22 TxPriority, TxReceipt, TxStatus, TxSummary, UnsignedTx,
23};
24use crate::{Error, Result, Signature};
25use async_trait::async_trait;
26use serde::{Deserialize, Serialize};
27#[allow(deprecated)]
30use solana_sdk::{
31 compute_budget::ComputeBudgetInstruction,
32 hash::Hash,
33 instruction::{AccountMeta, Instruction},
34 message::Message,
35 pubkey::Pubkey,
36 signature::Signature as SolanaSignature,
37 system_instruction,
38 transaction::Transaction,
39};
40use std::str::FromStr;
41
42use bincode1 as bincode;
44
45pub const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111";
51
52pub const TOKEN_PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
54
55pub const ATA_PROGRAM_ID: &str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SolanaConfig {
65 pub chain_id: ChainId,
67 pub rpc_urls: Vec<String>,
69 pub explorer_url: Option<String>,
71 #[serde(default)]
73 pub commitment: SolanaCommitment,
74 pub use_versioned_transactions: bool,
76}
77
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(rename_all = "lowercase")]
81pub enum SolanaCommitment {
82 Processed,
83 #[default]
84 Confirmed,
85 Finalized,
86}
87
88impl SolanaCommitment {
89 pub fn as_str(&self) -> &'static str {
91 match self {
92 SolanaCommitment::Processed => "processed",
93 SolanaCommitment::Confirmed => "confirmed",
94 SolanaCommitment::Finalized => "finalized",
95 }
96 }
97}
98
99impl SolanaConfig {
100 pub fn mainnet() -> Self {
102 Self {
103 chain_id: ChainId::SOLANA_MAINNET,
104 rpc_urls: vec![
105 "https://api.mainnet-beta.solana.com".to_string(),
106 "https://solana-api.projectserum.com".to_string(),
107 ],
108 explorer_url: Some("https://explorer.solana.com".to_string()),
109 commitment: SolanaCommitment::Confirmed,
110 use_versioned_transactions: true,
111 }
112 }
113
114 pub fn devnet() -> Self {
116 Self {
117 chain_id: ChainId::SOLANA_DEVNET,
118 rpc_urls: vec!["https://api.devnet.solana.com".to_string()],
119 explorer_url: Some("https://explorer.solana.com?cluster=devnet".to_string()),
120 commitment: SolanaCommitment::Confirmed,
121 use_versioned_transactions: true,
122 }
123 }
124
125 pub fn testnet() -> Self {
127 Self {
128 chain_id: ChainId::SOLANA_TESTNET,
129 rpc_urls: vec!["https://api.testnet.solana.com".to_string()],
130 explorer_url: Some("https://explorer.solana.com?cluster=testnet".to_string()),
131 commitment: SolanaCommitment::Confirmed,
132 use_versioned_transactions: true,
133 }
134 }
135
136 pub fn custom(rpc_urls: Vec<String>) -> Self {
138 Self {
139 chain_id: ChainId::SOLANA_MAINNET,
140 rpc_urls,
141 explorer_url: None,
142 commitment: SolanaCommitment::Confirmed,
143 use_versioned_transactions: true,
144 }
145 }
146
147 pub fn with_explorer(mut self, url: impl Into<String>) -> Self {
149 self.explorer_url = Some(url.into());
150 self
151 }
152
153 pub fn with_commitment(mut self, commitment: SolanaCommitment) -> Self {
155 self.commitment = commitment;
156 self
157 }
158
159 pub fn with_versioned_transactions(mut self, enabled: bool) -> Self {
161 self.use_versioned_transactions = enabled;
162 self
163 }
164}
165
166#[derive(Debug, Clone)]
172pub struct SolanaAdapter {
173 config: SolanaConfig,
174 rpc: RpcClient,
175}
176
177impl SolanaAdapter {
178 pub fn new(config: SolanaConfig) -> Result<Self> {
180 let rpc = RpcClient::new(config.rpc_urls.clone())?;
181 Ok(Self { config, rpc })
182 }
183
184 pub fn config(&self) -> &SolanaConfig {
186 &self.config
187 }
188
189 pub fn rpc(&self) -> &RpcClient {
191 &self.rpc
192 }
193
194 pub async fn get_recent_blockhash(&self) -> Result<Hash> {
196 #[derive(Deserialize)]
197 struct BlockhashResponse {
198 blockhash: String,
199 }
200
201 #[derive(Deserialize)]
202 struct RpcResponse {
203 value: BlockhashResponse,
204 }
205
206 let response: RpcResponse = self
207 .rpc
208 .request(
209 "getLatestBlockhash",
210 serde_json::json!([{
211 "commitment": self.config.commitment.as_str()
212 }]),
213 )
214 .await?;
215
216 Hash::from_str(&response.value.blockhash)
217 .map_err(|e| Error::ChainError(format!("Invalid blockhash: {}", e)))
218 }
219
220 async fn get_priority_fees(&self) -> Result<PriorityFees> {
222 #[derive(Deserialize)]
223 struct FeeEntry {
224 #[serde(rename = "prioritizationFee")]
225 prioritization_fee: u64,
226 }
227
228 let response: Vec<FeeEntry> = self
229 .rpc
230 .request("getRecentPrioritizationFees", serde_json::json!([]))
231 .await
232 .unwrap_or_default();
233
234 if response.is_empty() {
235 return Ok(PriorityFees::default());
236 }
237
238 let mut fees: Vec<u64> = response.iter().map(|e| e.prioritization_fee).collect();
239 fees.sort();
240
241 let len = fees.len();
242 Ok(PriorityFees {
243 low: fees.get(len / 4).copied().unwrap_or(0),
244 medium: fees.get(len / 2).copied().unwrap_or(1000),
245 high: fees.get(len * 3 / 4).copied().unwrap_or(10000),
246 })
247 }
248
249 pub async fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
251 let result: u64 = self
252 .rpc
253 .request(
254 "getMinimumBalanceForRentExemption",
255 serde_json::json!([data_len]),
256 )
257 .await?;
258
259 Ok(result)
260 }
261
262 pub async fn get_token_accounts(&self, owner: &str) -> Result<Vec<TokenAccount>> {
264 let owner_pubkey = Pubkey::from_str(owner)
265 .map_err(|e| Error::InvalidConfig(format!("Invalid owner address: {}", e)))?;
266
267 #[derive(Deserialize)]
268 struct AccountData {
269 pubkey: String,
270 account: AccountInfo,
271 }
272
273 #[derive(Deserialize)]
274 struct AccountInfo {
275 data: ParsedData,
276 }
277
278 #[derive(Deserialize)]
279 struct ParsedData {
280 parsed: ParsedInfo,
281 }
282
283 #[derive(Deserialize)]
284 struct ParsedInfo {
285 info: TokenInfo,
286 }
287
288 #[derive(Deserialize)]
289 struct TokenInfo {
290 mint: String,
291 #[serde(rename = "tokenAmount")]
292 token_amount: TokenAmount,
293 }
294
295 #[derive(Deserialize)]
296 struct TokenAmount {
297 amount: String,
298 decimals: u8,
299 #[serde(rename = "uiAmountString")]
300 ui_amount_string: String,
301 }
302
303 #[derive(Deserialize)]
304 struct RpcResponse {
305 value: Vec<AccountData>,
306 }
307
308 let response: RpcResponse = self
309 .rpc
310 .request(
311 "getTokenAccountsByOwner",
312 serde_json::json!([
313 owner_pubkey.to_string(),
314 {"programId": TOKEN_PROGRAM_ID},
315 {"encoding": "jsonParsed"}
316 ]),
317 )
318 .await?;
319
320 Ok(response
321 .value
322 .into_iter()
323 .map(|a| TokenAccount {
324 address: a.pubkey,
325 mint: a.account.data.parsed.info.mint,
326 balance: a.account.data.parsed.info.token_amount.amount,
327 decimals: a.account.data.parsed.info.token_amount.decimals,
328 formatted_balance: a.account.data.parsed.info.token_amount.ui_amount_string,
329 })
330 .collect())
331 }
332
333 pub fn get_associated_token_address(&self, owner: &str, mint: &str) -> Result<String> {
335 let owner_pubkey = Pubkey::from_str(owner)
336 .map_err(|e| Error::InvalidConfig(format!("Invalid owner: {}", e)))?;
337 let mint_pubkey = Pubkey::from_str(mint)
338 .map_err(|e| Error::InvalidConfig(format!("Invalid mint: {}", e)))?;
339
340 let ata =
341 spl_associated_token_account::get_associated_token_address(&owner_pubkey, &mint_pubkey);
342
343 Ok(ata.to_string())
344 }
345
346 pub async fn build_create_ata_instruction(
348 &self,
349 payer: &str,
350 owner: &str,
351 mint: &str,
352 ) -> Result<Option<Instruction>> {
353 let ata = self.get_associated_token_address(owner, mint)?;
354
355 let account_info = self.get_account_info(&ata).await?;
357
358 if account_info.is_none() {
359 let payer_pubkey = Pubkey::from_str(payer)
360 .map_err(|e| Error::InvalidConfig(format!("Invalid payer: {}", e)))?;
361 let owner_pubkey = Pubkey::from_str(owner)
362 .map_err(|e| Error::InvalidConfig(format!("Invalid owner: {}", e)))?;
363 let mint_pubkey = Pubkey::from_str(mint)
364 .map_err(|e| Error::InvalidConfig(format!("Invalid mint: {}", e)))?;
365
366 let instruction =
367 spl_associated_token_account::instruction::create_associated_token_account(
368 &payer_pubkey,
369 &owner_pubkey,
370 &mint_pubkey,
371 &spl_token::id(),
372 );
373
374 Ok(Some(instruction))
375 } else {
376 Ok(None)
377 }
378 }
379
380 async fn get_account_info(&self, address: &str) -> Result<Option<AccountInfoResponse>> {
382 #[derive(Deserialize)]
383 struct RpcResponse {
384 value: Option<AccountInfoResponse>,
385 }
386
387 let response: RpcResponse = self
388 .rpc
389 .request(
390 "getAccountInfo",
391 serde_json::json!([
392 address,
393 {"encoding": "base64"}
394 ]),
395 )
396 .await?;
397
398 Ok(response.value)
399 }
400
401 fn lamports_to_sol(lamports: u64) -> String {
403 let sol = lamports as f64 / 1_000_000_000.0;
404 format!("{:.9}", sol)
405 .trim_end_matches('0')
406 .trim_end_matches('.')
407 .to_string()
408 }
409
410 fn sol_to_lamports(sol: &str) -> Result<u64> {
412 let value: f64 = sol
413 .parse()
414 .map_err(|_| Error::InvalidConfig(format!("Invalid SOL value: {}", sol)))?;
415
416 Ok((value * 1_000_000_000.0) as u64)
417 }
418
419 fn build_transfer_instructions(
421 &self,
422 from: &Pubkey,
423 to: &Pubkey,
424 lamports: u64,
425 priority_fee: u64,
426 compute_units: u32,
427 ) -> Vec<Instruction> {
428 let mut instructions = Vec::new();
429
430 if priority_fee > 0 {
432 instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
433 priority_fee,
434 ));
435 }
436
437 instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(
439 compute_units,
440 ));
441
442 instructions.push(system_instruction::transfer(from, to, lamports));
444
445 instructions
446 }
447}
448
449#[async_trait]
450impl ChainAdapter for SolanaAdapter {
451 fn chain_id(&self) -> ChainId {
452 self.config.chain_id
453 }
454
455 fn native_symbol(&self) -> &str {
456 "SOL"
457 }
458
459 fn native_decimals(&self) -> u8 {
460 9
461 }
462
463 async fn get_balance(&self, address: &str) -> Result<Balance> {
464 let pubkey = Pubkey::from_str(address)
465 .map_err(|e| Error::InvalidConfig(format!("Invalid address: {}", e)))?;
466
467 #[derive(Deserialize)]
468 struct BalanceResponse {
469 value: u64,
470 }
471
472 let result: BalanceResponse = self
473 .rpc
474 .request("getBalance", serde_json::json!([pubkey.to_string()]))
475 .await?;
476
477 Ok(Balance::new(result.value.to_string(), 9, "SOL"))
478 }
479
480 async fn get_nonce(&self, _address: &str) -> Result<u64> {
481 Ok(0)
484 }
485
486 async fn build_transaction(&self, params: TxParams) -> Result<UnsignedTx> {
487 let from_pubkey = Pubkey::from_str(¶ms.from)
488 .map_err(|e| Error::InvalidConfig(format!("Invalid from address: {}", e)))?;
489 let to_pubkey = Pubkey::from_str(¶ms.to)
490 .map_err(|e| Error::InvalidConfig(format!("Invalid to address: {}", e)))?;
491
492 let lamports = Self::sol_to_lamports(¶ms.value)?;
494
495 let priority_fees = self.get_priority_fees().await?;
497 let priority_fee = match params.priority {
498 TxPriority::Low => priority_fees.low,
499 TxPriority::Medium => priority_fees.medium,
500 TxPriority::High | TxPriority::Urgent => priority_fees.high,
501 };
502
503 let _recent_blockhash = self.get_recent_blockhash().await?;
505
506 let compute_units = params.gas_limit.unwrap_or(200_000) as u32;
508
509 let instructions = if let Some(data) = ¶ms.data {
511 let mut ixs = Vec::new();
513
514 if priority_fee > 0 {
515 ixs.push(ComputeBudgetInstruction::set_compute_unit_price(
516 priority_fee,
517 ));
518 }
519 ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(
520 compute_units,
521 ));
522
523 ixs.push(Instruction {
525 program_id: to_pubkey,
526 accounts: vec![AccountMeta::new(from_pubkey, true)],
527 data: data.clone(),
528 });
529
530 ixs
531 } else {
532 self.build_transfer_instructions(
534 &from_pubkey,
535 &to_pubkey,
536 lamports,
537 priority_fee,
538 compute_units,
539 )
540 };
541
542 let message = Message::new(&instructions, Some(&from_pubkey));
544 let tx = Transaction::new_unsigned(message);
545
546 let signing_payload = tx.message_data();
548
549 let raw_tx = bincode::serialize(&tx)
551 .map_err(|e| Error::ChainError(format!("Failed to serialize transaction: {}", e)))?;
552
553 let estimated_fee = 5000 + (priority_fee * compute_units as u64 / 1_000_000);
555 let fee_formatted = Self::lamports_to_sol(estimated_fee);
556
557 let summary = TxSummary {
558 tx_type: if params.data.is_some() {
559 "Program Call".to_string()
560 } else {
561 "Transfer".to_string()
562 },
563 from: params.from.clone(),
564 to: params.to.clone(),
565 value: format!("{} SOL", params.value),
566 estimated_fee: format!("{} SOL", fee_formatted),
567 details: Some(format!("Priority fee: {} micro-lamports/CU", priority_fee)),
568 };
569
570 Ok(UnsignedTx {
571 chain_id: self.config.chain_id,
572 signing_payload,
573 raw_tx,
574 summary,
575 })
576 }
577
578 async fn broadcast(&self, signed_tx: &SignedTx) -> Result<TxHash> {
579 let encoded = bs58::encode(&signed_tx.raw_tx).into_string();
580
581 let result: String = self
582 .rpc
583 .request(
584 "sendTransaction",
585 serde_json::json!([
586 encoded,
587 {
588 "encoding": "base58",
589 "skipPreflight": false,
590 "preflightCommitment": self.config.commitment.as_str()
591 }
592 ]),
593 )
594 .await?;
595
596 let explorer_url = self.explorer_tx_url(&result);
597
598 Ok(TxHash {
599 hash: result,
600 explorer_url,
601 })
602 }
603
604 fn derive_address(&self, public_key: &[u8]) -> Result<String> {
605 if public_key.len() == 32 {
616 let pubkey = Pubkey::new_from_array(
618 public_key
619 .try_into()
620 .map_err(|_| Error::Crypto("Invalid public key length".into()))?,
621 );
622 Ok(pubkey.to_string())
623 } else if public_key.len() == 33 {
624 use sha2::{Digest, Sha256};
626 let mut hasher = Sha256::new();
627 hasher.update(public_key);
628 let hash = hasher.finalize();
629 let pubkey = Pubkey::new_from_array(hash.into());
630 Ok(pubkey.to_string())
631 } else if public_key.len() == 64 || public_key.len() == 65 {
632 use sha2::{Digest, Sha256};
634 let key_bytes = if public_key.len() == 65 {
635 &public_key[1..] } else {
637 public_key
638 };
639 let mut hasher = Sha256::new();
640 hasher.update(key_bytes);
641 let hash = hasher.finalize();
642 let pubkey = Pubkey::new_from_array(hash.into());
643 Ok(pubkey.to_string())
644 } else {
645 Err(Error::Crypto(format!(
646 "Invalid public key length: {}",
647 public_key.len()
648 )))
649 }
650 }
651
652 async fn get_gas_prices(&self) -> Result<GasPrices> {
653 let priority_fees = self.get_priority_fees().await?;
654
655 Ok(GasPrices {
656 low: GasPrice {
657 max_fee: priority_fees.low as u128,
658 max_priority_fee: priority_fees.low as u128,
659 estimated_wait_secs: Some(30),
660 },
661 medium: GasPrice {
662 max_fee: priority_fees.medium as u128,
663 max_priority_fee: priority_fees.medium as u128,
664 estimated_wait_secs: Some(10),
665 },
666 high: GasPrice {
667 max_fee: priority_fees.high as u128,
668 max_priority_fee: priority_fees.high as u128,
669 estimated_wait_secs: Some(5),
670 },
671 base_fee: Some(5000), })
673 }
674
675 async fn estimate_gas(&self, params: &TxParams) -> Result<u64> {
676 if params.data.is_some() {
682 Ok(200_000) } else {
684 Ok(200) }
686 }
687
688 async fn wait_for_confirmation(&self, tx_hash: &str, timeout_secs: u64) -> Result<TxReceipt> {
689 let start = std::time::Instant::now();
690 let timeout = std::time::Duration::from_secs(timeout_secs);
691
692 loop {
693 if start.elapsed() > timeout {
694 return Err(Error::Timeout(format!(
695 "Transaction {} not confirmed within {} seconds",
696 tx_hash, timeout_secs
697 )));
698 }
699
700 #[derive(Deserialize)]
701 struct TxResponse {
702 value: Option<TxInfo>,
703 }
704
705 #[derive(Deserialize)]
706 struct TxInfo {
707 slot: u64,
708 meta: Option<TxMeta>,
709 }
710
711 #[derive(Deserialize)]
712 struct TxMeta {
713 err: Option<serde_json::Value>,
714 fee: u64,
715 #[serde(rename = "computeUnitsConsumed")]
716 compute_units_consumed: Option<u64>,
717 }
718
719 let response: TxResponse = self
720 .rpc
721 .request(
722 "getTransaction",
723 serde_json::json!([
724 tx_hash,
725 {
726 "encoding": "json",
727 "commitment": self.config.commitment.as_str()
728 }
729 ]),
730 )
731 .await?;
732
733 if let Some(info) = response.value {
734 let status = if info.meta.as_ref().and_then(|m| m.err.as_ref()).is_some() {
735 TxStatus::Failed
736 } else {
737 TxStatus::Success
738 };
739
740 return Ok(TxReceipt {
741 tx_hash: tx_hash.to_string(),
742 block_number: info.slot,
743 status,
744 gas_used: info.meta.as_ref().and_then(|m| m.compute_units_consumed),
745 effective_gas_price: info.meta.as_ref().map(|m| m.fee as u128),
746 });
747 }
748
749 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
750 }
751 }
752
753 fn is_valid_address(&self, address: &str) -> bool {
754 Pubkey::from_str(address).is_ok()
755 }
756
757 fn explorer_tx_url(&self, tx_hash: &str) -> Option<String> {
758 self.config.explorer_url.as_ref().map(|base| {
759 if base.contains("?cluster=") {
760 format!("{}&tx={}", base, tx_hash)
761 } else {
762 format!("{}/tx/{}", base, tx_hash)
763 }
764 })
765 }
766
767 fn explorer_address_url(&self, address: &str) -> Option<String> {
768 self.config.explorer_url.as_ref().map(|base| {
769 if base.contains("?cluster=") {
770 format!("{}&address={}", base, address)
771 } else {
772 format!("{}/address/{}", base, address)
773 }
774 })
775 }
776
777 fn finalize_transaction(
778 &self,
779 unsigned_tx: &UnsignedTx,
780 signature: &Signature,
781 ) -> Result<SignedTx> {
782 let mut tx: Transaction = bincode::deserialize(&unsigned_tx.raw_tx)
784 .map_err(|e| Error::ChainError(format!("Failed to deserialize transaction: {}", e)))?;
785
786 let mut sig_bytes = [0u8; 64];
790 sig_bytes[..32].copy_from_slice(&signature.r);
791 sig_bytes[32..].copy_from_slice(&signature.s);
792
793 let solana_sig = SolanaSignature::from(sig_bytes);
794
795 tx.signatures = vec![solana_sig];
797
798 let raw_tx = bincode::serialize(&tx).map_err(|e| {
800 Error::ChainError(format!("Failed to serialize signed transaction: {}", e))
801 })?;
802
803 Ok(SignedTx {
804 chain_id: self.config.chain_id,
805 raw_tx,
806 tx_hash: bs58::encode(&sig_bytes).into_string(),
807 })
808 }
809}
810
811#[derive(Debug, Clone, Default)]
817struct PriorityFees {
818 low: u64,
819 medium: u64,
820 high: u64,
821}
822
823#[derive(Debug, Clone, Serialize, Deserialize)]
825pub struct TokenAccount {
826 pub address: String,
828 pub mint: String,
830 pub balance: String,
832 pub decimals: u8,
834 pub formatted_balance: String,
836}
837
838#[derive(Debug, Deserialize)]
840#[allow(dead_code)]
841struct AccountInfoResponse {
842 lamports: u64,
843 owner: String,
844 data: Vec<String>,
845 executable: bool,
846 #[serde(rename = "rentEpoch")]
847 rent_epoch: u64,
848}
849
850pub struct TokenTransferBuilder {
852 from: String,
853 to: String,
854 mint: String,
855 amount: u64,
856 decimals: u8,
857 create_ata_if_needed: bool,
858}
859
860impl TokenTransferBuilder {
861 pub fn new(
863 from: impl Into<String>,
864 to: impl Into<String>,
865 mint: impl Into<String>,
866 amount: u64,
867 decimals: u8,
868 ) -> Self {
869 Self {
870 from: from.into(),
871 to: to.into(),
872 mint: mint.into(),
873 amount,
874 decimals,
875 create_ata_if_needed: true,
876 }
877 }
878
879 pub fn without_ata_creation(mut self) -> Self {
881 self.create_ata_if_needed = false;
882 self
883 }
884
885 pub async fn build_instructions(&self, adapter: &SolanaAdapter) -> Result<Vec<Instruction>> {
887 let from_pubkey = Pubkey::from_str(&self.from)
888 .map_err(|e| Error::InvalidConfig(format!("Invalid from: {}", e)))?;
889 let to_pubkey = Pubkey::from_str(&self.to)
890 .map_err(|e| Error::InvalidConfig(format!("Invalid to: {}", e)))?;
891 let mint_pubkey = Pubkey::from_str(&self.mint)
892 .map_err(|e| Error::InvalidConfig(format!("Invalid mint: {}", e)))?;
893
894 let from_ata =
895 spl_associated_token_account::get_associated_token_address(&from_pubkey, &mint_pubkey);
896 let to_ata =
897 spl_associated_token_account::get_associated_token_address(&to_pubkey, &mint_pubkey);
898
899 let mut instructions = Vec::new();
900
901 if self.create_ata_if_needed
903 && let Some(create_ata_ix) = adapter
904 .build_create_ata_instruction(&self.from, &self.to, &self.mint)
905 .await?
906 {
907 instructions.push(create_ata_ix);
908 }
909
910 let transfer_ix = spl_token::instruction::transfer_checked(
912 &spl_token::id(),
913 &from_ata,
914 &mint_pubkey,
915 &to_ata,
916 &from_pubkey,
917 &[],
918 self.amount,
919 self.decimals,
920 )
921 .map_err(|e| Error::ChainError(format!("Failed to create transfer instruction: {}", e)))?;
922
923 instructions.push(transfer_ix);
924
925 Ok(instructions)
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932
933 #[test]
934 fn test_config_creation() {
935 let mainnet = SolanaConfig::mainnet();
936 assert_eq!(mainnet.chain_id, ChainId::SOLANA_MAINNET);
937
938 let devnet = SolanaConfig::devnet();
939 assert_eq!(devnet.chain_id, ChainId::SOLANA_DEVNET);
940 }
941
942 #[test]
943 fn test_lamports_conversion() {
944 assert_eq!(SolanaAdapter::lamports_to_sol(1_000_000_000), "1");
945 assert_eq!(SolanaAdapter::lamports_to_sol(500_000_000), "0.5");
946 assert_eq!(SolanaAdapter::lamports_to_sol(1_500_000_000), "1.5");
947 assert_eq!(SolanaAdapter::lamports_to_sol(0), "0");
948
949 assert_eq!(SolanaAdapter::sol_to_lamports("1").unwrap(), 1_000_000_000);
950 assert_eq!(SolanaAdapter::sol_to_lamports("0.5").unwrap(), 500_000_000);
951 assert_eq!(
952 SolanaAdapter::sol_to_lamports("1.5").unwrap(),
953 1_500_000_000
954 );
955 }
956
957 #[test]
958 fn test_address_validation() {
959 let config = SolanaConfig::mainnet();
960 let adapter = SolanaAdapter::new(config).unwrap();
961
962 assert!(adapter.is_valid_address("11111111111111111111111111111111"));
964 assert!(adapter.is_valid_address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"));
965
966 assert!(!adapter.is_valid_address("0x742d35Cc6634C0532925a3b844Bc9e7595f4e123"));
968 assert!(!adapter.is_valid_address("invalid"));
969 }
970
971 #[test]
972 fn test_derive_address() {
973 let config = SolanaConfig::mainnet();
974 let adapter = SolanaAdapter::new(config).unwrap();
975
976 let key32 = [1u8; 32];
978 let addr = adapter.derive_address(&key32).unwrap();
979 assert!(!addr.is_empty());
980
981 let key33 = [2u8; 33];
983 let addr = adapter.derive_address(&key33).unwrap();
984 assert!(!addr.is_empty());
985 }
986
987 #[test]
988 fn test_commitment_as_str() {
989 assert_eq!(SolanaCommitment::Processed.as_str(), "processed");
990 assert_eq!(SolanaCommitment::Confirmed.as_str(), "confirmed");
991 assert_eq!(SolanaCommitment::Finalized.as_str(), "finalized");
992 }
993}