Skip to content

Commit 45fb19f

Browse files
committed
feat(cli): add --sign-only flag for multisig transaction export
1 parent ba82ea9 commit 45fb19f

3 files changed

Lines changed: 102 additions & 19 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clients/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ spl-stake-pool = { version = "=2.0.3", path = "../../program", features = ["no-e
2828
spl-token = { version = "=8.0", features = ["no-entrypoint",] }
2929
spl-token-2022 = { version = "=8.0", features = ["no-entrypoint",] }
3030
bincode = "1.3.1"
31+
bs58 = "0.5"
3132

3233
[[bin]]
3334
name = "fogo-stake-pool"

clients/cli/src/main.rs

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use {
3838
message::Message,
3939
native_token::{self, Sol},
4040
signature::{Keypair, Signer},
41+
signer::null_signer::NullSigner,
4142
signers::Signers,
4243
transaction::Transaction,
4344
},
@@ -70,6 +71,8 @@ pub(crate) struct Config {
7071
token_owner: Box<dyn Signer>,
7172
fee_payer: Box<dyn Signer>,
7273
dry_run: bool,
74+
sign_only: bool,
75+
multisig_signers: Vec<NullSigner>,
7376
no_update: bool,
7477
compute_unit_price: Option<u64>,
7578
compute_unit_limit: ComputeUnitLimit,
@@ -205,7 +208,11 @@ fn send_transaction_no_wait(
205208
config: &Config,
206209
transaction: Transaction,
207210
) -> solana_client::client_error::Result<()> {
208-
if config.dry_run {
211+
if config.sign_only {
212+
let serialized = bincode::serialize(&transaction).unwrap();
213+
let encoded = bs58::encode(&serialized).into_string();
214+
println!("{}", encoded);
215+
} else if config.dry_run {
209216
let result = config.rpc_client.simulate_transaction(&transaction)?;
210217
println!("Simulate result: {:?}", result);
211218
} else {
@@ -219,7 +226,11 @@ fn send_transaction(
219226
config: &Config,
220227
transaction: Transaction,
221228
) -> solana_client::client_error::Result<()> {
222-
if config.dry_run {
229+
if config.sign_only {
230+
let serialized = bincode::serialize(&transaction).unwrap();
231+
let encoded = bs58::encode(&serialized).into_string();
232+
println!("{}", encoded);
233+
} else if config.dry_run {
223234
let result = config.rpc_client.simulate_transaction(&transaction)?;
224235
println!("Simulate result: {:?}", result);
225236
} else {
@@ -265,10 +276,12 @@ fn checked_transaction_with_signers_and_additional_fee<T: Signers>(
265276
Some(&config.fee_payer.pubkey()),
266277
&recent_blockhash,
267278
);
268-
check_fee_payer_balance(
269-
config,
270-
additional_fee.saturating_add(config.rpc_client.get_fee_for_message(&message)?),
271-
)?;
279+
if !config.sign_only {
280+
check_fee_payer_balance(
281+
config,
282+
additional_fee.saturating_add(config.rpc_client.get_fee_for_message(&message)?),
283+
)?;
284+
}
272285
let transaction = Transaction::new(signers, message, recent_blockhash);
273286
Ok(transaction)
274287
}
@@ -2097,18 +2110,26 @@ fn command_set_manager(
20972110
config: &Config,
20982111
stake_pool_address: &Pubkey,
20992112
new_manager: &Option<Box<dyn Signer>>,
2113+
new_manager_pubkey: &Option<Pubkey>,
21002114
new_fee_receiver: &Option<Pubkey>,
21012115
) -> CommandResult {
21022116
if !config.no_update {
21032117
command_update(config, stake_pool_address, false, false, false)?;
21042118
}
21052119
let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?;
21062120

2107-
// If new accounts are missing in the arguments use the old ones
2108-
let (new_manager_pubkey, mut signers): (Pubkey, Vec<&dyn Signer>) = match new_manager {
2109-
None => (stake_pool.manager, vec![]),
2110-
Some(value) => (value.pubkey(), vec![value.as_ref()]),
2111-
};
2121+
// Determine new manager pubkey and signers
2122+
let (new_manager_pk, mut signers): (Pubkey, Vec<&dyn Signer>) =
2123+
if let Some(signer) = new_manager {
2124+
// New manager provided as keypair
2125+
(signer.pubkey(), vec![signer.as_ref()])
2126+
} else if let Some(pubkey) = new_manager_pubkey {
2127+
// New manager provided as pubkey only (for multisig vaults)
2128+
(*pubkey, vec![])
2129+
} else {
2130+
// Keep current manager
2131+
(stake_pool.manager, vec![])
2132+
};
21122133

21132134
let new_fee_receiver = match new_fee_receiver {
21142135
None => stake_pool.manager_fee_account,
@@ -2137,7 +2158,7 @@ fn command_set_manager(
21372158
&config.stake_pool_program_id,
21382159
stake_pool_address,
21392160
&config.manager.pubkey(),
2140-
&new_manager_pubkey,
2161+
&new_manager_pk,
21412162
&new_fee_receiver,
21422163
)],
21432164
&signers,
@@ -2201,17 +2222,33 @@ fn command_set_fee(
22012222
stake_pool_address: &Pubkey,
22022223
new_fee: FeeType,
22032224
) -> CommandResult {
2204-
if !config.no_update {
2225+
if !config.no_update && !config.sign_only {
22052226
command_update(config, stake_pool_address, false, false, false)?;
22062227
}
2207-
let mut signers = vec![config.fee_payer.as_ref(), config.manager.as_ref()];
2208-
unique_signers!(signers);
2228+
2229+
// Determine manager pubkey and signer based on mode
2230+
let (manager_pubkey, signers): (Pubkey, Vec<&dyn Signer>) =
2231+
if config.sign_only && !config.multisig_signers.is_empty() {
2232+
// Use first multisig signer as manager for sign-only mode
2233+
let manager_pubkey = config.multisig_signers[0].pubkey();
2234+
let mut signers: Vec<&dyn Signer> = vec![config.fee_payer.as_ref()];
2235+
for signer in &config.multisig_signers {
2236+
signers.push(signer);
2237+
}
2238+
(manager_pubkey, signers)
2239+
} else {
2240+
let mut signers: Vec<&dyn Signer> =
2241+
vec![config.fee_payer.as_ref(), config.manager.as_ref()];
2242+
unique_signers!(signers);
2243+
(config.manager.pubkey(), signers)
2244+
};
2245+
22092246
let transaction = checked_transaction_with_signers(
22102247
config,
22112248
&[spl_stake_pool::instruction::set_fee(
22122249
&config.stake_pool_program_id,
22132250
stake_pool_address,
2214-
&config.manager.pubkey(),
2251+
&manager_pubkey,
22152252
new_fee,
22162253
)],
22172254
&signers,
@@ -2279,6 +2316,24 @@ fn main() {
22792316
.global(true)
22802317
.help("Simulate transaction instead of executing"),
22812318
)
2319+
.arg(
2320+
Arg::with_name("sign_only")
2321+
.long("sign-only")
2322+
.takes_value(false)
2323+
.global(true)
2324+
.help("Output base58-encoded transaction for multisig import instead of executing"),
2325+
)
2326+
.arg(
2327+
Arg::with_name("multisig_signer")
2328+
.long("multisig-signer")
2329+
.value_name("PUBKEY")
2330+
.validator(is_valid_pubkey)
2331+
.takes_value(true)
2332+
.multiple(true)
2333+
.global(true)
2334+
.requires("sign_only")
2335+
.help("Pubkey of a multisig signer (requires --sign-only). Can be specified multiple times."),
2336+
)
22822337
.arg(
22832338
Arg::with_name("no_update")
22842339
.long("no-update")
@@ -2987,8 +3042,18 @@ fn main() {
29873042
.validator(is_valid_signer)
29883043
.value_name("KEYPAIR")
29893044
.takes_value(true)
3045+
.conflicts_with("new_manager_pubkey")
29903046
.help("Keypair for the new stake pool manager."),
29913047
)
3048+
.arg(
3049+
Arg::with_name("new_manager_pubkey")
3050+
.long("new-manager-pubkey")
3051+
.validator(is_pubkey)
3052+
.value_name("PUBKEY")
3053+
.takes_value(true)
3054+
.conflicts_with("new_manager")
3055+
.help("Public key for the new stake pool manager (use for multisig vaults)."),
3056+
)
29923057
.arg(
29933058
Arg::with_name("new_fee_receiver")
29943059
.long("new-fee-receiver")
@@ -2999,6 +3064,7 @@ fn main() {
29993064
)
30003065
.group(ArgGroup::with_name("new_accounts")
30013066
.arg("new_manager")
3067+
.arg("new_manager_pubkey")
30023068
.arg("new_fee_receiver")
30033069
.required(true)
30043070
.multiple(true)
@@ -3212,6 +3278,15 @@ fn main() {
32123278
OutputFormat::Display
32133279
});
32143280
let dry_run = matches.is_present("dry_run");
3281+
let sign_only = matches.is_present("sign_only");
3282+
let multisig_signers: Vec<NullSigner> = matches
3283+
.values_of("multisig_signer")
3284+
.map(|values| {
3285+
values
3286+
.map(|s| NullSigner::new(&s.parse::<Pubkey>().unwrap()))
3287+
.collect()
3288+
})
3289+
.unwrap_or_default();
32153290
let no_update = matches.is_present("no_update");
32163291
let compute_unit_price = value_t!(matches, COMPUTE_UNIT_PRICE_ARG.name, u64).ok();
32173292
let compute_unit_limit = matches
@@ -3239,6 +3314,8 @@ fn main() {
32393314
token_owner,
32403315
fee_payer,
32413316
dry_run,
3317+
sign_only,
3318+
multisig_signers,
32423319
no_update,
32433320
compute_unit_price,
32443321
compute_unit_limit,
@@ -3433,7 +3510,8 @@ fn main() {
34333510
("set-manager", Some(arg_matches)) => {
34343511
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
34353512

3436-
let new_manager = if arg_matches.value_of("new_manager").is_some() {
3513+
let (new_manager, new_manager_pubkey) = if arg_matches.value_of("new_manager").is_some()
3514+
{
34373515
let signer = get_signer(
34383516
arg_matches,
34393517
"new-manager",
@@ -3445,16 +3523,19 @@ fn main() {
34453523
allow_null_signer: true,
34463524
},
34473525
);
3448-
Some(signer)
3526+
(Some(signer), None)
3527+
} else if let Some(pubkey) = pubkey_of(arg_matches, "new_manager_pubkey") {
3528+
(None, Some(pubkey))
34493529
} else {
3450-
None
3530+
(None, None)
34513531
};
34523532

34533533
let new_fee_receiver: Option<Pubkey> = pubkey_of(arg_matches, "new_fee_receiver");
34543534
command_set_manager(
34553535
&config,
34563536
&stake_pool_address,
34573537
&new_manager,
3538+
&new_manager_pubkey,
34583539
&new_fee_receiver,
34593540
)
34603541
}

0 commit comments

Comments
 (0)