use cardanowall::verifier::fetch::DENY_HOSTS_DEFAULT;
use cardanowall::verifier::{
verify_report_to_dict, verify_tx, Decryption, Profile, VerifyTxInput,
CONFIRMATION_DEPTH_THRESHOLD_DEFAULT,
};
use clap::Args;
use crate::config::{
read_config_file, resolve_gateways, GatewayFlags, SystemConfigEnv, SystemGatewayEnv,
};
use crate::output::render_human_report;
use crate::secret::{SecretEnv, SystemSecretEnv};
use crate::util::{hex_to_bytes, CliError};
#[derive(Debug, Args)]
pub struct VerifyArgs {
pub tx_hash: String,
#[arg(long)]
pub profile: Option<String>,
#[arg(long = "cardano-gateway", visible_alias = "gateway")]
pub cardano_gateway: Vec<String>,
#[arg(long)]
pub blockfrost: Option<String>,
#[arg(long = "arweave-gateway")]
pub arweave_gateway: Vec<String>,
#[arg(long = "ipfs-gateway")]
pub ipfs_gateway: Vec<String>,
#[arg(long)]
pub threshold: Option<String>,
#[arg(long = "deny-host")]
pub deny_host: Vec<String>,
#[arg(long = "secret-key")]
pub secret_key: Vec<String>,
#[arg(long = "secret-key-file")]
pub secret_key_file: Option<String>,
#[arg(long = "secret-key-stdin")]
pub secret_key_stdin: bool,
#[arg(long = "no-fetch")]
pub no_fetch: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub pretty: bool,
}
const PROFILES: [(&str, Profile); 4] = [
("core", Profile::Core),
("signed", Profile::Signed),
("sealed", Profile::Sealed),
("recipient-sealed", Profile::RecipientSealed),
];
struct DecryptionSpec {
item_index: i64,
recipient_secret_key: Vec<u8>,
}
fn parse_threshold(raw: Option<&str>) -> Result<Option<u32>, CliError> {
let Some(raw) = raw else { return Ok(None) };
match raw.parse::<i64>() {
Ok(n) if n >= 0 && n.to_string() == raw => Ok(Some(n as u32)),
_ => Err(CliError::input(format!(
"verify: --threshold must be a non-negative integer; got \"{raw}\""
))),
}
}
fn collect_secret_key_specs(
args: &VerifyArgs,
env: &dyn SecretEnv,
) -> Result<Vec<String>, CliError> {
if !args.secret_key.is_empty() {
return Ok(args.secret_key.clone());
}
if let Some(path) = args.secret_key_file.as_deref().filter(|p| !p.is_empty()) {
let raw = env.read_file(path)?;
return Ok(split_secret_lines(&raw));
}
if args.secret_key_stdin {
let raw = env.read_stdin()?;
return Ok(split_secret_lines(&raw));
}
if let Some(value) = env.var("CARDANOWALL_RECIPIENT_KEY") {
return Ok(split_secret_list(&value));
}
Ok(Vec::new())
}
fn split_secret_lines(raw: &str) -> Vec<String> {
raw.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(str::to_string)
.collect()
}
fn split_secret_list(raw: &str) -> Vec<String> {
raw.split([',', ' ', '\t', '\n'])
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn parse_secret_key(raw: &str) -> Result<DecryptionSpec, CliError> {
let (idx, hex) = match raw.split_once(':') {
Some((idx_raw, hex)) => {
let idx: i64 = idx_raw.parse().map_err(|_| {
CliError::input(format!(
"verify: --secret-key index must be a non-negative integer; got \"{raw}\""
))
})?;
if idx < 0 || idx.to_string() != idx_raw {
return Err(CliError::input(format!(
"verify: --secret-key index must be a non-negative integer; got \"{raw}\""
)));
}
(idx, hex)
}
None => (0, raw),
};
let bytes =
hex_to_bytes(hex).map_err(|e| CliError::input(format!("verify: --secret-key {e}")))?;
Ok(DecryptionSpec {
item_index: idx,
recipient_secret_key: bytes,
})
}
fn choose_profile(args: &VerifyArgs, have_secret_keys: bool) -> Result<Profile, CliError> {
if let Some(name) = &args.profile {
return PROFILES
.iter()
.find(|(n, _)| *n == name)
.map(|(_, p)| *p)
.ok_or_else(|| {
CliError::input(format!(
"verify: --profile must be one of {{core, signed, sealed, recipient-sealed}}; got \"{name}\""
))
});
}
if have_secret_keys {
return Ok(Profile::RecipientSealed);
}
Ok(Profile::Signed)
}
pub fn run(args: VerifyArgs) -> Result<(), CliError> {
if args.tx_hash.len() != 64 || !args.tx_hash.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(CliError::input(format!(
"verify: <tx-hash> must be 64 hex chars; got \"{}\"",
args.tx_hash
)));
}
let threshold = parse_threshold(args.threshold.as_deref())?;
let secret_key_specs = collect_secret_key_specs(&args, &SystemSecretEnv)?;
let mut decryption: Vec<DecryptionSpec> = Vec::new();
for raw in &secret_key_specs {
decryption.push(parse_secret_key(raw)?);
}
let profile = choose_profile(&args, !decryption.is_empty())?;
let config = read_config_file(&SystemConfigEnv)?;
let flags = GatewayFlags {
gateway: args.cardano_gateway.clone(),
blockfrost: args.blockfrost.clone(),
arweave_gateway: args.arweave_gateway.clone(),
ipfs_gateway: args.ipfs_gateway.clone(),
threshold,
deny_host: args.deny_host.clone(),
};
let resolved = resolve_gateways(&flags, &SystemGatewayEnv, config.as_ref())?;
let deny_hosts = resolved.deny_hosts.clone().unwrap_or_else(|| {
DENY_HOSTS_DEFAULT
.iter()
.map(|s| (*s).to_string())
.collect()
});
let mut input = VerifyTxInput::new(args.tx_hash.to_lowercase());
input.profile = profile;
input.cardano_gateway_chain = Some(resolved.cardano_gateway_chain.clone());
input.arweave_gateway_chain = Some(resolved.arweave_gateway_chain.clone());
input.ipfs_gateway_chain = resolved.ipfs_gateway_chain.clone();
input.blockfrost_project_id = resolved.blockfrost_project_id.clone();
input.confirmation_depth_threshold = Some(
resolved
.confirmation_depth_threshold
.unwrap_or(CONFIRMATION_DEPTH_THRESHOLD_DEFAULT),
);
input.deny_hosts = Some(deny_hosts);
if !decryption.is_empty() {
input.decryption = Some(
decryption
.into_iter()
.map(|d| Decryption::Recipient {
item_index: d.item_index,
recipient_secret_key: d.recipient_secret_key,
})
.collect(),
);
}
if args.no_fetch {
input.arweave_gateway_chain = Some(Vec::new());
input.ipfs_gateway_chain = Some(Vec::new());
}
let report = verify_tx(&input);
if args.json {
let dict = verify_report_to_dict(&report);
let rendered = if args.pretty {
serde_json::to_string_pretty(&dict)
} else {
serde_json::to_string(&dict)
}
.expect("VerifyReport dict serialises");
println!("{rendered}");
} else {
render_human_report(&report);
}
exit_code_for_report(&report)
}
pub fn exit_code_for_report(report: &cardanowall::verifier::VerifyReport) -> Result<(), CliError> {
let code = i32::from(report.exit_code.as_u8());
if code == 0 {
Ok(())
} else {
Err(CliError {
code,
message: String::new(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base_args(tx_hash: &str) -> VerifyArgs {
VerifyArgs {
tx_hash: tx_hash.to_string(),
profile: None,
cardano_gateway: vec![],
blockfrost: None,
arweave_gateway: vec![],
ipfs_gateway: vec![],
threshold: None,
deny_host: vec![],
secret_key: vec![],
secret_key_file: None,
secret_key_stdin: false,
no_fetch: false,
json: true,
pretty: false,
}
}
#[test]
fn rejects_non_hex_tx_hash() {
assert_eq!(run(base_args("not-a-hex-string")).unwrap_err().code, 4);
}
#[test]
fn rejects_bad_threshold() {
assert_eq!(parse_threshold(Some("banana")).unwrap_err().code, 4);
assert_eq!(parse_threshold(Some("-1")).unwrap_err().code, 4);
assert_eq!(parse_threshold(Some("15")).unwrap(), Some(15));
assert_eq!(parse_threshold(None).unwrap(), None);
}
#[test]
fn secret_key_parses_index_prefix() {
let spec = parse_secret_key(&format!("3:{}", "ab".repeat(32))).unwrap();
assert_eq!(spec.item_index, 3);
assert_eq!(spec.recipient_secret_key.len(), 32);
let bare = parse_secret_key(&"cd".repeat(32)).unwrap();
assert_eq!(bare.item_index, 0);
}
#[test]
fn unknown_profile_is_input_error() {
let mut args = base_args(&"0".repeat(64));
args.profile = Some("nope".to_string());
assert_eq!(choose_profile(&args, false).unwrap_err().code, 4);
}
#[test]
fn secret_key_specs_from_flags_take_priority() {
use crate::secret::test_support::FakeSecretEnv;
let mut args = base_args(&"0".repeat(64));
args.secret_key = vec![format!("0:{}", "ab".repeat(32))];
let env = FakeSecretEnv {
vars: std::collections::HashMap::from([(
"CARDANOWALL_RECIPIENT_KEY".to_string(),
"cd".repeat(32),
)]),
..FakeSecretEnv::default()
};
let specs = collect_secret_key_specs(&args, &env).unwrap();
assert_eq!(specs, vec![format!("0:{}", "ab".repeat(32))]);
assert_eq!(
choose_profile(&args, !specs.is_empty()).unwrap(),
Profile::RecipientSealed
);
}
#[test]
fn secret_key_specs_from_env_when_no_flag() {
use crate::secret::test_support::FakeSecretEnv;
let args = base_args(&"0".repeat(64));
let env = FakeSecretEnv {
vars: std::collections::HashMap::from([(
"CARDANOWALL_RECIPIENT_KEY".to_string(),
format!("{}, 1:{}", "ab".repeat(32), "cd".repeat(32)),
)]),
..FakeSecretEnv::default()
};
let specs = collect_secret_key_specs(&args, &env).unwrap();
assert_eq!(specs.len(), 2);
}
}