use crate::cli::authority::Authority;
use crate::cli::output::{emit, progress, subfield, OutputMode, Render};
use crate::squads;
use solana_client::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{
native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, signature::Keypair, signer::Signer,
};
use solana_system_interface::instruction as system_instruction;
use std::str::FromStr;
pub fn dedupe_signers<'a>(signers: &[&'a Keypair]) -> Vec<&'a Keypair> {
let mut seen: Vec<Pubkey> = Vec::with_capacity(signers.len());
let mut out: Vec<&Keypair> = Vec::with_capacity(signers.len());
for kp in signers {
let pk = kp.pubkey();
if !seen.contains(&pk) {
seen.push(pk);
out.push(kp);
}
}
out
}
#[derive(serde::Serialize)]
pub struct AirdropResult {
pubkey: String,
amount_sphr: f64,
signature: String,
new_balance_sphr: f64,
}
impl Render for AirdropResult {
fn to_text(&self) -> String {
let mut out = String::from("✅ Airdrop successful!\n");
out.push_str(&subfield("Account", &self.pubkey));
out.push_str(&subfield("Amount", format!("{} SPHR", self.amount_sphr)));
out.push_str(&subfield(
"New balance",
format!("{} SPHR", self.new_balance_sphr),
));
out.push_str(&subfield("Signature", &self.signature));
out
}
}
pub fn request_airdrop(
rpc_url: &str,
pubkey_str: String,
amount: f64,
) -> eyre::Result<AirdropResult> {
let rpc_client =
RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::confirmed());
let pubkey = Pubkey::from_str(&pubkey_str)
.map_err(|e| eyre::eyre!("Failed to parse pubkey {}: {}", pubkey_str, e))?;
let lamports = (amount * LAMPORTS_PER_SOL as f64) as u64;
let before = rpc_client.get_balance(&pubkey)?;
progress(format!(
"Requesting airdrop of {} SPHR to {}...",
amount, pubkey
));
let signature = rpc_client.request_airdrop(&pubkey, lamports)?;
progress("Confirming airdrop...");
let confirmed = rpc_client.confirm_transaction(&signature)?;
if !confirmed {
return Err(eyre::eyre!(
"Airdrop {} did not confirm — the faucet may be empty or rate-limited.",
signature
));
}
let after = rpc_client.get_balance(&pubkey)?;
if after <= before {
return Err(eyre::eyre!(
"Airdrop confirmed (signature {}) but the balance did not increase (still {} SPHR) \
— the faucet could not fund the request.",
signature,
after as f64 / LAMPORTS_PER_SOL as f64
));
}
Ok(AirdropResult {
pubkey: pubkey.to_string(),
amount_sphr: amount,
signature: signature.to_string(),
new_balance_sphr: after as f64 / LAMPORTS_PER_SOL as f64,
})
}
pub fn airdrop(
rpc_url: &str,
pubkey_str: String,
amount: f64,
mode: OutputMode,
) -> eyre::Result<()> {
emit(&request_airdrop(rpc_url, pubkey_str, amount)?, mode)
}
pub fn transfer(
rpc_url: &str,
from: Authority,
to: Option<String>,
to_multisig: Option<String>,
amount: f64,
mode: OutputMode,
) -> eyre::Result<()> {
let rpc_client =
RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::confirmed());
let destination =
crate::cli::authority::resolve_target(&rpc_client, to, to_multisig, "--to", "--to-multisig")?;
let lamports = (amount * LAMPORTS_PER_SOL as f64) as u64;
progress(format!("Transferring {} SPHR to {}", amount, destination));
match &from {
crate::cli::authority::Authority::SingleSig { keypair } => {
progress(format!("From: {}", keypair.pubkey()));
}
crate::cli::authority::Authority::MultiSig { multisig, member } => {
progress(format!("Proposer: {}", member.pubkey()));
progress(format!("Multisig: {}", multisig));
let program_id =
squads::types::SQUADS_PROGRAM_ID.parse::<solana_sdk::pubkey::Pubkey>()?;
let (vault_pda, _) = squads::types::get_vault_pda(multisig, 0, &program_id);
let vault_balance = rpc_client.get_balance(&vault_pda).unwrap_or(0);
progress(format!(
"Vault: {} ({:.9} SPHR)",
vault_pda,
vault_balance as f64 / LAMPORTS_PER_SOL as f64
));
}
}
let from_pubkey = from.instruction_authority_pubkey()?;
let instruction = system_instruction::transfer(&from_pubkey, &destination, lamports);
let description = format!("Transfer {} SPHR to {}", amount, destination);
let result = from.execute_instruction(&rpc_client, instruction, &description)?;
emit(&result, mode)
}