Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ lib
.env.*
.env

cli/*.sh
cli/*.sh

.idea
2 changes: 1 addition & 1 deletion 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 cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
48 changes: 48 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -342,3 +344,49 @@ vault-transaction-execute --keypair /path/to/keypair.json --multisig-pubkey <MUL
```

This example executes the transaction at index 1 in the specified multisig.

## Display Transaction

### Description

Fetches a vault transaction account and displays its decoded instructions, including each instruction's program ID, accounts (with writable/signer flags), base58-encoded data, and any address lookup tables used.

### Syntax

```bash
display-transaction --transaction-address <TRANSACTION_ADDRESS> [--rpc-url <RPC_URL>]
```

### Parameters

- `--transaction-address <TRANSACTION_ADDRESS>`: The public key of the VaultTransaction account to inspect.
- `--rpc-url <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 <TRANSACTION_ADDRESS> [--rpc-url <RPC_URL>]
```

### Parameters

- `--transaction-address <TRANSACTION_ADDRESS>`: The public key of the ConfigTransaction account to inspect.
- `--rpc-url <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
```
1 change: 1 addition & 0 deletions cli/src/command/config_transaction_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cli/src/command/config_transaction_execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
180 changes: 180 additions & 0 deletions cli/src/command/display_config_transaction.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

/// 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!();
Comment thread
sean-sqds marked this conversation as resolved.
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(())
}
}
1 change: 1 addition & 0 deletions cli/src/command/display_proposals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading