diff --git a/src/app.rs b/src/app.rs index 06fd5a1..57e6fa7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -151,7 +151,7 @@ pub async fn run() -> Result<()> { log::info!("🚀 Starting brc721"); let cli = crate::cli::parse(); - let ctx = context::Context::from_cli(&cli); + let ctx = context::Context::from_cli(&cli)?; if let Some(path) = ctx.log_file.as_deref() { log::info!("📝 Log file: {}", path.to_string_lossy()); diff --git a/src/commands/tx.rs b/src/commands/tx.rs index fd3237d..e6cbb24 100644 --- a/src/commands/tx.rs +++ b/src/commands/tx.rs @@ -85,9 +85,11 @@ fn run_register_collection( rebaseable, }; let payload = Brc721Payload::RegisterCollection(msg); - let output = Brc721OpReturnOutput::new(payload).into_txout().unwrap(); + let output = Brc721OpReturnOutput::new(payload) + .into_txout() + .context("build register-collection op_return output")?; - let passphrase = resolve_passphrase(passphrase); + let passphrase = resolve_passphrase(passphrase)?; let tx = wallet .build_tx(output, fee_rate, passphrase) .context("build tx")?; @@ -126,9 +128,11 @@ fn run_register_ownership( )?; let payload = Brc721Payload::RegisterOwnership(ownership); - let output = Brc721OpReturnOutput::new(payload).into_txout().unwrap(); + let output = Brc721OpReturnOutput::new(payload) + .into_txout() + .context("build register-ownership op_return output")?; - let passphrase = resolve_passphrase(passphrase); + let passphrase = resolve_passphrase(passphrase)?; let tx = wallet .build_tx_with_op_return_and_payments( output, @@ -158,7 +162,7 @@ fn run_send_amount( let wallet = load_wallet(ctx)?; let amount = Amount::from_sat(amount_sat); let address = Address::from_str(to)?.require_network(ctx.network)?; - let passphrase = resolve_passphrase(passphrase); + let passphrase = resolve_passphrase(passphrase)?; let tx = wallet .build_payment_tx(&address, amount, fee_rate, passphrase) .context("build payment tx")?; @@ -235,7 +239,7 @@ fn run_send_assets( let address = Address::from_str(to)?.require_network(ctx.network)?; let dust_amount = Amount::from_sat(dust_sat); - let passphrase = resolve_passphrase(passphrase); + let passphrase = resolve_passphrase(passphrase)?; let lock_outpoints = compute_wallet_token_outpoints_to_lock(&storage, &wallet_utxos, &token_outpoints) @@ -372,7 +376,7 @@ fn run_mix( .into_txout() .context("build mix op_return output")?; - let passphrase = resolve_passphrase(passphrase); + let passphrase = resolve_passphrase(passphrase)?; let tx = wallet .build_mix_tx( &token_outpoints, @@ -402,14 +406,13 @@ fn load_wallet(ctx: &context::Context) -> Result { Brc721Wallet::load(&ctx.data_dir, ctx.network, &ctx.rpc_url, ctx.auth.clone()) } -fn resolve_passphrase(passphrase: Option) -> SecretString { - passphrase.map(SecretString::from).unwrap_or_else(|| { - SecretString::from( - prompt_passphrase_once() - .expect("prompt") - .unwrap_or_default(), - ) - }) +fn resolve_passphrase(passphrase: Option) -> Result { + if let Some(passphrase) = passphrase { + return Ok(SecretString::from(passphrase)); + } + + let passphrase = prompt_passphrase_once().context("prompt passphrase")?; + Ok(SecretString::from(passphrase.unwrap_or_default())) } fn parse_outpoints(outpoints: &[String]) -> Result> { diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index eb7c1ae..4958fa6 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -55,7 +55,7 @@ fn run_init( Mnemonic::parse_in(Language::English, mnemonic_str).context("invalid mnemonic")?; // Resolve passphrase - let passphrase = resolve_passphrase_init(passphrase); + let passphrase = resolve_passphrase_init(passphrase)?; // Create new wallet let wallet = Brc721Wallet::create( @@ -475,21 +475,24 @@ fn load_local_wallet(ctx: &context::Context) -> Result { LocalWallet::load(&ctx.data_dir, ctx.network).context("loading local wallet") } -fn resolve_passphrase_init(passphrase: Option) -> SecretString { - passphrase.map(SecretString::from).unwrap_or_else(|| { - SecretString::from(prompt_passphrase().expect("prompt").unwrap_or_default()) - }) +fn resolve_passphrase_init(passphrase: Option) -> Result { + if let Some(passphrase) = passphrase { + return Ok(SecretString::from(passphrase)); + } + + let passphrase = prompt_passphrase().context("prompt passphrase")?; + Ok(SecretString::from(passphrase.unwrap_or_default())) } -fn generate_mnemonic(short: bool) -> Mnemonic { +fn generate_mnemonic(short: bool) -> Result { let entropy_bytes = if short { 16 } else { 32 }; let mut entropy = vec![0u8; entropy_bytes]; OsRng.fill_bytes(&mut entropy); - Mnemonic::from_entropy(&entropy).expect("mnemonic") + Mnemonic::from_entropy(&entropy).context("generate mnemonic") } fn run_generate(short: bool) -> Result<()> { - let mnemonic = generate_mnemonic(short); + let mnemonic = generate_mnemonic(short)?; println!("{}", mnemonic); Ok(()) } @@ -500,7 +503,7 @@ mod tests { #[test] fn generate_mnemonic_is_valid_12_words() { - let mnemonic = generate_mnemonic(true); + let mnemonic = generate_mnemonic(true).expect("generate mnemonic"); assert_eq!(mnemonic.word_count(), 12); let parsed = Mnemonic::parse_in(Language::English, mnemonic.to_string()).expect("parse"); assert_eq!(parsed.word_count(), 12); @@ -508,7 +511,7 @@ mod tests { #[test] fn generate_mnemonic_is_valid_24_words() { - let mnemonic = generate_mnemonic(false); + let mnemonic = generate_mnemonic(false).expect("generate mnemonic"); assert_eq!(mnemonic.word_count(), 24); let parsed = Mnemonic::parse_in(Language::English, mnemonic.to_string()).expect("parse"); assert_eq!(parsed.word_count(), 24); diff --git a/src/context.rs b/src/context.rs index 260b516..04698d6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -18,18 +18,18 @@ pub struct Context { } impl Context { - pub fn from_cli(cli: &crate::cli::Cli) -> Self { + pub fn from_cli(cli: &crate::cli::Cli) -> Result { let auth = match (&cli.rpc_user, &cli.rpc_pass) { (Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()), _ => Auth::None, }; - let rpc_url = Url::parse(&cli.rpc_url).expect("rpc url"); + let rpc_url = Url::parse(&cli.rpc_url).context("invalid rpc url")?; let network = detect_network(&rpc_url, &auth) - .unwrap_or_else(|_| panic!("detect network from node {}", rpc_url)); + .with_context(|| format!("detect network from node {}", rpc_url))?; let mut data_dir = PathBuf::from(&cli.data_dir); let network_dir = network.to_string(); data_dir.push(network_dir); - Self { + Ok(Self { network, data_dir, rpc_url, @@ -40,7 +40,7 @@ impl Context { log_file: cli.log_file.as_deref().map(PathBuf::from), reset: cli.reset, api_listen: cli.api_listen, - } + }) } } diff --git a/src/parser/brc721_parser.rs b/src/parser/brc721_parser.rs index 9b820f5..9b5915d 100644 --- a/src/parser/brc721_parser.rs +++ b/src/parser/brc721_parser.rs @@ -264,13 +264,14 @@ impl Brc721Parser { } let remaining_outputs_len = remaining_outputs.len(); + let last_output = remaining_outputs.last().ok_or_else(|| { + Brc721Error::TxError("implicit transfer requires at least one spendable output".into()) + })?; for (input_index, input) in token_inputs.into_iter().enumerate() { let (dest_vout, dest_txout) = if input_index < remaining_outputs_len { remaining_outputs[input_index] } else { - *remaining_outputs - .last() - .expect("non-empty remaining_outputs") + *last_output }; let dest_owner_h160 = h160_from_script_pubkey(&dest_txout.script_pubkey); diff --git a/src/wallet/brc721_wallet.rs b/src/wallet/brc721_wallet.rs index 9cc00ed..1161030 100644 --- a/src/wallet/brc721_wallet.rs +++ b/src/wallet/brc721_wallet.rs @@ -25,7 +25,7 @@ impl Brc721Wallet { auth: Auth, ) -> Result { let seed = mnemonic.to_seed(String::default()); - let master_xprv = Xpriv::new_master(network, &seed).expect("master_key"); + let master_xprv = Xpriv::new_master(network, &seed).context("create master key")?; let external = Bip86(master_xprv, KeychainKind::External); let internal = Bip86(master_xprv, KeychainKind::Internal); diff --git a/src/wallet/remote_wallet.rs b/src/wallet/remote_wallet.rs index 3a87071..df0402c 100644 --- a/src/wallet/remote_wallet.rs +++ b/src/wallet/remote_wallet.rs @@ -132,8 +132,16 @@ impl RemoteWallet { ], ) .context("watch wallet created")?; - if ans["name"].as_str().unwrap() != self.watch_name { - return Err(anyhow::anyhow!("Unexpected wallet name: {:?}", ans["name"])); + let created_name = ans + .get("name") + .and_then(|value| value.as_str()) + .context("createwallet response missing name")?; + if created_name != self.watch_name { + return Err(anyhow::anyhow!( + "Unexpected wallet name: expected '{}', got '{}'", + self.watch_name, + created_name + )); } if !ans["warning"].is_null() { return Err(anyhow::anyhow!( @@ -163,12 +171,15 @@ impl RemoteWallet { .call::("importdescriptors", &[imports]) .context("import descriptor")?; - let arr = ans.as_array().expect("array"); - if !arr.iter().all(|e| e["success"].as_bool() == Some(true)) { - return Err(anyhow::anyhow!( - "Failed to import descriptors: {}", - serde_json::to_string_pretty(&ans).unwrap() - )); + let arr = ans + .as_array() + .context("importdescriptors response was not an array")?; + if !arr + .iter() + .all(|e| e.get("success").and_then(|v| v.as_bool()) == Some(true)) + { + let pretty = serde_json::to_string_pretty(&ans).unwrap_or_else(|_| format!("{ans:?}")); + return Err(anyhow::anyhow!("Failed to import descriptors: {}", pretty)); } Ok(()) } diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index 8618834..2c83c52 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -49,7 +49,9 @@ impl Signer { .create_wallet_no_persist() .map_err(|e| Brc721Error::WalletError(e.to_string()))?; - let finalized = wallet.sign(psbt, Default::default()).expect("sign"); + let finalized = wallet + .sign(psbt, Default::default()) + .map_err(|e| Brc721Error::WalletError(e.to_string()))?; Ok(finalized) } }