]> Untitled Git - bdk-cli/commitdiff
fix(create_tx,bump_fee): return errors instead of panicking
authorVihiga Tyonum <withtvpeter@gmail.com>
Sat, 26 Sep 2026 07:56:45 +0000 (08:56 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Sat, 26 Sep 2026 07:56:45 +0000 (08:56 +0100)
`create_tx` and `bump_fee` called `.unwrap()` on `Result`s carrying
user-supplied input, so a mistyped argument aborted the process with a
Rust panic (exit 101) instead of a usage error (exit 1).

- propagate `add_utxos` failures in `create_tx` and `bump_fee` with `?`,
  adding `BDKCliError::AddUtxoError` so the malformed outpoint is named
- propagate `--add_data` base64 decoding and `PushBytesBuf` conversion
  failures in `create_tx`
- stop flattening `add_utxos` errors into `CreateTxError::UnknownUtxo`
in `create_sp_tx` and `create_dns_tx`, which discarded the real cause
- add an integration test asserting exit 1, never 101, for an unknown
  outpoint and for malformed base64

Fixes #325

CHANGELOG.md
src/error.rs
src/handlers/dns/mod.rs
src/handlers/offline.rs
tests/integration/offline.rs

index ec50ea1f5631795e49294ab1368f691ba1d63005..636e8034bb415d01e17ae402c1db8873bb091a5e 100644 (file)
@@ -7,6 +7,7 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
 
 - Added support for Multipath (two-paths) descriptors.
 - Fixed the data directory and config.toml permission being world-readable (0755/0644) to 0700/0600 on Unix.
+- Fixed `create_tx` and `bump_fee` panicking on malformed `--utxos` and `--add_data` values instead of returning an error
 
 ## [4.0.0]
 
index bfad510a34b065649f84a866f00d7e5e98190ad5..9ceb571ab1062b7c56c80ceee35d53845b1d58d7 100644 (file)
@@ -5,6 +5,9 @@ use thiserror::Error;
 
 #[derive(Debug, Error)]
 pub enum BDKCliError {
+    #[error("Add UTXO error: {0}")]
+    AddUtxoError(#[from] bdk_wallet::tx_builder::AddUtxoError),
+
     #[error("Cannot provide both a multipath descriptor and a separate internal descriptor.")]
     AmbiguousDescriptors,
 
index 628bfbdbb6ded62a1f89fe1f71e36252e7801d49..6719db087782208fcf9f1155687a2b0100d6507a 100644 (file)
@@ -135,17 +135,13 @@ impl AsyncAppCommand<AppContext<OfflineOperations<'_>>> for CreateDnsTxCommand {
             tx_builder.fee_rate(fee_rate);
         }
         if let Some(utxos) = &self.utxos {
-            tx_builder
-                .add_utxos(&utxos[..])
-                .map_err(|_| bdk_wallet::error::CreateTxError::UnknownUtxo)?;
+            tx_builder.add_utxos(&utxos[..])?;
         }
         if let Some(unspendable) = &self.unspendable {
             tx_builder.unspendable(unspendable.to_vec());
         }
         if let Some(base64_data) = &self.add_data {
-            let op_return_data = BASE64_STANDARD
-                .decode(base64_data)
-                .map_err(|e| Error::Generic(e.to_string()))?;
+            let op_return_data = BASE64_STANDARD.decode(base64_data)?;
             tx_builder.add_data(
                 &PushBytesBuf::try_from(op_return_data)
                     .map_err(|e| Error::Generic(e.to_string()))?,
index 13955374e5188bc93505a9835f749842d3b0211d..918a44731fa18a4429bdba04162b4cd4f47ef4c9 100644 (file)
@@ -288,7 +288,7 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for CreateTxCommand {
         }
 
         if let Some(utxos) = &self.utxos {
-            tx_builder.add_utxos(&utxos[..]).unwrap();
+            tx_builder.add_utxos(&utxos[..])?;
         }
 
         if let Some(unspendable) = &self.unspendable {
@@ -296,10 +296,14 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for CreateTxCommand {
         }
 
         if let Some(base64_data) = &self.add_data {
-            let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap();
-            tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap());
+            let op_return_data = BASE64_STANDARD.decode(base64_data)?;
+            tx_builder.add_data(
+                &PushBytesBuf::try_from(op_return_data)
+                    .map_err(|e| Error::Generic(e.to_string()))?,
+            );
         } else if let Some(string_data) = &self.add_string {
-            let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap();
+            let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec())
+                .map_err(|e| Error::Generic(e.to_string()))?;
             tx_builder.add_data(&data);
         }
 
@@ -319,8 +323,6 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for CreateTxCommand {
 
         let psbt = tx_builder.finish()?;
 
-        // let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize());
-
         Ok(PsbtResult::new(&psbt, Some(false)))
     }
 }
@@ -443,9 +445,7 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for CreateSpTxCommand {
         }
 
         if let Some(utxos) = &self.utxos {
-            tx_builder
-                .add_utxos(&utxos[..])
-                .map_err(|_| bdk_wallet::error::CreateTxError::UnknownUtxo)?;
+            tx_builder.add_utxos(&utxos[..])?;
         }
 
         if let Some(unspendable) = &self.unspendable {
@@ -453,9 +453,7 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for CreateSpTxCommand {
         }
 
         if let Some(base64_data) = &self.add_data {
-            let op_return_data = BASE64_STANDARD
-                .decode(base64_data)
-                .map_err(|e| Error::Generic(e.to_string()))?;
+            let op_return_data = BASE64_STANDARD.decode(base64_data)?;
             tx_builder.add_data(
                 &PushBytesBuf::try_from(op_return_data)
                     .map_err(|e| Error::Generic(e.to_string()))?,
@@ -599,7 +597,7 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for BumpFeeCommand {
         }
 
         if let Some(utxos) = &self.utxos {
-            tx_builder.add_utxos(&utxos[..]).unwrap();
+            tx_builder.add_utxos(&utxos[..])?;
         }
 
         if let Some(unspendable) = &self.unspendable {
index 27b6bd160c8d11861cf51db3372517f28d8d3dc4..3bb0f891a2d2b2f894e99198e322e60d2ed47026 100644 (file)
@@ -7,6 +7,8 @@ mod test_offline {
     use tempfile::TempDir;
 
     static WALLET_NAME: &str = "test_config_wallet";
+    /// A `--to` argument, the wallet is unfunded, so nothing is spent.
+    static RECIPIENT: &str = "tb1p4tp4l6glyr2gs94neqcpr5gha7344nfyznfkc8szkreflscsdkgqsdent4:10000";
 
     /// Helper to spin up a sandboxed CLI with the generated descriptors
     fn setup_wallet_config() -> (BdkCli, Command) {
@@ -151,6 +153,42 @@ mod test_offline {
         .stderr(predicate::str::contains("Invalid"));
     }
 
+    /// A malformed `create_tx` argument must be reported as an error (exit 1),
+    /// never as a panic (exit 101).
+    #[test]
+    fn test_create_tx_rejects_malformed_input_without_panicking() {
+        let (cli, mut cmd_init) = setup_wallet_config();
+        cmd_init.assert().success();
+
+        // Unknown outpoint
+        cli.wallet_cmd(&[
+            "--wallet",
+            WALLET_NAME,
+            "create_tx",
+            "--to",
+            RECIPIENT,
+            "--utxos",
+            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0",
+        ])
+        .assert()
+        .code(1)
+        .stderr(predicate::str::contains("UTXO not found"));
+
+        // Malformed base64
+        cli.wallet_cmd(&[
+            "--wallet",
+            WALLET_NAME,
+            "create_tx",
+            "--to",
+            RECIPIENT,
+            "--add_data",
+            "!!!not-base64!!!",
+        ])
+        .assert()
+        .code(1)
+        .stderr(predicate::str::contains("Base64 decoding error"));
+    }
+
     #[cfg(feature = "message_signer")]
     #[test]
     fn test_sign_message_and_verify_message() {