solan 0.1.2

使用自定义 RPC_URL 与私钥,签名并转出 SOL 到目标地址。
Documentation
use std::str::FromStr;

use bs58;
use hex;
use solana_client::rpc_client::RpcClient;
use solana_sdk::native_token::LAMPORTS_PER_SOL;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, Signer};
use solana_sdk::signature::SeedDerivable;
use solana_sdk::system_instruction;
use solana_sdk::transaction::Transaction;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum SolanError {
    #[error("invalid secret key format")] 
    InvalidSecret,
    #[error("invalid destination address")] 
    InvalidAddress,
    #[error("rpc error: {0}")] 
    Rpc(String),
    #[error("transaction error: {0}")] 
    Tx(String),
}

fn parse_hex_bytes(s: &str) -> Option<Vec<u8>> {
    let t = s.trim();
    let hex_str = if let Some(stripped) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
        stripped
    } else {
        return None;
    };
    hex::decode(hex_str).ok()
}

/// 从字符串解析 Keypair。支持:
/// - 0x 前缀的十六进制字符串:解码后为 64 字节 Keypair 或 32 字节种子
/// - JSON 数组格式:64 个 u8 的密钥对或 32 个 u8 的种子(如 Solana CLI 导出的文件内容)
/// - Base58 字符串格式:解码后为 64 字节 Keypair 或 32 字节种子
fn keypair_from_secret(secret: &str) -> Result<Keypair, SolanError> {
    // 优先处理 0x 十六进制
    if let Some(bytes) = parse_hex_bytes(secret) {
        match bytes.len() {
            64 => {
                return Keypair::from_bytes(&bytes).map_err(|_| SolanError::InvalidSecret);
            }
            32 => {
                let seed: [u8; 32] = bytes.try_into().map_err(|_| SolanError::InvalidSecret)?;
                return Keypair::from_seed(&seed).map_err(|_| SolanError::InvalidSecret);
            }
            _ => {
                return Err(SolanError::InvalidSecret);
            }
        }
    }

    // JSON 数组
    if secret.trim_start().starts_with('[') {
        let vec: Vec<u8> = serde_json::from_str(secret).map_err(|_| SolanError::InvalidSecret)?;
        match vec.len() {
            64 => {
                return Keypair::from_bytes(&vec).map_err(|_| SolanError::InvalidSecret);
            }
            32 => {
                let seed: [u8; 32] = vec.try_into().map_err(|_| SolanError::InvalidSecret)?;
                return Keypair::from_seed(&seed).map_err(|_| SolanError::InvalidSecret);
            }
            _ => {
                return Err(SolanError::InvalidSecret);
            }
        }
    }
    
    // Base58
    let decoded = bs58::decode(secret).into_vec().map_err(|_| SolanError::InvalidSecret)?;
    match decoded.len() {
        64 => Keypair::from_bytes(&decoded).map_err(|_| SolanError::InvalidSecret),
        32 => {
            let seed: [u8; 32] = decoded.try_into().map_err(|_| SolanError::InvalidSecret)?;
            Keypair::from_seed(&seed).map_err(|_| SolanError::InvalidSecret)
        }
        _ => Err(SolanError::InvalidSecret),
    }
}

fn pubkey_from_str_any(addr: &str) -> Result<Pubkey, SolanError> {
    // 0x 十六进制地址(32字节)
    if let Some(bytes) = parse_hex_bytes(addr) {
        if bytes.len() == 32 {
            let arr: [u8; 32] = bytes.try_into().map_err(|_| SolanError::InvalidAddress)?;
            return Ok(Pubkey::new_from_array(arr));
        } else {
            return Err(SolanError::InvalidAddress);
        }
    }

    // 尝试 base58
    if let Ok(pk) = Pubkey::from_str(addr) {
        return Ok(pk);
    }

    // 尝试将 base58 解码为 32 字节后构造
    let decoded = bs58::decode(addr).into_vec().map_err(|_| SolanError::InvalidAddress)?;
    if decoded.len() == 32 {
        let arr: [u8; 32] = decoded.try_into().map_err(|_| SolanError::InvalidAddress)?;
        Ok(Pubkey::new_from_array(arr))
    } else {
        Err(SolanError::InvalidAddress)
    }
}

/// 将指定数量的 SOL 从私钥对应账户转出到目标地址。
///
/// 参数:
/// - `rpc_url`:Solana RPC 节点地址(例如 devnet: https://api.devnet.solana.com)。
/// - `secret`:用户私钥字符串(支持 0x 十六进制、JSON 数组或 Base58 形式,解码后为 64 字节 Keypair 或 32 字节种子)。
/// - `to_address`:目标地址(支持 0x 十六进制 32 字节或 Base58)。
/// - `amount_sol`:转账数量,单位 SOL(将自动换算到 lamports)。
///
/// 返回:交易签名(Base58)或错误。
pub fn transfer_sol(
    rpc_url: &str,
    secret: &str,
    to_address: &str,
    amount_sol: f64,
) -> Result<String, SolanError> {
    let client = RpcClient::new(rpc_url.to_string());

    // 解析密钥
    let keypair = keypair_from_secret(secret)?;
    let from_pubkey = keypair.pubkey();

    // 解析目标地址(支持 0x 十六进制或 Base58)
    let to_pubkey = pubkey_from_str_any(to_address)?;

    // 获取区块哈希
    let blockhash = client
        .get_latest_blockhash()
        .map_err(|e| SolanError::Rpc(e.to_string()))?;

    // 计算 lamports
    let lamports = (amount_sol * (LAMPORTS_PER_SOL as f64)).round() as u64;

    // 构造指令与交易
    let ix = system_instruction::transfer(&from_pubkey, &to_pubkey, lamports);
    let tx = Transaction::new_signed_with_payer(&[ix], Some(&from_pubkey), &[&keypair], blockhash);

    // 发送并确认交易
    let sig = client
        .send_and_confirm_transaction(&tx)
        .map_err(|e| SolanError::Tx(e.to_string()))?;

    Ok(sig.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_secret_base58_invalid() {
        assert!(matches!(keypair_from_secret("not_base58"), Err(SolanError::InvalidSecret)));
    }

    #[test]
    fn parse_hex_invalid_length() {
        assert!(matches!(keypair_from_secret("0xdeadbeef"), Err(SolanError::InvalidSecret)));
    }
}