use crate::cli::output::{boxed_header, emit, field, newline, subfield, OutputMode, Render};
use crate::squads::{self, Permissions};
use solana_client::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
#[derive(serde::Serialize)]
pub struct MemberView {
key: String,
permissions: Vec<String>,
}
#[derive(serde::Serialize)]
pub struct MultisigView {
create_key: String,
multisig_pda: String,
vault_pda: String,
vault_balance_sphr: f64,
threshold: u16,
transaction_index: u64,
stale_transaction_index: u64,
time_lock: u32,
#[serde(skip_serializing_if = "Option::is_none")]
config_authority: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
rent_collector: Option<String>,
members: Vec<MemberView>,
}
impl Render for MultisigView {
fn to_text(&self) -> String {
let mut out = boxed_header("Multisig Vault");
out.push_str(newline());
out.push_str(&field("Create Key", &self.create_key));
out.push_str(&field("Multisig PDA", &self.multisig_pda));
out.push_str(&field("Vault PDA", &self.vault_pda));
out.push_str(&field(
"Vault Balance",
format!("{:.9} SPHR", self.vault_balance_sphr),
));
out.push_str(&field(
"Threshold",
format!("{}/{}", self.threshold, self.members.len()),
));
out.push_str(&field("Next TX Index", self.transaction_index));
out.push_str(&field("Stale TX Index", self.stale_transaction_index));
out.push_str(&field("Time Lock", format!("{} seconds", self.time_lock)));
out.push_str(&field(
"Config Authority",
self.config_authority.as_deref().unwrap_or("Autonomous"),
));
out.push_str(&field(
"Rent Collector",
self.rent_collector.as_deref().unwrap_or("Disabled"),
));
out.push_str(newline());
out.push_str(&format!("Members ({}):\n", self.members.len()));
for (i, m) in self.members.iter().enumerate() {
let perms = if m.permissions.is_empty() {
"No permissions".to_string()
} else {
m.permissions.join(" | ")
};
out.push_str(&subfield(
&format!("[{}]", i + 1),
format!("{} ({})", m.key, perms),
));
}
out
}
}
pub fn fetch(rpc_url: &str, create_key: Pubkey) -> eyre::Result<MultisigView> {
let rpc = RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::confirmed());
let resolved = squads::resolve(&rpc, &create_key)?;
let m = &resolved.account;
let vault_balance = rpc.get_balance(&resolved.vault).unwrap_or(0);
let config_authority =
(m.config_authority != Pubkey::default()).then(|| m.config_authority.to_string());
let members = m
.members
.iter()
.map(|member| MemberView {
key: member.key.to_string(),
permissions: permission_list(&member.permissions),
})
.collect();
Ok(MultisigView {
create_key: m.create_key.to_string(),
multisig_pda: resolved.multisig.to_string(),
vault_pda: resolved.vault.to_string(),
vault_balance_sphr: vault_balance as f64 / 1_000_000_000.0,
threshold: m.threshold,
transaction_index: m.transaction_index,
stale_transaction_index: m.stale_transaction_index,
time_lock: m.time_lock,
config_authority,
rent_collector: m.rent_collector.map(|rc| rc.to_string()),
members,
})
}
pub fn show(rpc_url: &str, create_key: String, mode: OutputMode) -> eyre::Result<()> {
let create_key = create_key
.parse::<Pubkey>()
.map_err(|e| eyre::eyre!("Invalid create-key '{}': {}", create_key, e))?;
emit(&fetch(rpc_url, create_key)?, mode)
}
fn permission_list(perms: &Permissions) -> Vec<String> {
let mut out = Vec::new();
if perms.mask & (squads::types::Permission::Initiate as u8) != 0 {
out.push("Initiate".to_string());
}
if perms.mask & (squads::types::Permission::Vote as u8) != 0 {
out.push("Vote".to_string());
}
if perms.mask & (squads::types::Permission::Execute as u8) != 0 {
out.push("Execute".to_string());
}
out
}