use blvm_consensus::*;
use blvm_consensus::serialization::transaction::serialize_transaction;
use blvm_consensus::serialization::block::serialize_block_header;
use serde_json::json;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct NodeRpcConfig {
pub url: String,
pub username: Option<String>,
pub password: Option<String>,
}
impl Default for NodeRpcConfig {
fn default() -> Self {
Self {
url: std::env::var("BLVM_NODE_RPC_URL")
.unwrap_or_else(|_| "http://127.0.0.1:18332".to_string()),
username: std::env::var("BLVM_NODE_RPC_USER").ok(),
password: std::env::var("BLVM_NODE_RPC_PASS").ok(),
}
}
}
pub struct NodeRpcClient {
config: NodeRpcConfig,
}
impl NodeRpcClient {
pub fn new(config: NodeRpcConfig) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self { config })
}
pub async fn call(&self, method: &str, params: serde_json::Value) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
use reqwest::Client;
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
let url = reqwest::Url::parse(&self.config.url)?;
let rpc_request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params
});
let mut request = client.post(url.clone());
if let (Some(user), Some(pass)) = (&self.config.username, &self.config.password) {
request = request.basic_auth(user, Some(pass));
}
let response = request
.json(&rpc_request)
.send()
.await?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()).into());
}
let rpc_response: serde_json::Value = response.json().await?;
if let Some(error) = rpc_response.get("error") {
return Err(format!("RPC error: {}", error).into());
}
Ok(rpc_response.get("result").cloned().unwrap_or(json!(null)))
}
pub async fn test_mempool_accept(&self, tx_hex: &str) -> Result<MempoolAcceptResult, Box<dyn std::error::Error>> {
let result = self.call("testmempoolaccept", json!([[tx_hex]])).await?;
if let Some(array) = result.as_array() {
if let Some(first) = array.get(0) {
return Ok(MempoolAcceptResult {
allowed: first.get("allowed").and_then(|v| v.as_bool()).unwrap_or(false),
reject_reason: first.get("reject-reason")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
});
}
}
Err("Invalid testmempoolaccept response".into())
}
pub async fn submit_block(&self, block_hex: &str) -> Result<String, Box<dyn std::error::Error>> {
let result = self.call("submitblock", json!([block_hex])).await?;
Ok(result.as_str().unwrap_or("").to_string())
}
pub async fn get_blockchain_info(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
self.call("getblockchaininfo", json!([])).await
}
pub async fn get_block(&self, hash: &str, verbosity: Option<u64>) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
self.call("getblock", json!([hash, verbosity.unwrap_or(1)])).await
}
}
#[derive(Debug, Clone)]
pub struct MempoolAcceptResult {
pub allowed: bool,
pub reject_reason: Option<String>,
}
pub async fn compare_transaction_validation_via_rpc(
tx: &Transaction,
config: &NodeRpcConfig,
) -> Result<ComparisonResult, Box<dyn std::error::Error>> {
let local_result = check_transaction(tx)?;
let local_valid = matches!(local_result, ValidationResult::Valid);
let tx_hex = hex::encode(&serialize_transaction(tx));
let rpc_client = NodeRpcClient::new(config.clone())?;
let rpc_result = rpc_client.test_mempool_accept(&tx_hex).await;
let rpc_valid = match rpc_result {
Ok(result) => result.allowed,
Err(e) => {
eprintln!("RPC not available: {}", e);
return Err(e);
}
};
let divergence = local_valid != rpc_valid;
let divergence_reason = if divergence {
Some(format!(
"Local (direct): {}, RPC (blvm-node): {}",
if local_valid { "valid" } else { "invalid" },
if rpc_valid { "valid" } else { "invalid" }
))
} else {
None
};
Ok(ComparisonResult {
local_valid,
core_valid: rpc_valid, divergence,
divergence_reason,
})
}
#[derive(Debug, Clone)]
pub struct ComparisonResult {
pub local_valid: bool,
pub core_valid: bool, pub divergence: bool,
pub divergence_reason: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_node_rpc_config_default() {
let config = NodeRpcConfig::default();
assert_eq!(config.url, "http://127.0.0.1:18332");
}
#[tokio::test]
async fn test_node_rpc_client_creation() {
let config = NodeRpcConfig::default();
let client = NodeRpcClient::new(config);
assert!(client.is_ok());
}
#[tokio::test]
async fn test_transaction_validation_comparison() {
let tx = Transaction {
version: 1,
inputs: vec![].into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![0x51].into(), }].into(),
lock_time: 0,
};
let config = NodeRpcConfig::default();
let local_result = check_transaction(&tx);
assert!(local_result.is_ok());
let comparison = compare_transaction_validation_via_rpc(&tx, &config).await;
if let Ok(result) = comparison {
if !result.divergence {
} else {
panic!("Divergence between RPC and direct validation: {:?}", result.divergence_reason);
}
}
}
#[test]
fn test_mempool_accept_result_parsing() {
let result = MempoolAcceptResult {
allowed: true,
reject_reason: None,
};
assert!(result.allowed);
let rejected = MempoolAcceptResult {
allowed: false,
reject_reason: Some("bad-txns-inputs-missing".to_string()),
};
assert!(!rejected.allowed);
assert_eq!(rejected.reject_reason, Some("bad-txns-inputs-missing".to_string()));
}
}