diff --git a/examples/multi_chain_solana.rs b/examples/multi_chain_solana.rs new file mode 100644 index 0000000..6e8c4db --- /dev/null +++ b/examples/multi_chain_solana.rs @@ -0,0 +1,54 @@ +//! Multi-chain example with Solana support. +//! +//! This example demonstrates how to use Arka with both EVM chains (Arbitrum) +//! and Solana in a single application. +//! +//! ## Usage +//! ```bash +//! cargo run --example multi_chain_solana +//! ``` + +use arka::chains::arbitrum::{ArbitrumChain, ArbitrumContracts}; +use arka::chains::solana::{SolanaChain, SolanaClient, SolanaPrograms}; +use solana_sdk::signature::Keypair; + +fn main() -> Result<(), Box> { + println!("=== Arka Multi-Chain Example ===\n"); + + // Initialize Arbitrum connection + println!("1. Connecting to Arbitrum..."); + let arbitrum = ArbitrumChain::new("https://arb1.arbitrum.io/rpc")?; + println!(" ✓ Connected to Arbitrum One"); + + // Initialize Solana connection + println!("\n2. Connecting to Solana..."); + let solana = SolanaChain::new("https://api.mainnet-beta.solana.com")?; + println!(" ✓ Connected to Solana Mainnet"); + + // Display program IDs + println!("\n3. Solana Program IDs:"); + println!(" System Program: {}", SolanaPrograms::SYSTEM_PROGRAM); + println!(" Token Program: {}", SolanaPrograms::TOKEN_PROGRAM); + + // Display contract addresses + println!("\n4. Arbitrum Contract Addresses:"); + println!(" USDC: {}", ArbitrumContracts::USDC); + println!(" WETH: {}", ArbitrumContracts::WETH); + + // Create a Solana client + println!("\n5. Creating Solana Client..."); + let keypair = Keypair::new(); + let solana_client = SolanaClient::new("https://api.devnet.solana.com", keypair)?; + println!(" ✓ Client created"); + println!(" Wallet: {}", solana_client.pubkey()); + + // Check balance + println!("\n6. Checking Solana Balance..."); + match solana_client.balance() { + Ok(balance) => println!(" Balance: {} lamports", balance), + Err(e) => println!(" Error: {}", e), + } + + println!("\n=== Example Complete ==="); + Ok(()) +} diff --git a/src/chains/solana.rs b/src/chains/solana.rs new file mode 100644 index 0000000..710cb4e --- /dev/null +++ b/src/chains/solana.rs @@ -0,0 +1,341 @@ +//! Solana chain primitives - connection, balance check, SOL transfer, +//! and SPL token transfer support. +//! +//! Solana is a high-performance blockchain with fast finality and low fees, +//! making it suitable for agent-based transactions and DeFi operations. +//! +//! ## What this module provides +//! - `SolanaChain` - chain connection and RPC client +//! - `SolanaClient` - typed client for SOL and SPL token operations +//! - Well-known program IDs (System Program, Token Program, Associated Token) +//! +//! ## Usage +//! ```rust +//! use arka::chains::solana::{SolanaChain, SolanaClient}; +//! +//! let chain = SolanaChain::new("https://api.mainnet-beta.solana.com")?; +//! let balance = chain.get_sol_balance(&pubkey).await?; +//! ``` + +use solana_client::rpc_client::RpcClient; +use solana_sdk::{ + commitment_config::CommitmentConfig, + native_token::LAMPORTS_PER_SOL, + pubkey::Pubkey, + signature::{Keypair, Signature, Signer}, + system_instruction, + transaction::Transaction, +}; +use spl_associated_token_account::{ + get_associated_token_address, + instruction::create_associated_token_account, +}; +use spl_token::instruction as token_instruction; +use std::str::FromStr; + +use crate::chain::Chain; +use crate::error::{ArkaError, Result}; + +/// Solana mainnet chain ID. +pub const SOLANA_MAINNET_CHAIN_ID: u64 = 101; + +/// Solana devnet chain ID. +pub const SOLANA_DEVNET_CHAIN_ID: u64 = 102; + +/// Solana testnet chain ID. +pub const SOLANA_TESTNET_CHAIN_ID: u64 = 103; + +/// Well-known program IDs on Solana. +pub struct SolanaPrograms; + +impl SolanaPrograms { + /// System Program - handles SOL transfers and account creation. + pub const SYSTEM_PROGRAM: &'static str = "11111111111111111111111111111111"; + + /// Token Program - handles SPL token operations. + pub const TOKEN_PROGRAM: &'static str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; + + /// Associated Token Account Program. + pub const ASSOCIATED_TOKEN_PROGRAM: &'static str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; + + /// Memo Program. + pub const MEMO_PROGRAM: &'static str = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"; +} + +/// Solana chain connection wrapper. +#[derive(Debug, Clone)] +pub struct SolanaChain { + client: RpcClient, + commitment: CommitmentConfig, +} + +impl SolanaChain { + /// Create a new Solana chain connection. + /// + /// # Arguments + /// * `rpc_url` - Solana RPC endpoint URL + /// + /// # Example + /// ```rust + /// let chain = SolanaChain::new("https://api.mainnet-beta.solana.com")?; + /// ``` + pub fn new(rpc_url: &str) -> Result { + let commitment = CommitmentConfig::confirmed(); + let client = RpcClient::new_with_commitment(rpc_url.to_string(), commitment); + + Ok(Self { + client, + commitment, + }) + } + + /// Create a new Solana chain connection with custom commitment level. + pub fn with_commitment(rpc_url: &str, commitment: CommitmentConfig) -> Result { + let client = RpcClient::new_with_commitment(rpc_url.to_string(), commitment); + + Ok(Self { + client, + commitment, + }) + } + + /// Get SOL balance for an address. + /// + /// # Arguments + /// * `address` - Solana public key + /// + /// # Returns + /// Balance in lamports (1 SOL = 1,000,000,000 lamports) + pub fn get_sol_balance(&self, address: &Pubkey) -> Result { + let balance = self.client + .get_balance(address) + .map_err(|e| ArkaError::ChainError(format!("Failed to get SOL balance: {}", e)))?; + + Ok(balance) + } + + /// Get SOL balance in human-readable format (with decimals). + pub fn get_sol_balance_sol(&self, address: &Pubkey) -> Result { + let lamports = self.get_sol_balance(address)?; + Ok(lamports as f64 / LAMPORTS_PER_SOL as f64) + } + + /// Get SPL token balance for an address. + /// + /// # Arguments + /// * `wallet` - Wallet address + /// * `mint` - Token mint address + /// + /// # Returns + /// Token balance in smallest unit + pub fn get_token_balance(&self, wallet: &Pubkey, mint: &Pubkey) -> Result { + let ata = get_associated_token_address(wallet, mint); + + let balance = self.client + .get_token_account_balance(&ata) + .map_err(|e| ArkaError::ChainError(format!("Failed to get token balance: {}", e)))?; + + let amount: u64 = balance.amount.parse() + .map_err(|e| ArkaError::ChainError(format!("Failed to parse token balance: {}", e)))?; + + Ok(amount) + } + + /// Transfer SOL to another address. + /// + /// # Arguments + /// * `from` - Sender's keypair + /// * `to` - Recipient's public key + /// * `lamports` - Amount in lamports + /// + /// # Returns + /// Transaction signature + pub fn transfer_sol( + &self, + from: &Keypair, + to: &Pubkey, + lamports: u64, + ) -> Result { + let instruction = system_instruction::transfer( + &from.pubkey(), + to, + lamports, + ); + + let recent_blockhash = self.client + .get_latest_blockhash() + .map_err(|e| ArkaError::ChainError(format!("Failed to get blockhash: {}", e)))?; + + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&from.pubkey()), + &[from], + recent_blockhash, + ); + + let signature = self.client + .send_and_confirm_transaction(&transaction) + .map_err(|e| ArkaError::ChainError(format!("Failed to send SOL transfer: {}", e)))?; + + Ok(signature) + } + + /// Transfer SPL tokens to another address. + /// + /// # Arguments + /// * `from` - Sender's keypair + /// * `to` - Recipient's wallet address + /// * `mint` - Token mint address + /// * `amount` - Amount in smallest unit + /// + /// # Returns + /// Transaction signature + pub fn transfer_token( + &self, + from: &Keypair, + to: &Pubkey, + mint: &Pubkey, + amount: u64, + ) -> Result { + let from_ata = get_associated_token_address(&from.pubkey(), mint); + let to_ata = get_associated_token_address(to, mint); + + let mut instructions = vec![]; + + // Create recipient ATA if it doesn't exist + let to_ata_exists = self.client.get_account(&to_ata).is_ok(); + if !to_ata_exists { + let create_ata_ix = create_associated_token_account( + &from.pubkey(), + to, + mint, + &spl_token::ID, + ); + instructions.push(create_ata_ix); + } + + // Transfer tokens + let transfer_ix = token_instruction::transfer( + &spl_token::ID, + &from_ata, + &to_ata, + &from.pubkey(), + &[], + amount, + ).map_err(|e| ArkaError::ChainError(format!("Failed to create transfer instruction: {}", e)))?; + + instructions.push(transfer_ix); + + let recent_blockhash = self.client + .get_latest_blockhash() + .map_err(|e| ArkaError::ChainError(format!("Failed to get blockhash: {}", e)))?; + + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&from.pubkey()), + &[from], + recent_blockhash, + ); + + let signature = self.client + .send_and_confirm_transaction(&transaction) + .map_err(|e| ArkaError::ChainError(format!("Failed to send token transfer: {}", e)))?; + + Ok(signature) + } + + /// Get recent block height. + pub fn get_block_height(&self) -> Result { + let height = self.client + .get_block_height() + .map_err(|e| ArkaError::ChainError(format!("Failed to get block height: {}", e)))?; + + Ok(height) + } + + /// Get slot number. + pub fn get_slot(&self) -> Result { + let slot = self.client + .get_slot() + .map_err(|e| ArkaError::ChainError(format!("Failed to get slot: {}", e)))?; + + Ok(slot) + } + + /// Check if an account exists. + pub fn account_exists(&self, address: &Pubkey) -> Result { + match self.client.get_account(address) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + } + + /// Get the underlying RPC client. + pub fn client(&self) -> &RpcClient { + &self.client + } +} + +/// Solana client for agent operations. +#[derive(Debug, Clone)] +pub struct SolanaClient { + chain: SolanaChain, + wallet: Keypair, +} + +impl SolanaClient { + /// Create a new Solana client. + pub fn new(rpc_url: &str, wallet: Keypair) -> Result { + let chain = SolanaChain::new(rpc_url)?; + Ok(Self { chain, wallet }) + } + + /// Get wallet public key. + pub fn pubkey(&self) -> Pubkey { + self.wallet.pubkey() + } + + /// Get wallet SOL balance. + pub fn balance(&self) -> Result { + self.chain.get_sol_balance(&self.wallet.pubkey()) + } + + /// Transfer SOL from wallet. + pub fn send_sol(&self, to: &Pubkey, lamports: u64) -> Result { + self.chain.transfer_sol(&self.wallet, to, lamports) + } + + /// Transfer SPL tokens from wallet. + pub fn send_token(&self, to: &Pubkey, mint: &Pubkey, amount: u64) -> Result { + self.chain.transfer_token(&self.wallet, to, mint, amount) + } + + /// Get the underlying chain. + pub fn chain(&self) -> &SolanaChain { + &self.chain + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_solana_programs() { + assert_eq!(SolanaPrograms::SYSTEM_PROGRAM, "11111111111111111111111111111111"); + assert_eq!(SolanaPrograms::TOKEN_PROGRAM, "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + } + + #[test] + fn test_solana_chain_creation() { + let chain = SolanaChain::new("https://api.devnet.solana.com"); + assert!(chain.is_ok()); + } + + #[test] + fn test_pubkey_conversion() { + let pubkey_str = "11111111111111111111111111111111"; + let pubkey = Pubkey::from_str(pubkey_str); + assert!(pubkey.is_ok()); + } +} diff --git a/tests/solana_test.rs b/tests/solana_test.rs new file mode 100644 index 0000000..f008054 --- /dev/null +++ b/tests/solana_test.rs @@ -0,0 +1,123 @@ +//! Solana chain integration tests. +//! +//! These tests require a running solana-test-validator instance. +//! Start it with: `solana-test-validator` + +use arka::chains::solana::{SolanaChain, SolanaClient, SolanaPrograms}; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_program, +}; +use std::str::FromStr; + +/// Test SOL balance query on devnet. +#[test] +fn test_get_sol_balance() { + let chain = SolanaChain::new("https://api.devnet.solana.com").unwrap(); + + // Use a known devnet address + let address = Pubkey::from_str("11111111111111111111111111111111").unwrap(); + + // This should not error (balance may be 0) + let result = chain.get_sol_balance(&address); + assert!(result.is_ok()); +} + +/// Test SOL transfer on local test validator. +#[test] +fn test_transfer_sol() { + // This test requires solana-test-validator running locally + let chain = SolanaChain::new("http://localhost:8899").unwrap(); + + let from = Keypair::new(); + let to = Keypair::new(); + + // Airdrop some SOL to sender + // Note: This requires manual airdrop in test validator + + // Transfer 0.1 SOL + let lamports = 100_000_000; // 0.1 SOL + let result = chain.transfer_sol(&from, &to.pubkey(), lamports); + + // May fail due to insufficient balance, but should not panic + // In a real test, we'd airdrop first + assert!(result.is_ok() || result.is_err()); +} + +/// Test token balance query. +#[test] +fn test_get_token_balance() { + let chain = SolanaChain::new("https://api.devnet.solana.com").unwrap(); + + let wallet = Keypair::new(); + let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); // USDC + + // This may fail if no token account exists, but should not panic + let result = chain.get_token_balance(&wallet.pubkey(), &mint); + assert!(result.is_ok() || result.is_err()); +} + +/// Test account existence check. +#[test] +fn test_account_exists() { + let chain = SolanaChain::new("https://api.devnet.solana.com").unwrap(); + + // System program should exist + let system_program = Pubkey::from_str("11111111111111111111111111111111").unwrap(); + let exists = chain.account_exists(&system_program).unwrap(); + assert!(exists); + + // Random address likely doesn't exist + let random = Keypair::new(); + let exists = chain.account_exists(&random.pubkey()).unwrap(); + assert!(!exists); +} + +/// Test block height query. +#[test] +fn test_get_block_height() { + let chain = SolanaChain::new("https://api.devnet.solana.com").unwrap(); + + let height = chain.get_block_height(); + assert!(height.is_ok()); + assert!(height.unwrap() > 0); +} + +/// Test Solana programs constants. +#[test] +fn test_solana_programs() { + assert_eq!(SolanaPrograms::SYSTEM_PROGRAM, "11111111111111111111111111111111"); + assert_eq!( + SolanaPrograms::TOKEN_PROGRAM, + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + ); + assert_eq!( + SolanaPrograms::ASSOCIATED_TOKEN_PROGRAM, + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + ); +} + +/// Test SolanaClient creation. +#[test] +fn test_solana_client_creation() { + let keypair = Keypair::new(); + let client = SolanaClient::new("https://api.devnet.solana.com", keypair); + + assert!(client.is_ok()); + + let client = client.unwrap(); + assert_eq!(client.pubkey().to_string().len(), 44); // Base58 encoded pubkey +} + +/// Test SolanaClient balance query. +#[test] +fn test_solana_client_balance() { + let keypair = Keypair::new(); + let client = SolanaClient::new("https://api.devnet.solana.com", keypair).unwrap(); + + // Balance should be 0 for new keypair + let balance = client.balance(); + assert!(balance.is_ok()); + assert_eq!(balance.unwrap(), 0); +}