1use crate::block_index::BlockIndex;
6use crate::fee_estimator::FeeEstimator;
7use crate::services::{BlockchainService, MempoolService, MiningService};
8use abtc_domain::script::Script;
9use abtc_ports::{ChainStateStore, RpcHandler};
10use async_trait::async_trait;
11use serde_json::{json, Value};
12use std::sync::Arc;
13use tokio::sync::RwLock;
14
15pub struct BlockchainRpcHandler {
17 blockchain: Arc<BlockchainService>,
18 mempool: Arc<MempoolService>,
19 fee_estimator: Arc<RwLock<FeeEstimator>>,
20 chain_state: Arc<dyn ChainStateStore>,
21 block_index: Arc<RwLock<BlockIndex>>,
22}
23
24impl BlockchainRpcHandler {
25 pub fn new(
27 blockchain: Arc<BlockchainService>,
28 mempool: Arc<MempoolService>,
29 fee_estimator: Arc<RwLock<FeeEstimator>>,
30 chain_state: Arc<dyn ChainStateStore>,
31 block_index: Arc<RwLock<BlockIndex>>,
32 ) -> Self {
33 BlockchainRpcHandler {
34 blockchain,
35 mempool,
36 fee_estimator,
37 chain_state,
38 block_index,
39 }
40 }
41}
42
43#[async_trait]
44impl RpcHandler for BlockchainRpcHandler {
45 async fn handle_request(
46 &self,
47 method: &str,
48 _params: &Value,
49 ) -> Result<Option<Value>, abtc_ports::RpcError> {
50 match method {
51 "getblockcount" => {
52 let info = self
53 .blockchain
54 .get_chain_info()
55 .await
56 .map_err(abtc_ports::RpcError::internal_error)?;
57 Ok(Some(Value::Number(info.height.into())))
58 }
59 "getbestblockhash" => {
60 let info = self
61 .blockchain
62 .get_chain_info()
63 .await
64 .map_err(abtc_ports::RpcError::internal_error)?;
65 Ok(Some(Value::String(info.best_block_hash.to_hex_reversed())))
66 }
67 "getblockchaininfo" => {
68 let info = self
69 .blockchain
70 .get_chain_info()
71 .await
72 .map_err(abtc_ports::RpcError::internal_error)?;
73 Ok(Some(json!({
74 "chain": "main",
75 "blocks": info.blocks,
76 "headers": info.blocks,
77 "bestblockhash": info.best_block_hash.to_hex_reversed(),
78 "difficulty": 1.0,
79 "mediantime": 0,
80 "verificationprogress": 1.0,
81 "initialblockdownload": false,
82 "chainwork": "0000000000000000000000000000000000000000000000000000000000000000",
83 "pruned": false
84 })))
85 }
86 "getmempoolinfo" => {
87 let mempool_info = self
88 .mempool
89 .get_mempool_info()
90 .await
91 .map_err(abtc_ports::RpcError::internal_error)?;
92 Ok(Some(json!({
93 "loaded": true,
94 "size": mempool_info.size,
95 "bytes": mempool_info.bytes,
96 "usage": mempool_info.usage,
97 "maxmempool": mempool_info.max_mempool,
98 "mempoolminfee": mempool_info.min_relay_fee,
99 "minrelaytxfee": mempool_info.min_relay_fee
100 })))
101 }
102 "getrawmempool" => {
103 let contents = self
104 .mempool
105 .get_mempool_contents()
106 .await
107 .map_err(abtc_ports::RpcError::internal_error)?;
108 Ok(Some(json!(contents)))
109 }
110 "getblockhash" => {
111 let height = _params
112 .get(0)
113 .and_then(|v| v.as_u64())
114 .ok_or_else(|| abtc_ports::RpcError::invalid_params("missing height"))?
115 as u32;
116
117 let idx = self.block_index.read().await;
118 match idx.get_hash_at_height(height) {
119 Some(hash) => Ok(Some(Value::String(hash.to_hex_reversed()))),
120 None => Err(abtc_ports::RpcError::invalid_params(
121 "Block height out of range",
122 )),
123 }
124 }
125 "getblock" => {
126 let hash_hex = _params
127 .get(0)
128 .and_then(|v| v.as_str())
129 .ok_or_else(|| abtc_ports::RpcError::invalid_params("missing blockhash"))?;
130 let verbosity = _params.get(1).and_then(|v| v.as_u64()).unwrap_or(1);
131
132 let hash = abtc_domain::primitives::BlockHash::from_hex(hash_hex)
133 .ok_or_else(|| abtc_ports::RpcError::invalid_params("invalid blockhash"))?;
134
135 let block = self
136 .blockchain
137 .get_block(&hash)
138 .await
139 .map_err(abtc_ports::RpcError::internal_error)?
140 .ok_or_else(|| abtc_ports::RpcError {
141 code: -5,
142 message: "Block not found".to_string(),
143 data: None,
144 })?;
145
146 if verbosity == 0 {
147 Ok(Some(Value::String(hash.to_hex_reversed())))
149 } else {
150 let idx = self.block_index.read().await;
152 let (block_height, confirmations) = match idx.get(&hash) {
153 Some(entry) => {
154 let confs = (idx.best_height() as i64) - (entry.height as i64) + 1;
155 (entry.height, confs.max(1) as u64)
156 }
157 None => (0u32, 1u64),
158 };
159 let next_block_hash = idx.get_hash_at_height(block_height + 1);
160 drop(idx);
161
162 let tx_ids: Vec<Value> = block
164 .transactions
165 .iter()
166 .map(|tx| Value::String(tx.txid().to_hex_reversed()))
167 .collect();
168 let mut result = json!({
169 "hash": hash.to_hex_reversed(),
170 "confirmations": confirmations,
171 "size": block.size(),
172 "weight": block.transactions.iter()
173 .map(|tx| tx.compute_weight() as u64).sum::<u64>(),
174 "height": block_height,
175 "version": block.header.version,
176 "merkleroot": block.header.merkle_root.to_hex_reversed(),
177 "tx": tx_ids,
178 "time": block.header.time,
179 "nonce": block.header.nonce,
180 "bits": format!("{:08x}", block.header.bits),
181 "difficulty": 1.0,
182 "nTx": block.transactions.len(),
183 "previousblockhash": block.header.prev_block_hash.to_hex_reversed()
184 });
185 if let Some(next_hash) = next_block_hash {
186 result["nextblockhash"] = Value::String(next_hash.to_hex_reversed());
187 }
188 Ok(Some(result))
189 }
190 }
191 "getdifficulty" => {
192 Ok(Some(json!(1.0)))
195 }
196 "getpeerinfo" => {
197 Ok(Some(json!([])))
199 }
200 "getnetworkinfo" => Ok(Some(json!({
201 "version": 270000,
202 "subversion": "/agentic-bitcoin:0.1.0/",
203 "protocolversion": 70016,
204 "localservices": "0000000000000409",
205 "localservicesnames": ["NETWORK", "WITNESS", "NETWORK_LIMITED"],
206 "localrelay": true,
207 "timeoffset": 0,
208 "networkactive": true,
209 "connections": 0,
210 "connections_in": 0,
211 "connections_out": 0,
212 "relayfee": 0.00001,
213 "incrementalfee": 0.00001,
214 "warnings": ""
215 }))),
216 "sendrawtransaction" => {
217 let hex_str = _params
219 .get(0)
220 .and_then(|v| v.as_str())
221 .ok_or_else(|| abtc_ports::RpcError::invalid_params("missing hex string"))?;
222
223 let tx_bytes = hex::decode(hex_str)
224 .map_err(|_| abtc_ports::RpcError::invalid_params("invalid hex encoding"))?;
225
226 let (tx, _) = abtc_domain::primitives::Transaction::deserialize(&tx_bytes)
227 .map_err(|e| {
228 abtc_ports::RpcError::invalid_params(format!("TX decode failed: {}", e))
229 })?;
230
231 let txid_hex = self
232 .mempool
233 .submit_transaction(&tx)
234 .await
235 .map_err(abtc_ports::RpcError::internal_error)?;
236
237 Ok(Some(Value::String(txid_hex)))
238 }
239 "getrawtransaction" => {
240 let txid_hex = _params
243 .get(0)
244 .and_then(|v| v.as_str())
245 .ok_or_else(|| abtc_ports::RpcError::invalid_params("missing txid"))?;
246 let verbose = _params.get(1).and_then(|v| v.as_u64()).unwrap_or(0);
247
248 let txid = abtc_domain::primitives::Txid::from_hex(txid_hex)
249 .ok_or_else(|| abtc_ports::RpcError::invalid_params("invalid txid"))?;
250
251 let mempool_entry = self.mempool.get_mempool_entry(&txid).await;
253 if let Some(entry) = mempool_entry {
254 if verbose == 0 {
255 let raw_hex = hex::encode(entry.tx.serialize());
256 return Ok(Some(Value::String(raw_hex)));
257 } else {
258 return Ok(Some(json!({
259 "txid": entry.tx.txid().to_hex_reversed(),
260 "hash": entry.tx.wtxid().to_hex_reversed(),
261 "version": entry.tx.version,
262 "size": entry.tx.serialize().len(),
263 "vsize": entry.tx.compute_vsize(),
264 "weight": entry.tx.compute_weight(),
265 "locktime": entry.tx.lock_time,
266 "vin": entry.tx.inputs.iter().map(|input| {
267 json!({
268 "txid": input.previous_output.txid.to_hex_reversed(),
269 "vout": input.previous_output.vout,
270 "sequence": input.sequence
271 })
272 }).collect::<Vec<Value>>(),
273 "vout": entry.tx.outputs.iter().enumerate().map(|(i, output)| {
274 json!({
275 "value": output.value.as_sat() as f64 / 100_000_000.0,
276 "n": i,
277 "scriptPubKey": {
278 "hex": hex::encode(output.script_pubkey.as_bytes())
279 }
280 })
281 }).collect::<Vec<Value>>(),
282 "hex": hex::encode(entry.tx.serialize()),
283 "confirmations": 0
284 })));
285 }
286 }
287
288 Err(abtc_ports::RpcError {
290 code: -5,
291 message: "No such mempool or blockchain transaction. Use gettxoutsetinfo to query for unspent outputs.".to_string(),
292 data: None,
293 })
294 }
295 "gettxoutsetinfo" => {
296 let info = self
297 .chain_state
298 .get_utxo_set_info()
299 .await
300 .map_err(|e| abtc_ports::RpcError::internal_error(e.to_string()))?;
301
302 let total_btc = info.total_amount.as_sat() as f64 / 100_000_000.0;
303
304 Ok(Some(json!({
305 "height": info.height,
306 "bestblock": info.best_block.to_hex_reversed(),
307 "txouts": info.txout_count,
308 "total_amount": total_btc,
309 "hash_serialized_2": "0000000000000000000000000000000000000000000000000000000000000000",
310 "disk_size": 0,
311 "bogosize": info.txout_count * 50
312 })))
313 }
314 "estimatesmartfee" => {
315 let conf_target = _params.get(0).and_then(|v| v.as_u64()).unwrap_or(6) as u32;
316 let _estimate_mode = _params
317 .get(1)
318 .and_then(|v| v.as_str())
319 .unwrap_or("conservative");
320
321 let estimator = self.fee_estimator.read().await;
322 let fee_rate_sat_vb = estimator.estimate_fee(conf_target);
323 drop(estimator);
324
325 let feerate_btc_kvb = fee_rate_sat_vb * 1000.0 / 100_000_000.0;
328
329 let mut result = json!({
330 "feerate": feerate_btc_kvb,
331 "blocks": conf_target
332 });
333
334 if fee_rate_sat_vb <= 1.0 {
336 result["errors"] = json!(["Insufficient data or no feerate found"]);
337 }
338
339 Ok(Some(result))
340 }
341 "estimaterawfee" => {
342 let conf_target = _params.get(0).and_then(|v| v.as_u64()).unwrap_or(6) as u32;
343
344 let estimator = self.fee_estimator.read().await;
345 let fee_rate = estimator.estimate_fee(conf_target);
346 let (p10, p25, p50, p75, p90) = estimator.fee_rate_percentiles();
347 drop(estimator);
348
349 Ok(Some(json!({
350 "short": {
351 "feerate": fee_rate * 1000.0 / 100_000_000.0,
352 "decay": 0.998,
353 "scale": 1,
354 "pass": {
355 "startrange": 1.0,
356 "endrange": 10000.0,
357 "totalconfirmed": 0.0,
358 "inmempool": 0.0,
359 "leftmempool": 0.0
360 },
361 "fail": {
362 "startrange": 0.0,
363 "endrange": 0.0,
364 "totalconfirmed": 0.0,
365 "inmempool": 0.0,
366 "leftmempool": 0.0
367 }
368 },
369 "medium": {
370 "feerate": fee_rate * 1000.0 / 100_000_000.0,
371 "decay": 0.998,
372 "scale": 2
373 },
374 "long": {
375 "feerate": fee_rate * 1000.0 / 100_000_000.0,
376 "decay": 0.998,
377 "scale": 4
378 },
379 "percentiles": {
380 "p10": p10,
381 "p25": p25,
382 "p50": p50,
383 "p75": p75,
384 "p90": p90
385 }
386 })))
387 }
388 _ => Ok(None), }
390 }
391}
392
393pub struct WalletRpcHandler {
395 wallet: Arc<dyn abtc_ports::WalletPort>,
396}
397
398impl WalletRpcHandler {
399 pub fn new(wallet: Arc<dyn abtc_ports::WalletPort>) -> Self {
401 WalletRpcHandler { wallet }
402 }
403}
404
405#[async_trait]
406impl RpcHandler for WalletRpcHandler {
407 async fn handle_request(
408 &self,
409 method: &str,
410 params: &Value,
411 ) -> Result<Option<Value>, abtc_ports::RpcError> {
412 match method {
413 "getbalance" => {
414 let balance = self
415 .wallet
416 .get_balance()
417 .await
418 .map_err(|e| abtc_ports::RpcError::internal_error(e.to_string()))?;
419 let btc = balance.confirmed.as_sat() as f64 / 100_000_000.0;
421 Ok(Some(json!(btc)))
422 }
423 "getwalletinfo" => {
424 let balance = self
425 .wallet
426 .get_balance()
427 .await
428 .map_err(|e| abtc_ports::RpcError::internal_error(e.to_string()))?;
429 Ok(Some(json!({
430 "walletname": "default",
431 "walletversion": 1,
432 "format": "memory",
433 "balance": balance.confirmed.as_sat() as f64 / 100_000_000.0,
434 "unconfirmed_balance": balance.unconfirmed.as_sat() as f64 / 100_000_000.0,
435 "immature_balance": balance.immature.as_sat() as f64 / 100_000_000.0,
436 "txcount": 0,
437 "keypoolsize": 0,
438 "paytxfee": 0.0,
439 "private_keys_enabled": true
440 })))
441 }
442 "getnewaddress" => {
443 let label = params.get(0).and_then(|v| v.as_str());
444 let address = self
445 .wallet
446 .get_new_address(label)
447 .await
448 .map_err(|e| abtc_ports::RpcError::internal_error(e.to_string()))?;
449 Ok(Some(Value::String(address)))
450 }
451 "listunspent" => {
452 let min_conf = params.get(0).and_then(|v| v.as_u64()).unwrap_or(1) as u32;
453 let utxos = self
454 .wallet
455 .list_unspent(min_conf, None)
456 .await
457 .map_err(|e| abtc_ports::RpcError::internal_error(e.to_string()))?;
458 let result: Vec<Value> = utxos
459 .iter()
460 .map(|u| {
461 json!({
462 "txid": u.outpoint.txid.to_hex_reversed(),
463 "vout": u.outpoint.vout,
464 "amount": u.output.value.as_sat() as f64 / 100_000_000.0,
465 "confirmations": u.confirmations,
466 "spendable": true,
467 "solvable": true
468 })
469 })
470 .collect();
471 Ok(Some(json!(result)))
472 }
473 "importprivkey" => {
474 let wif = params
475 .get(0)
476 .and_then(|v| v.as_str())
477 .ok_or_else(|| abtc_ports::RpcError::invalid_params("missing WIF key"))?;
478 let label = params.get(1).and_then(|v| v.as_str());
479 let rescan = params.get(2).and_then(|v| v.as_bool()).unwrap_or(true);
480 self.wallet
481 .import_key(wif, label, rescan)
482 .await
483 .map_err(|e| abtc_ports::RpcError::internal_error(e.to_string()))?;
484 Ok(Some(Value::Null))
485 }
486 _ => Ok(None),
487 }
488 }
489}
490
491pub struct MiningRpcHandler {
493 mining: Arc<MiningService>,
494}
495
496impl MiningRpcHandler {
497 pub fn new(mining: Arc<MiningService>) -> Self {
499 MiningRpcHandler { mining }
500 }
501}
502
503#[async_trait]
504impl RpcHandler for MiningRpcHandler {
505 async fn handle_request(
506 &self,
507 method: &str,
508 _params: &Value,
509 ) -> Result<Option<Value>, abtc_ports::RpcError> {
510 match method {
511 "getblocktemplate" => {
512 let template = self
513 .mining
514 .generate_block_template(&Script::new())
515 .await
516 .map_err(abtc_ports::RpcError::internal_error)?;
517
518 let total_fees: i64 = template.fees.iter().map(|f| f.as_sat()).sum();
519
520 Ok(Some(json!({
521 "version": template.block.header.version,
522 "previousblockhash": template.block.header.prev_block_hash.to_hex_reversed(),
523 "transactions": template.block.transactions.len() - 1, "coinbaseaux": {},
525 "coinbasevalue": template.block.transactions[0].total_output_value().as_sat(),
526 "longpollid": template.block.header.prev_block_hash.to_hex_reversed(),
527 "target": format!("{:08x}", template.target),
528 "mintime": template.block.header.time,
529 "mutable": ["time", "transactions", "prevblock"],
530 "noncerange": "00000000ffffffff",
531 "sigoplimit": 20000,
532 "sizelimit": 4000000,
533 "weightlimit": 4000000,
534 "curtime": template.block.header.time,
535 "bits": format!("{:08x}", template.target),
536 "height": template.height,
537 "fees": total_fees
538 })))
539 }
540 "submitblock" => {
541 Ok(Some(Value::Null))
543 }
544 "getmininginfo" => Ok(Some(json!({
545 "blocks": 0,
546 "difficulty": 1.0,
547 "networkhashps": 0,
548 "pooledtx": 0,
549 "chain": "main",
550 "warnings": ""
551 }))),
552 _ => Ok(None),
553 }
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use abtc_domain::primitives::{Amount, OutPoint, Transaction, TxIn, TxOut, Txid};
560 use abtc_domain::script::Script;
561 use std::sync::Arc;
562
563 #[test]
564 fn test_tx_hex_roundtrip() {
565 let tx = Transaction::v1(
567 vec![TxIn::final_input(
568 OutPoint::new(Txid::zero(), 0),
569 Script::new(),
570 )],
571 vec![TxOut::new(Amount::from_sat(50_000), Script::new())],
572 0,
573 );
574
575 let raw = tx.serialize();
576 let hex_str = hex::encode(&raw);
577
578 let decoded_bytes = hex::decode(&hex_str).unwrap();
579 let (decoded_tx, _) = Transaction::deserialize(&decoded_bytes).unwrap();
580 assert_eq!(decoded_tx.txid(), tx.txid());
581 }
582
583 #[test]
584 fn test_invalid_hex_detection() {
585 let bad_hex = "zzzz";
586 assert!(hex::decode(bad_hex).is_err());
587 }
588
589 #[tokio::test]
590 async fn test_estimatesmartfee_default_target() {
591 use crate::fee_estimator::FeeEstimator;
592 use tokio::sync::RwLock;
593
594 let estimator = Arc::new(RwLock::new(FeeEstimator::new()));
595
596 let est = estimator.read().await;
598 let fee_rate = est.estimate_fee(6);
599 drop(est);
600
601 assert!(fee_rate >= 1.0);
603
604 let btc_kvb = fee_rate * 1000.0 / 100_000_000.0;
606 assert!((btc_kvb - 0.00001).abs() < 1e-10);
607 }
608
609 #[tokio::test]
610 async fn test_estimatesmartfee_with_data() {
611 use crate::fee_estimator::FeeEstimator;
612 use tokio::sync::RwLock;
613
614 let estimator = Arc::new(RwLock::new(FeeEstimator::new()));
615
616 {
618 let mut est = estimator.write().await;
619 for height in 1..=20 {
620 let fees: Vec<(Amount, usize, u32)> = (0..10)
621 .map(|i| {
622 let fee = Amount::from_sat(((height * 10 + i) * 200) as i64);
623 let vsize = 200usize;
624 (fee, vsize, 1u32) })
626 .collect();
627 est.process_block(height, &fees);
628 }
629 }
630
631 let est = estimator.read().await;
632 let fee_rate = est.estimate_fee(6);
633 drop(est);
634
635 assert!(fee_rate >= 1.0);
637 }
638
639 #[tokio::test]
640 async fn test_gettxoutsetinfo_empty() {
641 use abtc_ports::ChainStateStore;
642
643 let chain_state = Arc::new(abtc_adapters::storage::InMemoryChainStateStore::new());
645
646 use abtc_domain::primitives::Amount;
648 let txid = Txid::zero();
649 let entry = abtc_ports::UtxoEntry {
650 output: TxOut::new(Amount::from_sat(50_000), Script::new()),
651 height: 1,
652 is_coinbase: false,
653 };
654 chain_state
655 .write_utxo_set(vec![(txid, 0, entry)], vec![])
656 .await
657 .unwrap();
658 chain_state
659 .write_chain_tip(abtc_domain::primitives::BlockHash::zero(), 5)
660 .await
661 .unwrap();
662
663 let info = chain_state.get_utxo_set_info().await.unwrap();
664 assert_eq!(info.txout_count, 1);
665 assert_eq!(info.total_amount.as_sat(), 50_000);
666 assert_eq!(info.height, 5);
667 }
668
669 #[test]
670 fn test_block_index_height_lookup() {
671 use crate::block_index::BlockIndex;
672 use abtc_domain::primitives::{BlockHash, BlockHeader, Hash256};
673
674 let mut index = BlockIndex::new();
675 let genesis = BlockHeader {
676 version: 1,
677 prev_block_hash: BlockHash::zero(),
678 merkle_root: Hash256::zero(),
679 time: 1231006505,
680 bits: 0x1d00ffff,
681 nonce: 0,
682 };
683 let genesis_hash = genesis.block_hash();
684 index.init_genesis(genesis);
685
686 assert_eq!(index.get_hash_at_height(0), Some(genesis_hash));
688
689 let h1 = BlockHeader {
691 version: 1,
692 prev_block_hash: genesis_hash,
693 merkle_root: Hash256::from_bytes([1u8; 32]),
694 time: 1231006506,
695 bits: 0x1d00ffff,
696 nonce: 1,
697 };
698 let (h1_hash, _) = index.add_header(h1).unwrap();
699
700 let h2 = BlockHeader {
702 version: 1,
703 prev_block_hash: h1_hash,
704 merkle_root: Hash256::from_bytes([2u8; 32]),
705 time: 1231006507,
706 bits: 0x1d00ffff,
707 nonce: 2,
708 };
709 let (h2_hash, _) = index.add_header(h2).unwrap();
710
711 assert_eq!(index.get_hash_at_height(0), Some(genesis_hash));
713 assert_eq!(index.get_hash_at_height(1), Some(h1_hash));
714 assert_eq!(index.get_hash_at_height(2), Some(h2_hash));
715 assert_eq!(index.get_hash_at_height(3), None);
716 }
717
718 #[test]
719 fn test_tx_verbose_fields() {
720 let tx = Transaction::v1(
722 vec![TxIn::final_input(
723 OutPoint::new(Txid::zero(), 0),
724 Script::new(),
725 )],
726 vec![
727 TxOut::new(Amount::from_sat(30_000), Script::new()),
728 TxOut::new(Amount::from_sat(20_000), Script::new()),
729 ],
730 0,
731 );
732
733 assert_eq!(tx.version, 1);
734 assert_eq!(tx.outputs.len(), 2);
735 assert!(tx.compute_vsize() > 0);
736 assert!(tx.compute_weight() > 0);
737 assert!(!tx.txid().to_hex_reversed().is_empty());
738 }
739}