Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ This CLI tool enables the creation of feature gate multisigs across all Solana n
The configuration file dictates the members and parent multisigs for the feature gate multisig to be created. The fee payer keypair is used to pay transaction fees and sets up the multisig configurations and proposals for a given feature gate.

**Proposals are always created in the following order:**
1. **Feature Activation Proposal** (Index 0)
2. **Feature Activation Revocation Proposal** (Index 1)
1. **Feature Activation Proposal** (Index 1)
2. **Feature Activation Revocation Proposal** (Index 2)

Once a feature gate multisig has been created, the CLI exposes transaction generation functionality to enable voting on either proposal and executing them when the threshold is met.

Expand Down
184 changes: 114 additions & 70 deletions src/commands/show.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use crate::constants::*;
use crate::squads::{get_vault_pda, get_transaction_pda, get_proposal_pda, Multisig, VaultTransaction, Proposal, ProposalStatus};
use crate::provision::{get_account_data_with_retry, create_rpc_client};
use crate::provision::{create_rpc_client, get_account_data_with_retry};
use crate::squads::{
get_proposal_pda, get_transaction_pda, get_vault_pda, Multisig, Proposal, ProposalStatus,
VaultTransaction,
};
use crate::utils::*;
use eyre::Result;
use colored::*;
use eyre::Result;
use inquire::Select;
use solana_client::rpc_client::RpcClient;
use solana_pubkey::Pubkey;
use std::str::FromStr;
Expand Down Expand Up @@ -31,8 +35,8 @@ pub async fn show_command(config: &Config, address: Option<String>) -> Result<()

