1use crate::error::BitcoinError;
26use base64::Engine;
27use serde::{Deserialize, Serialize};
28use std::time::Duration;
29
30#[derive(Debug, Clone)]
32pub struct AdvancedRpcConfig {
33 pub rpc_url: String,
35 pub rpc_user: String,
37 pub rpc_pass: String,
39 pub timeout_secs: u64,
41}
42
43impl Default for AdvancedRpcConfig {
44 fn default() -> Self {
45 Self {
46 rpc_url: "http://localhost:8332".to_string(),
47 rpc_user: "rpcuser".to_string(),
48 rpc_pass: "rpcpassword".to_string(),
49 timeout_secs: 30,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct DescriptorImportRequest {
59 pub descriptor: String,
61 pub timestamp: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub range: Option<[u32; 2]>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub label: Option<String>,
69 pub watch_only: bool,
71 pub active: bool,
73}
74
75impl Default for DescriptorImportRequest {
76 fn default() -> Self {
77 Self {
78 descriptor: String::new(),
79 timestamp: "now".to_string(),
80 range: None,
81 label: None,
82 watch_only: true,
83 active: false,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct DescriptorInfo {
91 pub descriptor: String,
93 pub checksum: String,
95 pub is_range: bool,
97 pub is_solvable: bool,
99 pub has_private_keys: bool,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct BlockTemplate {
108 pub version: u32,
110 #[serde(rename = "previousblockhash")]
112 pub previous_block_hash: String,
113 #[serde(default)]
115 pub transactions: Vec<String>,
116 #[serde(rename = "coinbaseaux")]
118 pub coinbase_aux: serde_json::Value,
119 pub target: String,
121 #[serde(rename = "mintime")]
123 pub min_time: u64,
124 pub bits: String,
126 pub height: u32,
128 #[serde(rename = "default_witness_commitment")]
130 pub default_witness_commitment: Option<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct NetworkPeerInfo {
136 pub id: u64,
138 pub addr: String,
140 pub version: u64,
142 pub subver: String,
144 pub inbound: bool,
146 pub connection_type: String,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct AddNodeResult {
153 pub node: String,
155 pub command: String,
157}
158
159#[derive(Debug, Clone)]
161pub struct SendMessageResult {
162 pub peer_id: u64,
164 pub message_type: String,
166 pub success: bool,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct PrioritisedTransaction {
173 pub txid: String,
175 pub fee_delta: i64,
177 pub priority: f64,
179}
180
181#[derive(Debug, Serialize)]
183struct RpcRequest<'a> {
184 jsonrpc: &'static str,
185 id: &'static str,
186 method: &'a str,
187 params: &'a serde_json::Value,
188}
189
190#[derive(Debug, Deserialize)]
192struct RpcResponse {
193 result: Option<serde_json::Value>,
194 error: Option<RpcError>,
195 #[allow(dead_code)]
196 id: Option<serde_json::Value>,
197}
198
199#[derive(Debug, Deserialize)]
201struct RpcError {
202 code: i64,
203 message: String,
204}
205
206#[derive(Debug)]
211pub struct AdvancedRpcClient {
212 pub config: AdvancedRpcConfig,
214 client: reqwest::Client,
216}
217
218impl AdvancedRpcClient {
219 pub fn new(config: AdvancedRpcConfig) -> Self {
221 let client = reqwest::Client::builder()
222 .timeout(Duration::from_secs(config.timeout_secs))
223 .build()
224 .unwrap_or_else(|_| reqwest::Client::new());
225 Self { config, client }
226 }
227
228 pub async fn import_descriptors(
232 &self,
233 requests: Vec<DescriptorImportRequest>,
234 ) -> Result<Vec<serde_json::Value>, BitcoinError> {
235 let params = serde_json::json!([requests]);
236 let result = self.rpc_call("importdescriptors", params).await?;
237 result
238 .as_array()
239 .ok_or_else(|| {
240 BitcoinError::RpcError("importdescriptors: expected array response".to_string())
241 })
242 .map(|arr| arr.to_vec())
243 }
244
245 pub async fn list_descriptors(
249 &self,
250 private: bool,
251 ) -> Result<Vec<DescriptorInfo>, BitcoinError> {
252 let params = serde_json::json!([private]);
253 let result = self.rpc_call("listdescriptors", params).await?;
254 let descriptors = result
256 .get("descriptors")
257 .or(result.as_array().map(|_| &result))
258 .ok_or_else(|| {
259 BitcoinError::RpcError("listdescriptors: missing 'descriptors' field".to_string())
260 })?;
261 serde_json::from_value::<Vec<DescriptorInfo>>(descriptors.clone())
262 .map_err(|e| BitcoinError::RpcError(format!("listdescriptors parse error: {}", e)))
263 }
264
265 pub async fn get_block_template(&self) -> Result<BlockTemplate, BitcoinError> {
269 let params = serde_json::json!([{"rules": ["segwit"]}]);
270 let result = self.rpc_call("getblocktemplate", params).await?;
271 serde_json::from_value::<BlockTemplate>(result)
272 .map_err(|e| BitcoinError::RpcError(format!("getblocktemplate parse error: {}", e)))
273 }
274
275 pub async fn submit_block(&self, hex_data: &str) -> Result<Option<String>, BitcoinError> {
279 let params = serde_json::json!([hex_data]);
280 let result = self.rpc_call("submitblock", params).await?;
281 if result.is_null() {
283 Ok(None)
284 } else {
285 Ok(result.as_str().map(|s| s.to_string()))
286 }
287 }
288
289 pub async fn prioritise_transaction(
293 &self,
294 txid: &str,
295 fee_delta: i64,
296 ) -> Result<bool, BitcoinError> {
297 let params = serde_json::json!([txid, 0, fee_delta]);
299 let result = self.rpc_call("prioritisetransaction", params).await?;
300 result.as_bool().ok_or_else(|| {
301 BitcoinError::RpcError("prioritisetransaction: expected boolean response".to_string())
302 })
303 }
304
305 pub async fn get_mempool_entry(&self, txid: &str) -> Result<serde_json::Value, BitcoinError> {
309 let params = serde_json::json!([txid]);
310 self.rpc_call("getmempoolentry", params).await
311 }
312
313 pub async fn get_block_height(&self) -> Result<u32, BitcoinError> {
317 let params = serde_json::json!([]);
318 let result = self.rpc_call("getblockcount", params).await?;
319 result.as_u64().map(|h| h as u32).ok_or_else(|| {
320 BitcoinError::RpcError("getblockcount: expected integer response".to_string())
321 })
322 }
323
324 pub async fn get_peer_info(&self) -> Result<Vec<NetworkPeerInfo>, BitcoinError> {
328 let params = serde_json::json!([]);
329 let result = self.rpc_call("getpeerinfo", params).await?;
330 serde_json::from_value::<Vec<NetworkPeerInfo>>(result)
331 .map_err(|e| BitcoinError::RpcError(format!("getpeerinfo parse error: {}", e)))
332 }
333
334 pub async fn add_node(&self, node: &str, command: &str) -> Result<(), BitcoinError> {
339 let params = serde_json::json!([node, command]);
340 self.rpc_call("addnode", params).await?;
341 Ok(())
342 }
343
344 pub async fn disconnect_node(&self, node: &str) -> Result<(), BitcoinError> {
348 let params = serde_json::json!([node]);
349 self.rpc_call("disconnectnode", params).await?;
350 Ok(())
351 }
352
353 pub async fn send_raw_message(
363 &self,
364 peer_id: u64,
365 message_type: &str,
366 data: Option<&str>,
367 ) -> Result<SendMessageResult, BitcoinError> {
368 if message_type == "ping" {
370 let params = serde_json::json!([]);
371 self.rpc_call("ping", params).await?;
372 } else {
373 let _data = data;
375 }
376 Ok(SendMessageResult {
377 peer_id,
378 message_type: message_type.to_string(),
379 success: true,
380 })
381 }
382
383 pub async fn get_net_totals(&self) -> Result<serde_json::Value, BitcoinError> {
387 let params = serde_json::json!([]);
388 self.rpc_call("getnettotals", params).await
389 }
390
391 async fn rpc_call(
396 &self,
397 method: &str,
398 params: serde_json::Value,
399 ) -> Result<serde_json::Value, BitcoinError> {
400 let credentials = base64::engine::general_purpose::STANDARD
401 .encode(format!("{}:{}", self.config.rpc_user, self.config.rpc_pass));
402
403 let body = RpcRequest {
404 jsonrpc: "1.0",
405 id: "kaccy",
406 method,
407 params: ¶ms,
408 };
409
410 let response = self
411 .client
412 .post(&self.config.rpc_url)
413 .header("Authorization", format!("Basic {}", credentials))
414 .header("Content-Type", "application/json")
415 .json(&body)
416 .send()
417 .await
418 .map_err(|e| BitcoinError::ConnectionFailed(format!("HTTP request failed: {}", e)))?;
419
420 let status = response.status();
421 let text = response
422 .text()
423 .await
424 .map_err(|e| BitcoinError::RpcError(format!("Failed to read response body: {}", e)))?;
425
426 if !status.is_success() && status.as_u16() != 500 {
427 return Err(BitcoinError::ConnectionFailed(format!(
428 "HTTP {}: {}",
429 status, text
430 )));
431 }
432
433 let rpc_response: RpcResponse = serde_json::from_str(&text).map_err(|e| {
434 BitcoinError::RpcError(format!("Failed to parse JSON-RPC response: {}", e))
435 })?;
436
437 if let Some(err) = rpc_response.error {
438 return Err(BitcoinError::RpcError(format!(
439 "RPC error {}: {}",
440 err.code, err.message
441 )));
442 }
443
444 rpc_response.result.ok_or_else(|| {
445 BitcoinError::RpcError("JSON-RPC response missing 'result' field".to_string())
446 })
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn test_config_default() {
456 let config = AdvancedRpcConfig::default();
457 assert_eq!(config.rpc_url, "http://localhost:8332");
458 assert_eq!(config.rpc_user, "rpcuser");
459 assert_eq!(config.rpc_pass, "rpcpassword");
460 assert_eq!(config.timeout_secs, 30);
461 }
462
463 #[test]
464 fn test_descriptor_import_request_default() {
465 let req = DescriptorImportRequest::default();
466 assert_eq!(req.timestamp, "now");
467 assert!(req.watch_only);
468 assert!(!req.active);
469 assert!(req.range.is_none());
470 assert!(req.label.is_none());
471 }
472
473 #[test]
474 fn test_descriptor_import_request_serde_roundtrip() {
475 let req = DescriptorImportRequest {
476 descriptor:
477 "wpkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)#7w87s3yd"
478 .to_string(),
479 timestamp: "0".to_string(),
480 range: Some([0, 100]),
481 label: Some("test".to_string()),
482 watch_only: true,
483 active: false,
484 };
485 let json = serde_json::to_string(&req).expect("serialize");
486 let back: DescriptorImportRequest = serde_json::from_str(&json).expect("deserialize");
487 assert_eq!(req.descriptor, back.descriptor);
488 assert_eq!(req.timestamp, back.timestamp);
489 assert_eq!(req.range, back.range);
490 assert_eq!(req.label, back.label);
491 assert_eq!(req.watch_only, back.watch_only);
492 assert_eq!(req.active, back.active);
493 }
494
495 #[test]
496 fn test_block_template_deserialization() {
497 let json = serde_json::json!({
498 "version": 536870912u32,
499 "previousblockhash": "000000000000000000028fa0b9a89a72d1c52b3f4b25f0ec6b8b4d39d0e7f3d1",
500 "transactions": [],
501 "coinbaseaux": {"flags": ""},
502 "target": "0000000000000000000512a8000000000000000000000000000000000000000000",
503 "mintime": 1700000000u64,
504 "bits": "1709caa9",
505 "height": 823456u32,
506 "default_witness_commitment": "6a24aa21a9ed..."
507 });
508 let bt: BlockTemplate = serde_json::from_value(json).expect("deserialize BlockTemplate");
509 assert_eq!(bt.version, 536870912);
510 assert_eq!(bt.height, 823456);
511 assert_eq!(bt.bits, "1709caa9");
512 assert!(bt.default_witness_commitment.is_some());
513 assert!(bt.transactions.is_empty());
514 }
515
516 #[test]
517 fn test_prioritised_transaction() {
518 let pt = PrioritisedTransaction {
519 txid: "abc123def456abc123def456abc123def456abc123def456abc123def456abc123".to_string(),
520 fee_delta: 1000,
521 priority: 42.5,
522 };
523 assert_eq!(pt.fee_delta, 1000);
524 assert!((pt.priority - 42.5).abs() < f64::EPSILON);
525 }
526
527 #[test]
528 fn test_rpc_client_creation() {
529 let config = AdvancedRpcConfig::default();
530 let client = AdvancedRpcClient::new(config);
531 assert_eq!(client.config.timeout_secs, 30);
532 }
533
534 #[test]
535 fn test_descriptor_info_serde() {
536 let info = DescriptorInfo {
537 descriptor: "wpkh([d34db33f/44h/0h/0h]03aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/*')#abcdef01".to_string(),
538 checksum: "abcdef01".to_string(),
539 is_range: true,
540 is_solvable: true,
541 has_private_keys: false,
542 };
543 let json = serde_json::to_string(&info).expect("serialize");
544 let back: DescriptorInfo = serde_json::from_str(&json).expect("deserialize");
545 assert_eq!(info.checksum, back.checksum);
546 assert!(back.is_range);
547 assert!(back.is_solvable);
548 assert!(!back.has_private_keys);
549 }
550
551 #[test]
552 fn test_network_peer_info_serde() {
553 let peer = NetworkPeerInfo {
554 id: 7,
555 addr: "192.168.1.1:8333".to_string(),
556 version: 70015,
557 subver: "/Satoshi:24.0.1/".to_string(),
558 inbound: false,
559 connection_type: "outbound-full-relay".to_string(),
560 };
561 let json = serde_json::to_string(&peer).expect("serialize");
562 let back: NetworkPeerInfo = serde_json::from_str(&json).expect("deserialize");
563 assert_eq!(back.id, 7);
564 assert_eq!(back.addr, "192.168.1.1:8333");
565 assert_eq!(back.version, 70015);
566 assert!(!back.inbound);
567 assert_eq!(back.connection_type, "outbound-full-relay");
568 }
569
570 #[test]
571 fn test_send_message_result_fields() {
572 let result = SendMessageResult {
573 peer_id: 42,
574 message_type: "ping".to_string(),
575 success: true,
576 };
577 assert_eq!(result.peer_id, 42);
578 assert_eq!(result.message_type, "ping");
579 assert!(result.success);
580 }
581
582 #[test]
583 fn test_add_node_result_serde() {
584 let result = AddNodeResult {
585 node: "1.2.3.4:8333".to_string(),
586 command: "add".to_string(),
587 };
588 let json = serde_json::to_string(&result).expect("serialize");
589 let back: AddNodeResult = serde_json::from_str(&json).expect("deserialize");
590 assert_eq!(back.node, "1.2.3.4:8333");
591 assert_eq!(back.command, "add");
592 }
593
594 #[test]
595 fn test_client_has_custom_message_capability() {
596 let config = AdvancedRpcConfig {
598 rpc_url: "http://localhost:8332".to_string(),
599 rpc_user: "user".to_string(),
600 rpc_pass: "pass".to_string(),
601 timeout_secs: 10,
602 };
603 let client = AdvancedRpcClient::new(config);
604 assert_eq!(client.config.timeout_secs, 10);
606 }
607
608 #[test]
609 fn test_descriptor_import_request_no_range_in_json() {
610 let req = DescriptorImportRequest {
611 descriptor: "tr(key)".to_string(),
612 timestamp: "now".to_string(),
613 range: None,
614 label: None,
615 watch_only: false,
616 active: true,
617 };
618 let json = serde_json::to_value(&req).expect("serialize");
619 assert!(json.get("range").is_none());
621 assert!(json.get("label").is_none());
622 assert_eq!(json["active"], true);
623 assert_eq!(json["watch_only"], false);
624 }
625}