diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b0eed44e9..c6ff5951a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,6 +29,38 @@ jobs: - name: Run tests (Bitcoin mode, REST+Electrum) run: RUST_LOG=debug cargo test + test-bitcoin-28: + runs-on: ubuntu-22.04 + steps: + - run: sudo apt-get update && sudo apt-get install libfuse2 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.75.0 + - uses: Swatinem/rust-cache@v2 + - name: Download bitcoind 28.0 + run: | + curl -sSL https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz | tar -xz + chmod +x bitcoin-28.0/bin/bitcoind + - name: Run tests (Bitcoin 28.0, REST+Electrum) + run: RUST_LOG=debug cargo test --features bitcoind_28_0 + env: + BITCOIND_EXE: ${{ github.workspace }}/bitcoin-28.0/bin/bitcoind + + test-bitcoin-27: + runs-on: ubuntu-22.04 + steps: + - run: sudo apt-get update && sudo apt-get install libfuse2 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.75.0 + - uses: Swatinem/rust-cache@v2 + - name: Download bitcoind 27.0 + run: | + curl -sSL https://bitcoincore.org/bin/bitcoin-core-27.0/bitcoin-27.0-x86_64-linux-gnu.tar.gz | tar -xz + chmod +x bitcoin-27.0/bin/bitcoind + - name: Run tests (Bitcoin 27.0, REST+Electrum) + run: RUST_LOG=debug cargo test --features bitcoind_28_0 + env: + BITCOIND_EXE: ${{ github.workspace }}/bitcoin-28.0/bin/bitcoind + test-electrum-raw: runs-on: ubuntu-22.04 steps: diff --git a/Cargo.toml b/Cargo.toml index 6bf669a83..0ae50f3e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ otlp-tracing = [ "opentelemetry-semantic-conventions", "electrs_macros/otlp-tracing" ] +bitcoind_28_0 = [] [dependencies] arraydeque = "0.5.1" diff --git a/tests/rest.rs b/tests/rest.rs index 90502f8f6..382ad16fd 100644 --- a/tests/rest.rs +++ b/tests/rest.rs @@ -207,92 +207,88 @@ fn test_rest() -> Result<()> { let status = empty_package_resp.status(); assert_eq!(status, 400); - // Elements-only tests - #[cfg(not(feature = "liquid"))] + // bitcoin 28.0 only tests - submitpackage + #[cfg(all(not(feature = "liquid"), feature = "bitcoind_28_0"))] { - let network_info = tester.node_client().call::("getnetworkinfo", &[])?; - let version = network_info["version"].as_u64().expect("network version"); - if version >= 280000 { - // Test with a real transaction package - create parent-child transactions - // submitpackage requires between 2 and 25 transactions with proper dependencies - let package_addr1 = tester.newaddress()?; - let package_addr2 = tester.newaddress()?; - - // Create parent transaction - let tx1_result = tester.node_client().call::( - "createrawtransaction", - &[ - serde_json::json!([]), - serde_json::json!({package_addr1.to_string(): 0.5}), - ], - )?; - let tx1_unsigned_hex = tx1_result.as_str().expect("raw tx hex").to_string(); - - let tx1_fund_result = tester - .node_client() - .call::("fundrawtransaction", &[serde_json::json!(tx1_unsigned_hex)])?; - let tx1_funded_hex = tx1_fund_result["hex"] - .as_str() - .expect("funded tx hex") - .to_string(); - - let tx1_sign_result = tester.node_client().call::( - "signrawtransactionwithwallet", - &[serde_json::json!(tx1_funded_hex)], - )?; - let tx1_signed_hex = tx1_sign_result["hex"] - .as_str() - .expect("signed tx hex") - .to_string(); - - // Decode parent transaction to get its txid and find the output to spend - let tx1_decoded = tester - .node_client() - .call::("decoderawtransaction", &[serde_json::json!(tx1_signed_hex)])?; - let tx1_txid = tx1_decoded["txid"].as_str().expect("parent txid"); - - // Find the output going to package_addr1 (the one we want to spend) - let tx1_vouts = tx1_decoded["vout"].as_array().expect("parent vouts"); - let mut spend_vout_index = None; - let mut spend_vout_value = 0u64; - - for (i, vout) in tx1_vouts.iter().enumerate() { - if let Some(script_pub_key) = vout.get("scriptPubKey") { - if let Some(address) = script_pub_key.get("address") { - if address.as_str() == Some(&package_addr1.to_string()) { - spend_vout_index = Some(i); - // Convert from BTC to satoshis - spend_vout_value = (vout["value"].as_f64().expect("vout value") - * 100_000_000.0) - as u64; - break; - } + // Test with a real transaction package - create parent-child transactions + // submitpackage requires between 2 and 25 transactions with proper dependencies + let package_addr1 = tester.newaddress()?; + let package_addr2 = tester.newaddress()?; + + // Create parent transaction + let tx1_result = tester.node_client().call::( + "createrawtransaction", + &[ + serde_json::json!([]), + serde_json::json!({package_addr1.to_string(): 0.5}), + ], + )?; + let tx1_unsigned_hex = tx1_result.as_str().expect("raw tx hex").to_string(); + + let tx1_fund_result = tester + .node_client() + .call::("fundrawtransaction", &[serde_json::json!(tx1_unsigned_hex)])?; + let tx1_funded_hex = tx1_fund_result["hex"] + .as_str() + .expect("funded tx hex") + .to_string(); + + let tx1_sign_result = tester.node_client().call::( + "signrawtransactionwithwallet", + &[serde_json::json!(tx1_funded_hex)], + )?; + let tx1_signed_hex = tx1_sign_result["hex"] + .as_str() + .expect("signed tx hex") + .to_string(); + + // Decode parent transaction to get its txid and find the output to spend + let tx1_decoded = tester + .node_client() + .call::("decoderawtransaction", &[serde_json::json!(tx1_signed_hex)])?; + let tx1_txid = tx1_decoded["txid"].as_str().expect("parent txid"); + + // Find the output going to package_addr1 (the one we want to spend) + let tx1_vouts = tx1_decoded["vout"].as_array().expect("parent vouts"); + let mut spend_vout_index = None; + let mut spend_vout_value = 0u64; + + for (i, vout) in tx1_vouts.iter().enumerate() { + if let Some(script_pub_key) = vout.get("scriptPubKey") { + if let Some(address) = script_pub_key.get("address") { + if address.as_str() == Some(&package_addr1.to_string()) { + spend_vout_index = Some(i); + // Convert from BTC to satoshis + spend_vout_value = + (vout["value"].as_f64().expect("vout value") * 100_000_000.0) as u64; + break; } } } + } - let spend_vout_index = spend_vout_index.expect("Could not find output to spend"); - - // Create child transaction that spends from parent - // Leave some satoshis for fee (e.g., 1000 sats) - let child_output_value = spend_vout_value - 1000; - let child_output_btc = child_output_value as f64 / 100_000_000.0; - - let tx2_result = tester.node_client().call::( - "createrawtransaction", - &[ - serde_json::json!([{ - "txid": tx1_txid, - "vout": spend_vout_index - }]), - serde_json::json!({package_addr2.to_string(): child_output_btc}), - ], - )?; - let tx2_unsigned_hex = tx2_result.as_str().expect("raw tx hex").to_string(); - - // Sign the child transaction - // We need to provide the parent transaction's output details for signing - let tx2_sign_result = tester.node_client().call::( + let spend_vout_index = spend_vout_index.expect("Could not find output to spend"); + + // Create child transaction that spends from parent + // Leave some satoshis for fee (e.g., 1000 sats) + let child_output_value = spend_vout_value - 1000; + let child_output_btc = child_output_value as f64 / 100_000_000.0; + + let tx2_result = tester.node_client().call::( + "createrawtransaction", + &[ + serde_json::json!([{ + "txid": tx1_txid, + "vout": spend_vout_index + }]), + serde_json::json!({package_addr2.to_string(): child_output_btc}), + ], + )?; + let tx2_unsigned_hex = tx2_result.as_str().expect("raw tx hex").to_string(); + + // Sign the child transaction + // We need to provide the parent transaction's output details for signing + let tx2_sign_result = tester.node_client().call::( "signrawtransactionwithwallet", &[ serde_json::json!(tx2_unsigned_hex), @@ -304,50 +300,49 @@ fn test_rest() -> Result<()> { }]) ], )?; - let tx2_signed_hex = tx2_sign_result["hex"] - .as_str() - .expect("signed tx hex") - .to_string(); - - // Debug: try calling submitpackage directly to see the result - eprintln!("Trying submitpackage directly with parent-child transactions..."); - let direct_result = tester.node_client().call::( - "submitpackage", - &[serde_json::json!([ - tx1_signed_hex.clone(), - tx2_signed_hex.clone() - ])], - ); - match direct_result { - Ok(result) => { - eprintln!("Direct submitpackage succeeded: {:#?}", result); - } - Err(e) => { - eprintln!("Direct submitpackage failed: {:?}", e); - } + let tx2_signed_hex = tx2_sign_result["hex"] + .as_str() + .expect("signed tx hex") + .to_string(); + + // Debug: try calling submitpackage directly to see the result + eprintln!("Trying submitpackage directly with parent-child transactions..."); + let direct_result = tester.node_client().call::( + "submitpackage", + &[serde_json::json!([ + tx1_signed_hex.clone(), + tx2_signed_hex.clone() + ])], + ); + match direct_result { + Ok(result) => { + eprintln!("Direct submitpackage succeeded: {:#?}", result); + } + Err(e) => { + eprintln!("Direct submitpackage failed: {:?}", e); } + } - // Now submit this transaction package via the package endpoint - let package_json = - serde_json::json!([tx1_signed_hex.clone(), tx2_signed_hex.clone()]).to_string(); - let package_result = ureq::post(&format!("http://{}/txs/package", rest_addr)) - .set("Content-Type", "application/json") - .send_string(&package_json); + // Now submit this transaction package via the package endpoint + let package_json = + serde_json::json!([tx1_signed_hex.clone(), tx2_signed_hex.clone()]).to_string(); + let package_result = ureq::post(&format!("http://{}/txs/package", rest_addr)) + .set("Content-Type", "application/json") + .send_string(&package_json); - let package_resp = package_result.unwrap(); - assert_eq!(package_resp.status(), 200); - let package_result = package_resp.into_json::()?; + let package_resp = package_result.unwrap(); + assert_eq!(package_resp.status(), 200); + let package_result = package_resp.into_json::()?; - // Verify the response structure - assert!(package_result["tx-results"].is_object()); - assert!(package_result["package_msg"].is_string()); + // Verify the response structure + assert!(package_result["tx-results"].is_object()); + assert!(package_result["package_msg"].is_string()); - let tx_results = package_result["tx-results"].as_object().unwrap(); - assert_eq!(tx_results.len(), 2); + let tx_results = package_result["tx-results"].as_object().unwrap(); + assert_eq!(tx_results.len(), 2); - // The transactions should be processed (whether accepted or rejected) - assert!(!tx_results.is_empty()); - } + // The transactions should be processed (whether accepted or rejected) + assert!(!tx_results.is_empty()); } // Elements-only tests