async fn show_multisig(config: &Config, address: &str) -> Result<()> {
// Parse the multisig address
let multisig_pubkey = Pubkey::from_str(address)
.map_err(|_| eyre::eyre!("Invalid multisig address format"))?;
let multisig_pubkey =
Pubkey::from_str(address).map_err(|_| eyre::eyre!("Invalid multisig address format"))?;

println!(
"{}",
Expand All @@ -51,34 +55,27 @@ async fn show_multisig(config: &Config, address: &str) -> Result<()> {
vec![DEFAULT_DEVNET_URL.to_string()]
};

println!("Available networks to search:");
for (i, network) in networks_to_try.iter().enumerate() {
println!(" {}: {}", i + 1, network);
}
println!();
let rpc_url: String =
Select::new("Which network would you like to query?", networks_to_try).prompt()?;

for rpc_url in &networks_to_try {
println!("🌐 Trying network: {}", rpc_url.bright_white());
println!("🌐 Trying network: {}", rpc_url.bright_white());

let rpc_client = create_rpc_client(rpc_url);
match get_account_data_with_retry(&rpc_client, &multisig_pubkey) {
Ok(data) => {
println!("✅ Found account on: {}", rpc_url.bright_green());
account_data = Some(data);
successful_rpc_url = Some(rpc_url.clone());
break;
}
Err(e) => {
let error_str = e.to_string();
if error_str.contains("AccountNotFound")
|| error_str.contains("could not find account")
{
println!("❌ Account not found on: {}", rpc_url.bright_red());
last_error = Some(format!("Account not found: {}. This address may not exist on any of the configured networks or may not be a multisig account.", multisig_pubkey));
} else {
println!("❌ Error querying {}: {}", rpc_url.bright_red(), e);
last_error = Some(format!("Failed to query networks: {}", e));
}
let rpc_client = create_rpc_client(&rpc_url);
match get_account_data_with_retry(&rpc_client, &multisig_pubkey) {
Ok(data) => {
println!("✅ Found account on: {}", rpc_url.bright_green());
account_data = Some(data);
successful_rpc_url = Some(rpc_url.clone());
}
Err(e) => {
let error_str = e.to_string();
if error_str.contains("AccountNotFound") || error_str.contains("could not find account")
{
println!("❌ Account not found on: {}", rpc_url.bright_red());
last_error = Some(format!("Account not found: {}. This address may not exist on any of the configured networks or may not be a multisig account.", multisig_pubkey));
} else {
println!("❌ Error querying {}: {}", rpc_url.bright_red(), e);
last_error = Some(format!("Failed to query networks: {}", e));
}
}
}
Expand All @@ -104,9 +101,7 @@ async fn show_multisig(config: &Config, address: &str) -> Result<()> {
println!();

if account_data.len() < 8 {
return Err(eyre::eyre!(
"Account data too small to be a valid multisig"
));
return Err(eyre::eyre!("Account data too small to be a valid multisig"));
}

println!("📊 Account data length: {} bytes", account_data.len());
Expand Down Expand Up @@ -167,10 +162,15 @@ fn display_multisig_details(multisig: &Multisig, address: &Pubkey) -> Result<()>
MultisigInfo {
property: "Threshold".to_string(),
value: {
let voting_members_count = multisig.members.iter()
let voting_members_count = multisig
.members
.iter()
.filter(|member| member.permissions.mask & 2 != 0) // Check Vote permission (bit 1)
.count();
format!("{} of {} voting members", multisig.threshold, voting_members_count)
format!(
"{} of {} voting members",
multisig.threshold, voting_members_count
)
},
},
MultisigInfo {
Expand Down Expand Up @@ -308,45 +308,62 @@ async fn fetch_and_display_transactions_and_proposals(
return Ok(());
}

println!(
"🔍 Fetching transaction and proposal data for indices 1 and 2..."
);
println!("🔍 Fetching transaction and proposal data for indices 1 and 2...");
println!();

// Fetch data for indices 1 and 2
for tx_index in 1..=2u64 {
if tx_index > multisig.transaction_index {
println!("Transaction index {} not yet created (current max: {})", tx_index, multisig.transaction_index);
println!(
"Transaction index {} not yet created (current max: {})",
tx_index, multisig.transaction_index
);
continue;
}

println!(
"{}",
format!("📋 TRANSACTION INDEX {}", tx_index).bright_cyan().bold()
format!("📋 TRANSACTION INDEX {}", tx_index)
.bright_cyan()
.bold()
);
println!("{}", "─".repeat(50).bright_cyan());

// Generate PDAs for transaction and proposal
let (transaction_pda, _) = get_transaction_pda(multisig_pubkey, tx_index, None);
let (proposal_pda, _) = get_proposal_pda(multisig_pubkey, tx_index, None);

println!("🎯 Transaction PDA: {}", transaction_pda.to_string().bright_white());
println!("🎯 Proposal PDA: {}", proposal_pda.to_string().bright_white());
println!(
"🎯 Transaction PDA: {}",
transaction_pda.to_string().bright_white()
);
println!(
"🎯 Proposal PDA: {}",
proposal_pda.to_string().bright_white()
);
println!();

// Fetch transaction account
match fetch_and_display_transaction(rpc_client, &transaction_pda, tx_index).await {
Ok(_) => {},
Ok(_) => {}
Err(e) => {
println!("❌ Failed to fetch transaction {}: {}", tx_index, e.to_string().bright_red());
println!(
"❌ Failed to fetch transaction {}: {}",
tx_index,
e.to_string().bright_red()
);
}
}

// Fetch proposal account
// Fetch proposal account
match fetch_and_display_proposal(rpc_client, &proposal_pda, tx_index).await {
Ok(_) => {},
Ok(_) => {}
Err(e) => {
println!("❌ Failed to fetch proposal {}: {}", tx_index, e.to_string().bright_red());
println!(
"❌ Failed to fetch proposal {}: {}",
tx_index,
e.to_string().bright_red()
);
}
}

Expand Down Expand Up @@ -381,13 +398,14 @@ async fn fetch_and_display_transaction(
}

// Deserialize the VaultTransaction
let transaction: VaultTransaction = match borsh::BorshDeserialize::deserialize(&mut &account_data[8..]) {
Ok(tx) => tx,
Err(e) => {
println!(" ❌ Failed to deserialize transaction: {}", e);
return Ok(());
}
};
let transaction: VaultTransaction =
match borsh::BorshDeserialize::deserialize(&mut &account_data[8..]) {
Ok(tx) => tx,
Err(e) => {
println!(" ❌ Failed to deserialize transaction: {}", e);
return Ok(());
}
};

// Display transaction details in a table
#[derive(Tabled)]
Expand Down Expand Up @@ -436,7 +454,7 @@ async fn fetch_and_display_transaction(
// Display transaction message details
println!();
println!("📋 Transaction Message Details:");

#[derive(Tabled)]
struct MessageInfo {
#[tabled(rename = "Property")]
Expand Down Expand Up @@ -480,7 +498,7 @@ async fn fetch_and_display_transaction(
if !transaction.message.instructions.is_empty() {
println!();
println!("📋 Instructions Details:");

#[derive(Tabled)]
struct InstructionDetails {
#[tabled(rename = "Instruction #")]
Expand All @@ -493,12 +511,18 @@ async fn fetch_and_display_transaction(
data: String,
}

let instruction_details: Vec<InstructionDetails> = transaction.message.instructions.iter()
let instruction_details: Vec<InstructionDetails> = transaction
.message
.instructions
.iter()
.enumerate()
.map(|(i, instruction)| {
// Get the program ID from account_keys
let program_id = if (instruction.program_id_index as usize) < transaction.message.account_keys.len() {
transaction.message.account_keys[instruction.program_id_index as usize].to_string()
let program_id = if (instruction.program_id_index as usize)
< transaction.message.account_keys.len()
{
transaction.message.account_keys[instruction.program_id_index as usize]
.to_string()
} else {
format!("Invalid index ({})", instruction.program_id_index)
};
Expand All @@ -507,11 +531,17 @@ async fn fetch_and_display_transaction(
let accounts_info = if instruction.account_indexes.is_empty() {
"None".to_string()
} else {
instruction.account_indexes.iter()
instruction
.account_indexes
.iter()
.map(|&account_idx| {
if (account_idx as usize) < transaction.message.account_keys.len() {
format!("{}:{}", account_idx,
&transaction.message.account_keys[account_idx as usize].to_string()[..8])
format!(
"{}:{}",
account_idx,
&transaction.message.account_keys[account_idx as usize]
.to_string()[..8]
)
} else {
format!("{}:Invalid", account_idx)
}
Expand All @@ -525,13 +555,17 @@ async fn fetch_and_display_transaction(
"Empty".to_string()
} else if instruction.data.len() <= 32 {
// Show full data for small instructions
instruction.data.iter()
instruction
.data
.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(" ")
} else {
// Show first 16 bytes + length for large instructions
let preview = instruction.data.iter()
let preview = instruction
.data
.iter()
.take(16)
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
Expand All @@ -556,7 +590,7 @@ async fn fetch_and_display_transaction(
if !transaction.message.account_keys.is_empty() {
println!();
println!("🔑 Account Keys Reference:");

#[derive(Tabled)]
struct AccountKeyInfo {
#[tabled(rename = "Index")]
Expand All @@ -567,7 +601,10 @@ async fn fetch_and_display_transaction(
role: String,
}

let account_key_info: Vec<AccountKeyInfo> = transaction.message.account_keys.iter()
let account_key_info: Vec<AccountKeyInfo> = transaction
.message
.account_keys
.iter()
.enumerate()
.map(|(i, pubkey)| {
let role = if i < transaction.message.num_signers as usize {
Expand All @@ -576,7 +613,11 @@ async fn fetch_and_display_transaction(
} else {
"Read-only Signer"
}
} else if i < (transaction.message.num_signers + transaction.message.num_writable_non_signers) as usize {
} else if i
< (transaction.message.num_signers
+ transaction.message.num_writable_non_signers)
as usize
{
"Writable Non-signer"
} else {
"Read-only Non-signer"
Expand Down Expand Up @@ -691,7 +732,10 @@ async fn fetch_and_display_proposal(
println!("{}", proposal_table);

// Display voting details if there are votes
if !proposal.approved.is_empty() || !proposal.rejected.is_empty() || !proposal.cancelled.is_empty() {
if !proposal.approved.is_empty()
|| !proposal.rejected.is_empty()
|| !proposal.cancelled.is_empty()
{
println!();
println!("🗳️ Voting Details:");

Expand Down Expand Up @@ -735,4 +779,4 @@ async fn fetch_and_display_proposal(
println!();

Ok(())
}
}
Loading