]> Untitled Git - bdk-cli/commitdiff
test: add tests for repl, transactions & send_all
authorVihiga Tyonum <withtvpeter@gmail.com>
Mon, 6 Jul 2026 15:57:04 +0000 (15:57 +0000)
committerVihiga Tyonum <withtvpeter@gmail.com>
Wed, 8 Jul 2026 21:49:32 +0000 (22:49 +0100)
tests/integration/offline.rs
tests/integration/online.rs

index 9beb8ff068eee0afc1d65319ecba5c87c9693939..f19f623c0baf286112c8cd79dd174d3618289d84 100644 (file)
@@ -1,6 +1,5 @@
 #[cfg(feature = "rpc")]
 mod test_offline {
-    use super::*;
     use crate::common::BdkCli;
     use assert_cmd::Command;
     use predicates::prelude::*;
@@ -261,4 +260,92 @@ mod test_offline {
         .success()
         .stdout(predicate::str::contains("\"valid\": false"));
     }
+
+    #[test]
+    fn test_create_tx_send_all_rejects_multiple_recipients() {
+        let (cli, mut cmd_init) = setup_wallet_config();
+        cmd_init.assert().success();
+
+        // Same address twice => two recipients => must be rejected under --send_all.
+        let recipient = "tb1p4tp4l6glyr2gs94neqcpr5gha7344nfyznfkc8szkreflscsdkgqsdent4:0";
+        cli.wallet_cmd(&[
+            "--wallet",
+            WALLET_NAME,
+            "create_tx",
+            "--send_all",
+            "--to",
+            recipient,
+            "--to",
+            recipient,
+        ])
+        .assert()
+        .failure()
+        .stderr(predicate::str::contains(
+            "Wallet can only be drained to a single output",
+        ));
+    }
+}
+
+#[cfg(all(feature = "repl", feature = "electrum"))]
+mod repl_tests {
+    use crate::common::BdkCli;
+    use serde_json::Value;
+    use tempfile::TempDir;
+
+    const WALLET_NAME: &str = "repl_test_wallet";
+
+    fn setup_repl_wallet() -> (BdkCli, TempDir) {
+        let temp = TempDir::new().unwrap();
+        let cli = BdkCli::new("regtest", Some(temp.path().to_path_buf()));
+
+        let desc = cli.cmd("descriptor", &["--type", "tr"]).output().unwrap();
+        let v: Value = serde_json::from_slice(&desc.stdout).unwrap();
+        let ext = v["private_descriptors"]["external"].as_str().unwrap();
+        let int = v["private_descriptors"]["internal"].as_str().unwrap();
+
+        cli.build_base_cmd()
+            .args(["wallet", "--wallet", WALLET_NAME, "config"])
+            .args(["--ext-descriptor", ext, "--int-descriptor", int])
+            .args(["--client-type", "electrum", "--database-type", "sqlite"])
+            .args(["--url", "127.0.0.1:1"])
+            .assert()
+            .success();
+
+        (cli, temp)
+    }
+
+    #[test]
+    fn test_repl_executes_commands_and_exits() {
+        let (cli, _temp) = setup_repl_wallet();
+
+        let output = cli
+            .build_base_cmd()
+            .args(["repl", "--wallet", WALLET_NAME])
+            .write_stdin("wallet new_address\nwallet balance\nexit\n")
+            .output()
+            .expect("failed to run repl");
+
+        assert!(
+            output.status.success(),
+            "repl exited non-zero: {}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+        let stdout = String::from_utf8_lossy(&output.stdout);
+        assert!(
+            stdout.contains("Entering REPL mode"),
+            "missing banner:\n{stdout}"
+        );
+        assert!(
+            stdout.contains("\"address\":"),
+            "new_address output missing:\n{stdout}"
+        );
+        assert!(
+            stdout.contains("\"confirmed\":"),
+            "balance output missing:\n{stdout}"
+        );
+        assert!(
+            stdout.contains("Exiting..."),
+            "no exit acknowledgement:\n{stdout}"
+        );
+    }
 }
index 9474d76daf91d51b8a8dd8fad034cf85b4cf07da..bdeb39fceebd4c706eb5ee606b92092b4db18fdc 100644 (file)
@@ -178,6 +178,108 @@ mod test_online {
         Txid::from_str(txid).expect("broadcast: invalid txid")
     }
 
+    #[test]
+    fn test_funded_wallet_unspent_and_transactions() {
+        let (cli, mut cmd_init, env) = setup_online_wallet();
+        cmd_init.assert().success();
+        fund_and_sync_wallet(&cli, &env);
+
+        //  unspent: exactly one confirmed, unspent 50,000,000 sat UTXO
+        let unspent = run_wallet_json(&cli, &["unspent"]);
+        assert_eq!(
+            unspent["count"].as_u64(),
+            Some(1),
+            "funded wallet should report exactly one UTXO, got: {unspent}"
+        );
+
+        let utxo = &unspent["items"][0];
+        assert_eq!(
+            utxo["txout"]["value"].as_u64(),
+            Some(50_000_000),
+            "the single UTXO should equal the 0.5 BTC funding amount"
+        );
+        assert_eq!(
+            utxo["is_spent"].as_bool(),
+            Some(false),
+            "the funding UTXO must be unspent"
+        );
+        assert!(
+            utxo["outpoint"].as_str().is_some_and(|o| o.contains(':')),
+            "UTXO outpoint should be a `txid:vout` string, got: {}",
+            utxo["outpoint"]
+        );
+        assert!(
+            utxo["derivation_index"].is_number(),
+            "UTXO should expose a numeric derivation_index"
+        );
+
+        // transactions: exactly one relevant tx, funding our address
+        let txs = run_wallet_json(&cli, &["transactions"]);
+        assert_eq!(
+            txs["count"].as_u64(),
+            Some(1),
+            "funded wallet should report exactly one transaction, got: {txs}"
+        );
+
+        let tx = &txs["items"][0];
+        assert_eq!(
+            tx["is_coinbase"].as_bool(),
+            Some(false),
+            "the funding transaction is a normal send, not a coinbase"
+        );
+        assert!(
+            tx["txid"].as_str().is_some_and(|t| t.len() == 64),
+            "transaction should expose a 64-char hex txid, got: {}",
+            tx["txid"]
+        );
+        // The funding tx must contain the output that paid us 0.5 BTC.
+        let outputs = tx["outputs"]
+            .as_array()
+            .expect("transaction outputs should be a JSON array");
+        assert!(
+            outputs
+                .iter()
+                .any(|o| o["value"].as_u64() == Some(50_000_000)),
+            "funding tx should contain a 50,000,000 sat output to the wallet"
+        );
+    }
+
+    #[test]
+    fn test_create_tx_send_all_drains_wallet() {
+        let (cli, mut cmd_init, env) = setup_online_wallet();
+        cmd_init.assert().success();
+        fund_and_sync_wallet(&cli, &env);
+
+        let unsigned_psbt = run_wallet_json(
+            &cli,
+            &["create_tx", "--send_all", "--to", &format!("{RECIPIENT}:0")],
+        )["psbt"]
+            .as_str()
+            .expect("create_tx: missing 'psbt' field")
+            .to_string();
+
+        let (signed_psbt, finalized) = cli_sign(&cli, &unsigned_psbt);
+        assert!(finalized, "drain PSBT should be finalized after signing");
+
+        let raw_tx = cli_extract_psbt(&cli, &signed_psbt);
+        let drain_txid = cli_broadcast(&cli, &raw_tx);
+        env.rpc_client()
+            .get_mempool_entry(&drain_txid)
+            .expect("drain tx not found in node mempool");
+
+        env.mine_blocks(1, None)
+            .expect("Failed to mine drain confirmation block");
+        env.wait_until_electrum_sees_block(Duration::from_secs(10))
+            .expect("Electrum did not catch up to drain confirmation");
+
+        cli_sync(&cli);
+        assert_eq!(
+            cli_balance(&cli),
+            0,
+            "send_all should leave a zero confirmed balance"
+        );
+    }
+
     #[test]
     fn test_full_online_transaction_lifecycle() {
         let (cli, mut cmd_init, env) = setup_online_wallet();