#[cfg(feature = "rpc")]
mod test_offline {
- use super::*;
use crate::common::BdkCli;
use assert_cmd::Command;
use predicates::prelude::*;
.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}"
+ );
+ }
}
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();