Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,21 @@ jobs:
- name: Build
working-directory: examples/${{ matrix.example-dir }}
run: cargo build
regtest-examples:
needs: prepare
name: Regtest CLI Examples
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ needs.prepare.outputs.rust_version }}
override: true
cache: true
- name: Run regtest integration tests
working-directory: examples/example_bitcoind_rpc_polling
run: cargo test --test regtest -- --nocapture
4 changes: 4 additions & 0 deletions examples/example_bitcoind_rpc_polling/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ bdk_chain = { path = "../../crates/chain", features = ["serde"] }
bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
example_cli = { path = "../example_cli" }
ctrlc = { version = "^2" }

[dev-dependencies]
bdk_testenv = { path = "../../crates/testenv", features = ["download"] }
tempfile = "3"
115 changes: 115 additions & 0 deletions examples/example_bitcoind_rpc_polling/tests/regtest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use bdk_chain::bitcoin::Amount;
use bdk_testenv::TestEnv;
use std::process::Command;
use std::str::FromStr;

const DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)";

fn run_cmd(
args: &[&str],
rpc_url: &str,
cookie: &str,
workdir: &std::path::Path,
) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_example_bitcoind_rpc_polling"))
.args(args)
.env("RPC_URL", rpc_url)
.env("RPC_COOKIE", cookie)
.env("DESCRIPTOR", DESCRIPTOR)
.current_dir(workdir)
.output()
.expect("failed to run example binary")
}

fn assert_cmd_success(out: &std::process::Output, label: &str) {
assert!(
out.status.success(),
"{} failed:\nstdout: {}\nstderr: {}",
label,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}

#[test]
fn test_sync_and_balance_regtest() {
let env = TestEnv::new().expect("failed to create testenv");
let tmp = tempfile::tempdir().expect("failed to create tempdir");

let rpc_url = format!("127.0.0.1:{}", env.bitcoind.params.rpc_socket.port());
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rpc_url is manually constructed from the port only. electrsd::corepc_node::Node already exposes rpc_url() (used elsewhere in the repo) which avoids mismatches (e.g., missing http:// scheme or non-local bind addresses). Prefer using env.bitcoind.rpc_url() here for the client URL.

Copilot uses AI. Check for mistakes.
let cookie = env
.bitcoind
.params
.cookie_file
.to_str()
.expect("cookie path is valid utf8");

// 1. Init wallet on regtest
let out = run_cmd(
&["init", "--network", "regtest"],
&rpc_url,
cookie,
tmp.path(),
);
assert_cmd_success(&out, "init");

// 2. Get next wallet address
let out = run_cmd(&["address", "next"], &rpc_url, cookie, tmp.path());
assert_cmd_success(&out, "address next");
let address_output = String::from_utf8_lossy(&out.stdout);
// Parse the address from output like: "[address @ 0] bcrt1q..."
let address_str = address_output
.split_whitespace()
.last()
.expect("address output should have at least one word");
println!("wallet address: {}", address_str);

// 3. Mine 101 blocks to make coinbase spendable
env.mine_blocks(101, None).expect("failed to mine blocks");

// 4. Send 0.05 BTC to our wallet address
let wallet_address = bdk_chain::bitcoin::Address::from_str(address_str)
.expect("valid address")
.assume_checked();
Comment on lines +71 to +73
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address::from_str(...).assume_checked() bypasses network validation. Since the test expects a regtest address, require the network (e.g., require_network(regtest)), so the test fails if the CLI ever prints an address for the wrong network.

Copilot uses AI. Check for mistakes.
env.send(&wallet_address, Amount::from_btc(0.05).unwrap())
.expect("failed to send to wallet");
Comment on lines +74 to +75
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Amount::from_btc(0.05) relies on float parsing and unwrap(). For deterministic tests, prefer using satoshis directly (e.g., Amount::from_sat(5_000_000)) and avoid the unwrap failure mode.

Copilot uses AI. Check for mistakes.

// 5. Sync - should see unconfirmed tx
let out = run_cmd(&["sync"], &rpc_url, cookie, tmp.path());
assert_cmd_success(&out, "sync (unconfirmed)");

// 6. Check unconfirmed balance is 0.05 BTC
let out = run_cmd(&["balance"], &rpc_url, cookie, tmp.path());
assert_cmd_success(&out, "balance (unconfirmed)");
let balance_str = String::from_utf8_lossy(&out.stdout);
println!("balance (unconfirmed):\n{}", balance_str);
assert!(
balance_str.contains("5000000"),
"expected 5000000 sats unconfirmed, got: {}",
balance_str
);
Comment on lines +86 to +90
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The balance assertion uses contains("5000000"), which can produce false positives (e.g., 50000000 also matches). Consider parsing the numeric field(s) from the balance output and asserting exact values (and ideally also that confirmed is 0 at this stage).

Copilot uses AI. Check for mistakes.

// 7. Mine 1 block to confirm the tx
env.mine_blocks(1, None)
.expect("failed to mine confirming block");

// 8. Sync again - should see confirmed tx
let out = run_cmd(&["sync"], &rpc_url, cookie, tmp.path());
assert_cmd_success(&out, "sync (confirmed)");

// 9. Check confirmed balance is 0.05 BTC (5_000_000 sats)
let out = run_cmd(&["balance"], &rpc_url, cookie, tmp.path());
assert_cmd_success(&out, "balance (confirmed)");
let balance_str = String::from_utf8_lossy(&out.stdout);
println!("balance (confirmed):\n{}", balance_str);
assert!(
balance_str.contains("5000000"),
"expected 5000000 sats confirmed, got: {}",
balance_str
);
Comment on lines +105 to +109
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: contains("5000000") can pass even if the amount is wrong but includes that substring. Parse and assert the exact confirmed/unconfirmed totals to make this test reliably detect regressions.

Copilot uses AI. Check for mistakes.

// 10. List txouts - should show our received utxo
let out = run_cmd(&["txout", "list"], &rpc_url, cookie, tmp.path());
assert_cmd_success(&out, "txout list");
println!("txout list:\n{}", String::from_utf8_lossy(&out.stdout));
Comment on lines +111 to +114
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The txout list step only prints output; it doesn't assert that the expected UTXO (value/address/outpoint) is present. Adding an assertion here would make the test actually verify the behavior described in the PR (listing the received output).

Copilot uses AI. Check for mistakes.
}
Loading