Skip to main content

abtc_application/
handlers.rs

1//! Event and command handlers for the application layer
2//!
3//! Handles RPC requests, network messages, and other application events.
4
5use 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
15/// RPC handler for blockchain queries and operations
16pub 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    /// Create a new blockchain RPC handler.
26    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                    // Return serialised hex (simplified — just return hash)
148                    Ok(Some(Value::String(hash.to_hex_reversed())))
149                } else {
150                    // Look up block height and confirmations from the block index
151                    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                    // Return JSON object
163                    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                // Difficulty = pow_limit_target / current_target
193                // For now return 1.0 (minimum difficulty)
194                Ok(Some(json!(1.0)))
195            }
196            "getpeerinfo" => {
197                // Stub — would need PeerManager access
198                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                // Parse hex-encoded raw transaction and submit to mempool.
218                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                // Look up a transaction by txid.
241                // Checks mempool first, then block store.
242                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                // Try mempool first.
252                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                // Not in mempool — transaction not found (block store lookup would go here).
289                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                // Bitcoin Core returns fee rate in BTC/kvB (per 1000 virtual bytes).
326                // fee_rate_sat_vb is in sat/vB, so: BTC/kvB = sat/vB * 1000 / 100_000_000
327                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 we have no data, include an errors array like Bitcoin Core does
335                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), // Let another handler try
389        }
390    }
391}
392
393/// RPC handler for wallet operations
394pub struct WalletRpcHandler {
395    wallet: Arc<dyn abtc_ports::WalletPort>,
396}
397
398impl WalletRpcHandler {
399    /// Create a new wallet RPC handler.
400    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                // Return confirmed balance in BTC
420                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
491/// RPC handler for mining operations
492pub struct MiningRpcHandler {
493    mining: Arc<MiningService>,
494}
495
496impl MiningRpcHandler {
497    /// Create a new mining RPC handler.
498    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, // Exclude coinbase
524                    "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                // TODO: Parse block from hex params
542                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        // Build a simple transaction, serialize to hex, deserialize back.
566        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        // With no data, should still return a result with the minimum fee rate
597        let est = estimator.read().await;
598        let fee_rate = est.estimate_fee(6);
599        drop(est);
600
601        // Minimum fee rate is 1.0 sat/vB
602        assert!(fee_rate >= 1.0);
603
604        // Verify BTC/kvB conversion: 1.0 sat/vB * 1000 / 100_000_000 = 0.00001
605        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        // Feed some blocks with varying fee rates
617        {
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) // all confirmed in 1 block
625                    })
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        // After processing data, should return a fee rate above minimum
636        assert!(fee_rate >= 1.0);
637    }
638
639    #[tokio::test]
640    async fn test_gettxoutsetinfo_empty() {
641        use abtc_ports::ChainStateStore;
642
643        // Create a mock chain state store
644        let chain_state = Arc::new(abtc_adapters::storage::InMemoryChainStateStore::new());
645
646        // Add a UTXO
647        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        // Height 0 → genesis
687        assert_eq!(index.get_hash_at_height(0), Some(genesis_hash));
688
689        // Add block at height 1
690        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        // Add block at height 2
701        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        // All heights should be resolvable
712        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        // Verify that we can compute all the fields needed for verbose getrawtransaction.
721        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}