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()
}
fn keypair_from_secret(secret: &str) -> Result<Keypair, SolanError> {
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);
}
}
}
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);
}
}
}
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> {
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);
}
}
if let Ok(pk) = Pubkey::from_str(addr) {
return Ok(pk);
}
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)
}
}
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();
let to_pubkey = pubkey_from_str_any(to_address)?;
let blockhash = client
.get_latest_blockhash()
.map_err(|e| SolanError::Rpc(e.to_string()))?;
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)));
}
}