diff --git a/.gitignore b/.gitignore index d1d46bb8..00c2cb7b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ lib .env.* .env -cli/*.sh \ No newline at end of file +cli/*.sh + +.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 813fd1db..27031caf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5119,7 +5119,7 @@ dependencies = [ [[package]] name = "squads-multisig-cli" -version = "0.1.7" +version = "0.1.8" dependencies = [ "bincode", "clap 3.2.25", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a864fc6e..63320486 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "squads-multisig-cli" -version = "0.1.7" +version = "0.1.8" edition = "2021" authors = ["Valentin Madrid", "Vova Guguiev"] license = "MIT OR Apache-2.0" @@ -26,4 +26,4 @@ solana-program = "2.2.20" clap_v3 = { package = "clap", version = "3.0", optional = false } # The version needed for solana_clap_utils solana-address-lookup-table-interface = "2.2" spl-token = "8" -spl-associated-token-account = "7" +spl-associated-token-account = "7" \ No newline at end of file diff --git a/cli/README.md b/cli/README.md index 4a5deea1..6e532067 100644 --- a/cli/README.md +++ b/cli/README.md @@ -14,6 +14,8 @@ Overview - [Reclaim Vault Transaction rent](#vault-transaction-accounts-close) - [Create new Vault Transaction](#vault-transaction-create) - [Execute Vault Transaction](#vault-transaction-execute) + - [Display Vault Transaction](#display-transaction) + - [Display Config Transaction](#display-config-transaction) # 1. Installation @@ -342,3 +344,49 @@ vault-transaction-execute --keypair /path/to/keypair.json --multisig-pubkey [--rpc-url ] +``` + +### Parameters + +- `--transaction-address `: The public key of the VaultTransaction account to inspect. +- `--rpc-url `: (Optional) The URL of the Solana RPC endpoint. Defaults to `https://api.mainnet-beta.solana.com`. + +### Example Usage + +```bash +squads-multisig-cli display-transaction --transaction-address 279SBhVyHLEBsphBkDBNMUbSoA31yRBDtnU5mzSM3d5n +``` + +## Display Config Transaction + +### Description + +Fetches a config transaction account and displays its decoded actions, such as adding or removing members, changing the threshold, setting the time lock, managing spending limits, and updating the rent collector. + +### Syntax + +```bash +display-config-transaction --transaction-address [--rpc-url ] +``` + +### Parameters + +- `--transaction-address `: The public key of the ConfigTransaction account to inspect. +- `--rpc-url `: (Optional) The URL of the Solana RPC endpoint. Defaults to `https://api.mainnet-beta.solana.com`. + +### Example Usage + +```bash +squads-multisig-cli display-config-transaction --transaction-address AQb6VyZGzC2kL7vFU7WqoTJYTNZdHKhKuHmzuxkqGGjV +``` diff --git a/cli/src/command/config_transaction_create.rs b/cli/src/command/config_transaction_create.rs index c78581df..42396dc9 100644 --- a/cli/src/command/config_transaction_create.rs +++ b/cli/src/command/config_transaction_create.rs @@ -32,6 +32,7 @@ use squads_multisig::state::{ConfigAction, Period, Permission, Permissions}; use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Create a new config transaction (add/remove member, change threshold, etc.) and activate its proposal. #[derive(Args)] pub struct ConfigTransactionCreate { /// RPC URL diff --git a/cli/src/command/config_transaction_execute.rs b/cli/src/command/config_transaction_execute.rs index c6f9c6c8..1ef5c633 100644 --- a/cli/src/command/config_transaction_execute.rs +++ b/cli/src/command/config_transaction_execute.rs @@ -20,6 +20,7 @@ use squads_multisig::squads_multisig_program::instruction::ConfigTransactionExec use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Execute an approved config transaction on-chain. #[derive(Args)] pub struct ConfigTransactionExecute { /// RPC URL diff --git a/cli/src/command/display_config_transaction.rs b/cli/src/command/display_config_transaction.rs new file mode 100644 index 00000000..deff345c --- /dev/null +++ b/cli/src/command/display_config_transaction.rs @@ -0,0 +1,180 @@ +use clap::Args; +use colored::Colorize; +use solana_sdk::pubkey::Pubkey; +use squads_multisig::anchor_lang::AccountDeserialize; +use squads_multisig::solana_rpc_client::nonblocking::rpc_client::RpcClient; +use squads_multisig::squads_multisig_program::state::ConfigTransaction; +use squads_multisig::state::{ConfigAction, Period, Permission, Permissions}; +use std::str::FromStr; + +/// Fetch a config transaction account and display its decoded actions (add/remove member, change threshold, etc.). +#[derive(Args)] +pub struct DisplayConfigTransaction { + /// RPC URL (default: https://api.mainnet-beta.solana.com) + #[arg(long)] + rpc_url: Option, + + /// The ConfigTransaction account address to inspect + #[arg(long)] + transaction_address: String, +} + +fn format_permissions(permissions: Permissions) -> String { + let mut parts = Vec::new(); + if permissions.has(Permission::Initiate) { + parts.push("Proposer"); + } + if permissions.has(Permission::Vote) { + parts.push("Voter"); + } + if permissions.has(Permission::Execute) { + parts.push("Executor"); + } + if parts.is_empty() { + "None".to_string() + } else { + parts.join(", ") + } +} + +fn format_period(period: Period) -> &'static str { + match period { + Period::OneTime => "One-time", + Period::Day => "Daily", + Period::Week => "Weekly", + Period::Month => "Monthly", + } +} + +impl DisplayConfigTransaction { + pub async fn execute(self) -> eyre::Result<()> { + let rpc_url = self + .rpc_url + .unwrap_or_else(|| "https://api.mainnet-beta.solana.com".to_string()); + let transaction_address = Pubkey::from_str(&self.transaction_address)?; + + let rpc_client = RpcClient::new(rpc_url); + + let account_data = match rpc_client.get_account(&transaction_address).await { + Ok(account) => account.data, + Err(_) => { + println!("Account closed or not found."); + return Ok(()); + } + }; + + let config_tx = + ConfigTransaction::try_deserialize(&mut account_data.as_slice())?; + + println!(); + println!("{}", "Config Transaction Details".bold()); + println!(" Address: {}", transaction_address); + println!(" Multisig: {}", config_tx.multisig); + println!(" Creator: {}", config_tx.creator); + println!(" Index: {}", config_tx.index); + println!(); + + println!( + "{}", + format!("Actions ({})", config_tx.actions.len()).bold() + ); + println!(); + + for (i, action) in config_tx.actions.iter().enumerate() { + match action { + ConfigAction::AddMember { new_member } => { + println!("{}", format!("Action {}: Add Member", i + 1).yellow().bold()); + println!(" Key: {}", new_member.key); + println!( + " Permissions: {}", + format_permissions(new_member.permissions) + ); + } + ConfigAction::RemoveMember { old_member } => { + println!( + "{}", + format!("Action {}: Remove Member", i + 1).yellow().bold() + ); + println!(" Key: {}", old_member); + } + ConfigAction::ChangeThreshold { new_threshold } => { + println!( + "{}", + format!("Action {}: Change Threshold", i + 1).yellow().bold() + ); + println!(" New Threshold: {}", new_threshold); + } + ConfigAction::SetTimeLock { new_time_lock } => { + println!( + "{}", + format!("Action {}: Set Time Lock", i + 1).yellow().bold() + ); + println!(" New Time Lock: {} seconds", new_time_lock); + } + ConfigAction::AddSpendingLimit { + create_key, + vault_index, + mint, + amount, + period, + members, + destinations, + } => { + println!( + "{}", + format!("Action {}: Add Spending Limit", i + 1).yellow().bold() + ); + println!(" Create Key: {}", create_key); + println!(" Vault Index: {}", vault_index); + println!(" Mint: {}", mint); + println!(" Amount: {}", amount); + println!(" Period: {}", format_period(*period)); + if members.is_empty() { + println!(" Members: (all)"); + } else { + println!(" Members:"); + for m in members { + println!(" {}", m); + } + } + if destinations.is_empty() { + println!(" Destinations: (any)"); + } else { + println!(" Destinations:"); + for d in destinations { + println!(" {}", d); + } + } + } + ConfigAction::RemoveSpendingLimit { spending_limit } => { + println!( + "{}", + format!("Action {}: Remove Spending Limit", i + 1).yellow().bold() + ); + println!(" Spending Limit: {}", spending_limit); + } + ConfigAction::SetRentCollector { new_rent_collector } => { + println!( + "{}", + format!("Action {}: Set Rent Collector", i + 1).yellow().bold() + ); + match new_rent_collector { + Some(key) => println!(" New Rent Collector: {}", key), + None => println!(" New Rent Collector: (disabled)"), + } + } + _ => { + println!( + "{}", + format!("Action {}: (unknown action type)", i + 1) + .yellow() + .bold() + ); + } + } + println!(); + } + + Ok(()) + } +} diff --git a/cli/src/command/display_proposals.rs b/cli/src/command/display_proposals.rs index 8d500640..50308f3b 100644 --- a/cli/src/command/display_proposals.rs +++ b/cli/src/command/display_proposals.rs @@ -10,6 +10,7 @@ use squads_multisig::pda::get_proposal_pda; use squads_multisig::solana_rpc_client::nonblocking::rpc_client::RpcClient; use squads_multisig::state::{Multisig, Proposal, ProposalStatus}; +/// Fetch and display all proposals for a multisig, showing their status and transaction index. #[derive(Args)] pub struct DisplayProposals { /// RPC URL diff --git a/cli/src/command/display_transaction.rs b/cli/src/command/display_transaction.rs new file mode 100644 index 00000000..a47bfdb5 --- /dev/null +++ b/cli/src/command/display_transaction.rs @@ -0,0 +1,164 @@ +use clap::Args; +use colored::Colorize; +use solana_address_lookup_table_interface::state::AddressLookupTable; +use solana_sdk::pubkey::Pubkey; +use squads_multisig::anchor_lang::AccountDeserialize; +use squads_multisig::solana_rpc_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::bs58; +use squads_multisig::squads_multisig_program::state::VaultTransaction; +use squads_multisig::state::VaultTransactionMessage; +use std::str::FromStr; + +/// Fetch a vault transaction account and display its decoded instructions, accounts, and address lookup tables. +#[derive(Args)] +pub struct DisplayTransaction { + /// RPC URL (default: https://api.mainnet-beta.solana.com) + #[arg(long)] + rpc_url: Option, + + /// The VaultTransaction account address to inspect + #[arg(long)] + transaction_address: String, +} + +struct AccountEntry { + pubkey: Pubkey, + is_writable: bool, + is_signer: bool, +} + +impl DisplayTransaction { + pub async fn execute(self) -> eyre::Result<()> { + let rpc_url = self + .rpc_url + .unwrap_or_else(|| "https://api.mainnet-beta.solana.com".to_string()); + let transaction_address = Pubkey::from_str(&self.transaction_address)?; + + let rpc_client = RpcClient::new(rpc_url); + + let account_data = match rpc_client.get_account(&transaction_address).await { + Ok(account) => account.data, + Err(_) => { + println!("Account closed or not found."); + return Ok(()); + } + }; + + let vault_tx = + VaultTransaction::try_deserialize(&mut account_data.as_slice())?; + + println!(); + println!("{}", "Transaction Details".bold()); + println!(" Address: {}", transaction_address); + println!(" Multisig: {}", vault_tx.multisig); + println!(" Creator: {}", vault_tx.creator); + println!(" Index: {}", vault_tx.index); + println!(" Vault Index: {}", vault_tx.vault_index); + println!(); + + let message = &vault_tx.message; + + // Build the full resolved account list: static keys first, then ALT-derived keys. + let mut all_accounts: Vec = message + .account_keys + .iter() + .enumerate() + .map(|(i, key)| AccountEntry { + pubkey: *key, + is_writable: message.is_static_writable_index(i), + is_signer: message.is_signer_index(i), + }) + .collect(); + + // Fetch each ALT, resolve its addresses, and append them to all_accounts. + // Track the resolved entries for the summary printed at the end. + let mut alt_summaries: Vec<(Pubkey, Vec, Vec)> = Vec::new(); + + for lookup in &message.address_table_lookups { + let alt_data = rpc_client.get_account(&lookup.account_key).await?.data; + let alt = AddressLookupTable::deserialize(&alt_data)?; + + let mut writable_pubkeys = Vec::new(); + let mut readonly_pubkeys = Vec::new(); + + for &idx in &lookup.writable_indexes { + let pubkey = alt.addresses[idx as usize]; + writable_pubkeys.push(pubkey); + all_accounts.push(AccountEntry { + pubkey, + is_writable: true, + is_signer: false, + }); + } + + for &idx in &lookup.readonly_indexes { + let pubkey = alt.addresses[idx as usize]; + readonly_pubkeys.push(pubkey); + all_accounts.push(AccountEntry { + pubkey, + is_writable: false, + is_signer: false, + }); + } + + alt_summaries.push((lookup.account_key, writable_pubkeys, readonly_pubkeys)); + } + + // Print each instruction. + println!( + "{}", + format!("Instructions ({})", message.instructions.len()).bold() + ); + println!(); + + for (i, ix) in message.instructions.iter().enumerate() { + let program_pubkey = all_accounts[ix.program_id_index as usize].pubkey; + + println!("{}", format!("Instruction {}", i + 1).yellow().bold()); + println!(" Program: {}", program_pubkey); + + if !ix.account_indexes.is_empty() { + println!(" Accounts:"); + for (j, &idx) in ix.account_indexes.iter().enumerate() { + let acc = &all_accounts[idx as usize]; + let flags = match (acc.is_writable, acc.is_signer) { + (true, true) => " [writable, signer]".cyan().to_string(), + (true, false) => " [writable]".cyan().to_string(), + (false, true) => " [signer]".cyan().to_string(), + (false, false) => String::new(), + }; + println!(" {}: {}{}", j + 1, acc.pubkey, flags); + } + } + + if !ix.data.is_empty() { + println!(" Data: {}", bs58::encode(&ix.data).into_string()); + } + + println!(); + } + + // Print ALT summary if any were used. + if !alt_summaries.is_empty() { + println!("{}", "Address Lookup Tables".bold()); + for (key, writable, readonly) in &alt_summaries { + println!(" {}", key); + if !writable.is_empty() { + println!(" Writable:"); + for pk in writable { + println!(" {}", pk); + } + } + if !readonly.is_empty() { + println!(" Readonly:"); + for pk in readonly { + println!(" {}", pk); + } + } + } + println!(); + } + + Ok(()) + } +} diff --git a/cli/src/command/display_vault.rs b/cli/src/command/display_vault.rs index 56dadf9f..e781a5d9 100644 --- a/cli/src/command/display_vault.rs +++ b/cli/src/command/display_vault.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use clap::Args; +/// Derive and display the vault PDA address for a given multisig and vault index. #[derive(Args)] pub struct DisplayVault { /// Multisig Program ID diff --git a/cli/src/command/initiate_program_upgrade.rs b/cli/src/command/initiate_program_upgrade.rs index fd8ebb51..fa085b2e 100644 --- a/cli/src/command/initiate_program_upgrade.rs +++ b/cli/src/command/initiate_program_upgrade.rs @@ -32,6 +32,7 @@ use squads_multisig::state::Permission; use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Create and activate a vault transaction that upgrades a BPF upgradeable program. #[derive(Args)] pub struct InitiateProgramUpgrade { /// RPC URL diff --git a/cli/src/command/initiate_transfer.rs b/cli/src/command/initiate_transfer.rs index 4930a95c..44eab756 100644 --- a/cli/src/command/initiate_transfer.rs +++ b/cli/src/command/initiate_transfer.rs @@ -38,6 +38,7 @@ use crate::command::transfer_common::{ }; use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Create and activate a vault transaction that transfers SOL or SPL tokens from the vault. #[derive(Args)] pub struct InitiateTransfer { /// RPC URL diff --git a/cli/src/command/mod.rs b/cli/src/command/mod.rs index 93cd497e..338ee582 100644 --- a/cli/src/command/mod.rs +++ b/cli/src/command/mod.rs @@ -11,6 +11,8 @@ use crate::command::proposal_vote::ProposalVote; use crate::command::claim_rent::ClaimRent; use crate::command::vault_transaction_accounts_close::VaultTransactionAccountsClose; use crate::command::vault_transaction_create::VaultTransactionCreate; +use crate::command::display_config_transaction::DisplayConfigTransaction; +use crate::command::display_transaction::DisplayTransaction; use crate::command::vault_transaction_execute::VaultTransactionExecute; use clap::Subcommand; @@ -27,6 +29,8 @@ pub mod multisig_create; pub mod program_config_init; pub mod proposal_vote; pub mod claim_rent; +pub mod display_config_transaction; +pub mod display_transaction; pub mod vault_transaction_accounts_close; pub mod vault_transaction_create; pub mod vault_transaction_execute; @@ -47,4 +51,6 @@ pub enum Command { InitiateProgramUpgrade(InitiateProgramUpgrade), DisplayVault(DisplayVault), DisplayProposals(DisplayProposals), + DisplayTransaction(DisplayTransaction), + DisplayConfigTransaction(DisplayConfigTransaction), } diff --git a/cli/src/command/multisig_create.rs b/cli/src/command/multisig_create.rs index 27e9d369..27a8b696 100644 --- a/cli/src/command/multisig_create.rs +++ b/cli/src/command/multisig_create.rs @@ -26,6 +26,7 @@ use squads_multisig::state::{Member, Permissions}; use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Create a new Squads multisig with the specified members and threshold. #[derive(Args)] pub struct MultisigCreate { /// RPC URL diff --git a/cli/src/command/program_config_init.rs b/cli/src/command/program_config_init.rs index ac44c5e5..03d5a0b5 100644 --- a/cli/src/command/program_config_init.rs +++ b/cli/src/command/program_config_init.rs @@ -22,6 +22,7 @@ use squads_multisig::squads_multisig_program::ProgramConfigInitArgs; use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Initialize the global program config account (one-time setup, authority only). #[derive(Args)] pub struct ProgramConfigInit { /// RPC URL diff --git a/cli/src/command/proposal_vote.rs b/cli/src/command/proposal_vote.rs index f8945f69..36060cc1 100644 --- a/cli/src/command/proposal_vote.rs +++ b/cli/src/command/proposal_vote.rs @@ -25,6 +25,7 @@ use squads_multisig::squads_multisig_program::ProposalVoteArgs; use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Cast an approve or reject vote on an existing proposal. #[derive(Args)] pub struct ProposalVote { /// RPC URL diff --git a/cli/src/command/vault_transaction_accounts_close.rs b/cli/src/command/vault_transaction_accounts_close.rs index 316b748b..69725b35 100644 --- a/cli/src/command/vault_transaction_accounts_close.rs +++ b/cli/src/command/vault_transaction_accounts_close.rs @@ -22,6 +22,7 @@ use squads_multisig::squads_multisig_program::instruction::VaultTransactionAccou use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Reclaim rent by closing the accounts of an executed, rejected, or cancelled vault transaction. #[derive(Args)] pub struct VaultTransactionAccountsClose { /// RPC URL diff --git a/cli/src/command/vault_transaction_create.rs b/cli/src/command/vault_transaction_create.rs index ffb8ddfd..40ec3b09 100644 --- a/cli/src/command/vault_transaction_create.rs +++ b/cli/src/command/vault_transaction_create.rs @@ -31,6 +31,7 @@ use squads_multisig::state::Permission; use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Create a new vault transaction and activate its proposal for voting. #[derive(Args)] pub struct VaultTransactionCreate { /// RPC URL diff --git a/cli/src/command/vault_transaction_execute.rs b/cli/src/command/vault_transaction_execute.rs index 4f81066c..89225e0c 100644 --- a/cli/src/command/vault_transaction_execute.rs +++ b/cli/src/command/vault_transaction_execute.rs @@ -25,6 +25,7 @@ use std::time::Duration; use crate::utils::{create_signer_from_path, send_and_confirm_transaction}; +/// Execute an approved vault transaction on-chain. #[derive(Args)] pub struct VaultTransactionExecute { /// RPC URL diff --git a/cli/src/main.rs b/cli/src/main.rs index 53387b7a..fec75b2e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -30,5 +30,7 @@ async fn main() -> eyre::Result<()> { Command::InitiateProgramUpgrade(command) => command.execute().await, Command::DisplayVault(command) => command.execute().await, Command::DisplayProposals(command) => command.execute().await, + Command::DisplayTransaction(command) => command.execute().await, + Command::DisplayConfigTransaction(command) => command.execute().await, } }