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
114 changes: 97 additions & 17 deletions src/cli/wormhole.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,15 +809,20 @@ pub enum WormholeCommands {
/// It mirrors the withdrawal flow used by the miner app.
CollectRewards {
/// Wallet name (used for HD derivation of wormhole secret and exit address)
/// Either --wallet or --mnemonic must be provided.
#[arg(short, long, required_unless_present = "mnemonic")]
/// Either --wallet, --mnemonic, or --secret must be provided.
#[arg(short, long, required_unless_present_any = ["mnemonic", "secret"], conflicts_with_all = ["mnemonic", "secret"])]
wallet: Option<String>,

/// Mnemonic phrase for HD derivation (alternative to --wallet)
/// Use this to derive wormhole secrets without a stored wallet.
#[arg(short = 'm', long, required_unless_present = "wallet")]
#[arg(short = 'm', long, required_unless_present_any = ["wallet", "secret"], conflicts_with_all = ["wallet", "secret"])]
mnemonic: Option<String>,

/// Direct wormhole secret (32-byte hex string, alternative to --wallet or --mnemonic)
/// Use this with a secret generated by `quantus-node key quantus --scheme wormhole`
#[arg(long, required_unless_present_any = ["wallet", "mnemonic"], conflicts_with_all = ["wallet", "mnemonic"])]
secret: Option<String>,

/// Password for the wallet (only used with --wallet)
#[arg(short, long)]
password: Option<String>,
Expand All @@ -830,15 +835,15 @@ pub enum WormholeCommands {
#[arg(short, long)]
amount: Option<f64>,

/// Destination address for withdrawn funds (required when using --mnemonic)
/// Destination address for withdrawn funds (required when using --mnemonic or --secret)
#[arg(long)]
destination: Option<String>,

/// Subsquid indexer URL for querying transfers
#[arg(long, default_value = "https://subsquid.quantus.com/blue/graphql")]
subsquid_url: String,

/// Wormhole address index for HD derivation (default: 0)
/// Wormhole address index for HD derivation (default: 0, ignored when using --secret)
#[arg(long, default_value = "0")]
wormhole_index: usize,

Expand Down Expand Up @@ -1016,6 +1021,7 @@ pub async fn handle_wormhole_command(
WormholeCommands::CollectRewards {
wallet,
mnemonic,
secret,
password,
password_file,
amount,
Expand All @@ -1028,6 +1034,7 @@ pub async fn handle_wormhole_command(
run_collect_rewards(
wallet,
mnemonic,
secret,
password,
password_file,
amount,
Expand Down Expand Up @@ -3052,6 +3059,7 @@ async fn run_dissolve(
async fn run_collect_rewards(
wallet_name: Option<String>,
mnemonic_arg: Option<String>,
secret_arg: Option<String>,
password: Option<String>,
password_file: Option<String>,
amount: Option<f64>,
Expand All @@ -3062,7 +3070,9 @@ async fn run_collect_rewards(
node_url: &str,
at_block: Option<u32>,
) -> crate::error::Result<()> {
use crate::collect_rewards_lib::{collect_rewards, CollectRewardsConfig, ProgressCallback};
use crate::collect_rewards_lib::{
collect_rewards, CollectRewardsConfig, ProgressCallback, WormholeCredential,
};
use colored::Colorize;

log_print!("");
Expand All @@ -3071,28 +3081,34 @@ async fn run_collect_rewards(
log_print!("==================================================");
log_print!("");

// Get mnemonic and wallet address from either wallet or direct mnemonic
let (mnemonic, wallet_address) = if let Some(wallet_name) = wallet_name {
// Get credential and wallet address from wallet, mnemonic, or secret
let (credential, wallet_address) = if let Some(wallet_name) = wallet_name {
// Load from stored wallet
let wallet = load_multiround_wallet(&wallet_name, password, password_file)?;
(wallet.mnemonic, Some(wallet.wallet_address))
(
WormholeCredential::Mnemonic { phrase: wallet.mnemonic, wormhole_index },
Some(wallet.wallet_address),
)
} else if let Some(mnemonic) = mnemonic_arg {
// Use provided mnemonic directly
(mnemonic, None)
(WormholeCredential::Mnemonic { phrase: mnemonic, wormhole_index }, None)
} else if let Some(secret) = secret_arg {
// Use provided secret directly (no HD derivation)
(WormholeCredential::Secret { hex: secret }, None)
} else {
return Err(crate::error::QuantusError::Generic(
"Either --wallet or --mnemonic must be provided".to_string(),
"Either --wallet, --mnemonic, or --secret must be provided".to_string(),
));
};

// Destination address - required when using mnemonic directly
// Destination address - required when using mnemonic or secret directly
let destination_address = if let Some(dest) = &destination {
dest.clone()
} else if let Some(addr) = wallet_address.as_ref() {
addr.clone()
} else {
return Err(crate::error::QuantusError::Generic(
"--destination is required when using --mnemonic".to_string(),
"--destination is required when using --mnemonic or --secret".to_string(),
));
};

Expand All @@ -3103,9 +3119,23 @@ async fn run_collect_rewards(
if let Some(ref addr) = wallet_address {
log_print!(" Wallet: {}", addr.bright_yellow());
} else {
log_print!(" Wallet: {}", "(from mnemonic)".bright_yellow());
match &credential {
WormholeCredential::Mnemonic { .. } => {
log_print!(" Wallet: {}", "(from mnemonic)".bright_yellow());
},
WormholeCredential::Secret { .. } => {
log_print!(" Wallet: {}", "(from secret)".bright_yellow());
},
}
}
match &credential {
WormholeCredential::Mnemonic { wormhole_index, .. } => {
log_print!(" Wormhole index: {}", wormhole_index);
},
WormholeCredential::Secret { .. } => {
log_print!(" Wormhole index: {}", "(N/A - using direct secret)".dimmed());
},
}
log_print!(" Wormhole index: {}", wormhole_index);
log_print!(" Destination: {}", destination_address.bright_green());
log_print!(" Subsquid URL: {}", subsquid_url);
log_print!(" Node URL: {}", node_url);
Expand Down Expand Up @@ -3144,8 +3174,7 @@ async fn run_collect_rewards(
}

let config = CollectRewardsConfig {
mnemonic,
wormhole_index,
credential,
destination_address: destination_address.clone(),
subsquid_url,
node_url: node_url.to_string(),
Expand Down Expand Up @@ -3909,4 +3938,55 @@ mod tests {
}
}
}

/// Wrapper so `WormholeCommands` can be parsed directly via clap in tests.
#[derive(clap::Parser, Debug)]
struct CollectRewardsTestCli {
#[command(subcommand)]
cmd: WormholeCommands,
}

fn try_parse_collect_rewards(extra_args: &[&str]) -> Result<WormholeCommands, clap::Error> {
use clap::Parser;
let mut args = vec!["test", "collect-rewards"];
args.extend_from_slice(extra_args);
CollectRewardsTestCli::try_parse_from(args).map(|cli| cli.cmd)
}

#[test]
fn collect_rewards_requires_one_credential() {
let err = try_parse_collect_rewards(&[]).unwrap_err();
let s = err.to_string();
assert!(
s.contains("--wallet") || s.contains("--mnemonic") || s.contains("--secret"),
"expected missing-credential error, got: {s}"
);
}

#[test]
fn collect_rewards_accepts_each_credential_alone() {
assert!(try_parse_collect_rewards(&["--wallet", "w"]).is_ok());
assert!(try_parse_collect_rewards(&["--mnemonic", "word ".repeat(24).trim()]).is_ok());
assert!(try_parse_collect_rewards(&[
"--secret",
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
])
.is_ok());
}

#[test]
fn collect_rewards_credentials_mutually_exclusive() {
let pairs: &[(&str, &str, &str, &str)] = &[
("--wallet", "w", "--mnemonic", "m"),
("--wallet", "w", "--secret", "s"),
("--mnemonic", "m", "--secret", "s"),
];
for (a, av, b, bv) in pairs {
let err = try_parse_collect_rewards(&[a, av, b, bv]).unwrap_err().to_string();
assert!(
err.contains("cannot be used with"),
"expected conflict error for {a} + {b}, got: {err}"
);
}
}
}
Loading
Loading