use std::error::Error;
use std::collections::HashMap;
use std::env;
use std::time::Duration;
use bitcoin::Network;
use tokio::time::sleep;
use crate::blockchain::{
BlockchainAdapter, NodePort, WalletPort, SmartContractPort, MetricsPort, SecurityPort,
AlertComparison, TransactionParams, TxInput,
};
use crate::bitcoin::rpc::BitcoinRpcClient;
use crate::blockchain::bitcoin::adapter::{BitcoinAdapter, BitcoinAdapterConfig};
async fn setup_testnet_adapter() -> Option<BitcoinAdapter> -> Result<(), Box<dyn Error>> {
let rpc_url = env::var("BITCOIN_TESTNET_RPC_URL").unwrap_or_else(|_| "http://localhost:18332".to_string());
let rpc_user = env::var("BITCOIN_TESTNET_RPC_USER").unwrap_or_else(|_| "bitcoin".to_string());
let rpc_password = env::var("BITCOIN_TESTNET_RPC_PASSWORD").unwrap_or_else(|_| "password".to_string());
let config = BitcoinAdapterConfig {
network: Network::Testnet,
rpc_url,
rpc_user,
rpc_password,
timeout: 10,
metrics_interval: 5,
security_interval: 5,
mempool_interval: 5,
fee_estimation_blocks: vec![1, 6, 144],
enable_security_monitoring: true,
chain_split_threshold: 3,
fee_spike_threshold: 2.0,
max_utxo_cache_size: 100,
max_block_cache_size: 10,
max_tx_cache_size: 100,
};
match BitcoinAdapter::new(config).await {
Ok(adapter) => Some(adapter),
Err(e) => {
eprintln!("Failed to connect to Bitcoin testnet node: {}", e);
eprintln!("Skipping tests that require a testnet node");
None
}
}
}
fn should_skip_test(adapter: &Option<BitcoinAdapter>) -> bool -> Result<(), Box<dyn Error>> {
if adapter.is_none() {
eprintln!("Skipping test as no testnet node is available");
return true;
}
false
}
#[tokio::test]
#[ignore] async fn test_adapter_initialization() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let result = adapter.initialize().await;
assert!(result.is_ok(), "Failed to initialize adapter: {:?}", result);
let chain_id = adapter.get_chain_id();
assert_eq!(chain_id, "bitcoin-testnet", "Chain ID should be bitcoin-testnet");
let state = adapter.get_blockchain_state().await;
assert!(state.is_ok(), "Failed to get blockchain state: {:?}", state);
let state = state?;
assert!(state.best_block_height > 0, "Block height should be greater than 0");
println!("Adapter initialized with state: {:?}", state);
}
#[tokio::test]
#[ignore]
async fn test_node_port() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let _ = adapter.initialize().await?;
let state = adapter.get_blockchain_state().await?;
println!("Current block height: {}", state.best_block_height);
let height = state.best_block_height - 5; let block = adapter.get_block_by_height(height).await?;
println!("Block at height {}: {}", height, block.hash);
let block_by_hash = adapter.get_block_by_hash(&block.hash).await?;
assert_eq!(block.height, block_by_hash.height, "Blocks should have the same height");
let raw_block = adapter.get_raw_block(&block.hash).await?;
assert!(!raw_block.is_empty(), "Raw block should not be empty");
if block.tx_count > 0 {
let transactions = adapter.get_block_by_hash(&block.hash).await?.tx_ids;
if let Some(txid) = transactions.get(0) {
let tx = adapter.get_transaction(txid).await?;
println!("Transaction {}: size={}, vsize={}", txid, tx.size, tx.vsize);
let raw_tx = adapter.get_raw_transaction(txid).await?;
assert!(!raw_tx.is_empty(), "Raw transaction should not be empty");
}
}
let mempool = adapter.get_mempool_status().await?;
println!("Mempool: {} transactions, {} bytes", mempool.tx_count, mempool.size);
let mempool_txs = adapter.get_mempool_transactions().await?;
println!("Mempool has {} transactions", mempool_txs.len());
let fee = adapter.estimate_fee(6).await?;
println!("Estimated fee for 6 blocks: {} sat/byte", fee);
let peers = adapter.get_peer_info().await?;
println!("Connected to {} peers", peers.len());
assert!(!peers.is_empty(), "Should be connected to at least one peer");
let hashrate = adapter.get_network_hashrate().await?;
println!("Network hashrate: {} hashes/sec", hashrate);
assert!(hashrate > 0.0, "Network hashrate should be greater than 0");
}
#[tokio::test]
#[ignore]
async fn test_wallet_port_readonly() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let _ = adapter.initialize().await?;
let state = adapter.get_blockchain_state().await?;
let height = state.best_block_height - 5;
let block = adapter.get_block_by_height(height).await?;
if let Some(txid) = block.tx_ids.get(0) {
let analysis = adapter.analyze_transaction(&txid).await?;
println!("Transaction analysis: {} inputs, {} outputs", analysis.inputs.len(), analysis.outputs.len());
for output in &analysis.outputs {
if let Some(address) = &output.address {
let balance = adapter.get_address_balance(address).await?;
println!("Address {} balance: {} confirmed, {} unconfirmed",
address, balance.confirmed, balance.unconfirmed);
let txs = adapter.get_address_transactions(address, Some(10)).await?;
println!("Address {} has {} transactions", address, txs.len());
let utxos = adapter.get_address_utxos(address).await?;
println!("Address {} has {} UTXOs", address, utxos.len());
break;
}
}
let inputs = vec![
TxInput {
txid: txid.clone(),
vout: 0,
sequence: None,
}
];
let mut outputs = HashMap::new();
outputs.insert("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx".to_string(), 0.0001);
let result = adapter.create_raw_transaction(inputs, outputs).await;
println!("Create raw transaction result: {:?}", result);
}
}
#[tokio::test]
#[ignore]
async fn test_smart_contract_port() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let _ = adapter.initialize().await?;
let script_hex = "5121030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000020252ae";
let result = adapter.deploy_contract(script_hex, "", &["0.0001".to_string()]).await;
println!("Deploy contract result: {:?}", result);
let state = adapter.get_blockchain_state().await?;
let p2sh_address = "2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc";
let result = adapter.call_contract(p2sh_address, "", "", &[]).await;
println!("Call contract result: {:?}", result);
let result = adapter.get_contract_balance(p2sh_address).await;
println!("Contract balance result: {:?}", result);
let result = adapter.get_contract_events(
p2sh_address, "", "all",
Some(state.best_block_height - 100),
Some(state.best_block_height)
).await;
println!("Contract events result: {:?}", result);
}
#[tokio::test]
#[ignore]
async fn test_metrics_port() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let _ = adapter.initialize().await?;
let metrics = adapter.get_metrics().await?;
println!("Block count: {}, TX count: {}", metrics.block_count, metrics.tx_count);
assert!(metrics.block_count > 0, "Block count should be greater than 0");
let hashrate = adapter.get_network_hashrate().await?;
println!("Network hashrate: {} hashes/sec", hashrate);
let mempool_size = adapter.get_mempool_size().await?;
println!("Mempool size: {} transactions", mempool_size);
let fee_estimates = adapter.get_fee_estimates().await?;
println!("Fee estimates: {:?}", fee_estimates);
assert!(!fee_estimates.is_empty(), "Should have at least one fee estimate");
let version = adapter.get_node_version().await?;
println!("Node version: {}", version);
assert!(!version.is_empty(), "Node version should not be empty");
let volume = adapter.get_transaction_volume(10).await?;
println!("Transaction volume for last 10 blocks: {} satoshis", volume);
let avg_block_time = adapter.get_block_time_average(10).await?;
println!("Average block time for last 10 blocks: {} seconds", avg_block_time);
assert!(avg_block_time > 0.0, "Average block time should be greater than 0");
let difficulty = adapter.get_difficulty().await?;
println!("Current difficulty: {}", difficulty);
assert!(difficulty > 0.0, "Difficulty should be greater than 0");
let histogram = adapter.get_mempool_fee_histogram().await?;
println!("Mempool fee histogram has {} data points", histogram.len());
}
#[tokio::test]
#[ignore]
async fn test_security_port() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let _ = adapter.initialize().await?;
let chain_split = adapter.check_chain_split().await?;
println!("Chain split detection result: {:?}", chain_split);
let state = adapter.get_blockchain_state().await?;
let height = state.best_block_height - 5;
let block = adapter.get_block_by_height(height).await?;
if let Some(txid) = block.tx_ids.get(0) {
let double_spend = adapter.detect_double_spend(txid, 1).await?;
println!("Double spend detection result: {:?}", double_spend);
let malleability = adapter.check_transaction_malleability(txid).await?;
println!("Transaction malleability check result: {:?}", malleability);
}
let anomalies = adapter.detect_anomalous_fees().await?;
println!("Detected {} fee anomalies", anomalies.len());
let large_txs = adapter.monitor_large_transactions(1.0).await?;
println!("Detected {} large transactions (>1 BTC)", large_txs.len());
let reorg = adapter.check_reorg_depth(height).await?;
println!("Reorg depth check result: {:?}", reorg);
}
#[tokio::test]
#[ignore]
async fn test_monitoring() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let _ = adapter.initialize().await?;
let result = adapter.start_monitoring().await;
assert!(result.is_ok(), "Failed to start monitoring: {:?}", result);
println!("Waiting for monitoring to collect data...");
sleep(Duration::from_secs(10)).await;
let unusual_txs = adapter.get_unusual_transactions().await?;
println!("Detected {} unusual transactions", unusual_txs.len());
let alerts = adapter.get_security_alerts().await?;
println!("Detected {} security alerts", alerts.len());
let alert = AlertComparison {
field: "block_height".to_string(),
comparison: "gt".to_string(),
value: "1000".to_string(), extra: None,
};
let alert_result = adapter.compare_with_alert(&alert).await?;
assert!(alert_result, "Block height should be greater than 1000 on testnet");
let result = adapter.stop_monitoring().await;
assert!(result.is_ok(), "Failed to stop monitoring: {:?}", result);
}
#[tokio::test]
#[ignore]
async fn test_transaction_creation_readonly() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let _ = adapter.initialize().await?;
let params = TransactionParams {
inputs: None, outputs: HashMap::from([
("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx".to_string(), 0.0001)
]),
fee_rate: Some(5.0), change_address: None,
op_return_data: Some(vec![1, 2, 3, 4, 5]),
rbf: Some(true),
locktime: None,
};
let result = adapter.create_transaction(params).await;
println!("Create transaction result: {:?}", result);
}
#[tokio::test]
#[ignore]
async fn test_integration_workflow() -> Result<(), Box<dyn Error>> {
let adapter = setup_testnet_adapter().await;
if should_skip_test(&adapter) {
return;
}
let adapter = adapter?;
let _ = adapter.initialize().await?;
let state = adapter.get_blockchain_state().await?;
println!("Current block height: {}", state.best_block_height);
let height = state.best_block_height - 3;
let block = adapter.get_block_by_height(height).await?;
println!("Block at height {}: hash={}, tx_count={}", height, block.hash, block.tx_count);
if let Some(txid) = block.tx_ids.get(0) {
let tx = adapter.get_transaction(txid).await?;
println!("Transaction {}: size={}, vsize={}", txid, tx.size, tx.vsize);
let analysis = adapter.analyze_transaction(txid).await?;
println!("Transaction has {} inputs and {} outputs", analysis.inputs.len(), analysis.outputs.len());
let in_mempool = adapter.is_in_mempool(txid).await?;
assert!(!in_mempool, "Confirmed transaction should not be in mempool");
let double_spend = adapter.detect_double_spend(txid, 3).await?;
assert!(double_spend.is_none(), "Confirmed transaction should not have double spends");
let malleability = adapter.check_transaction_malleability(txid).await?;
println!("Malleability check: {:?}", malleability);
for (i, output) in analysis.outputs.iter().enumerate() {
if let Some(address) = &output.address {
let balance = adapter.get_address_balance(address).await?;
println!("Output #{} to {}: {} confirmed, {} unconfirmed",
i, address, balance.confirmed, balance.unconfirmed);
let utxo = adapter.get_utxo(txid, i as u32).await?;
if let Some(utxo_info) = utxo {
println!("UTXO: txid={}, vout={}, amount={}",
utxo_info.txid, utxo_info.vout, utxo_info.amount);
} else {
println!("UTXO was spent or doesn't exist");
}
break;
}
}
}
let fee_estimates = adapter.get_fee_estimates().await?;
println!("Fee estimates: {:?}", fee_estimates);
let metrics = adapter.get_metrics().await?;
println!("Metrics: block_count={}, tx_count={}, difficulty={}",
metrics.block_count, metrics.tx_count, metrics.difficulty);
let chain_split = adapter.check_chain_split().await?;
println!("Chain split detection: {:?}", chain_split);
}