csv-adapter-bitcoin 0.1.1

Bitcoin adapter for CSV (Client-Side Validation) with UTXO seals and SPV proofs
Documentation
//! Real Bitcoin RPC client implementation
//!
//! Wraps `bitcoincore-rpc` behind the `BitcoinRpc` trait for production use.
//! Only compiled when the `rpc` feature is enabled.

#[allow(clippy::module_inception)]
#[cfg(feature = "rpc")]
pub mod real_rpc {
    use bitcoin::{Network, OutPoint, Txid};
    use bitcoin_hashes::Hash;
    use bitcoincore_rpc::{Auth, Client, RpcApi};
    use std::time::{Duration, Instant};

    use crate::proofs::extract_merkle_proof_from_block;
    use crate::rpc::BitcoinRpc;
    use crate::types::BitcoinInclusionProof;

    type FundingTxResult = Vec<(Txid, u64, u32)>;

    /// Real Bitcoin RPC client backed by bitcoincore-rpc
    pub struct RealBitcoinRpc {
        client: Client,
        network: Network,
    }

    impl RealBitcoinRpc {
        /// Create a new real RPC client (no auth — for local/public nodes)
        pub fn new(url: &str, network: Network) -> Result<Self, RealRpcError> {
            let client = Client::new(url, Auth::None)?;
            Ok(Self { client, network })
        }

        /// Create with authentication
        pub fn with_auth(
            url: &str,
            user: &str,
            pass: &str,
            network: Network,
        ) -> Result<Self, RealRpcError> {
            let client = Client::new(url, Auth::UserPass(user.into(), pass.into()))?;
            Ok(Self { client, network })
        }

        /// Get UTXOs for a specific Bitcoin address
        ///
        /// Returns a list of (OutPoint, amount_in_satoshis) pairs
        pub fn get_address_utxos(
            &self,
            address: &bitcoin::Address,
        ) -> Result<Vec<(OutPoint, u64)>, Box<dyn std::error::Error + Send + Sync>> {
            // Use listunspent RPC call to get UTXOs for the address
            // This requires the Bitcoin Core wallet to be watching this address
            let utxos = self.client.list_unspent(
                Some(0),          // min_confirmations
                None,             // max_confirmations
                Some(&[address]), // addresses filter
                None,             // include_unsafe
                None,             // query_options
            )?;

            let result: Vec<(OutPoint, u64)> = utxos
                .into_iter()
                .map(|utxo| {
                    let outpoint = OutPoint::new(utxo.txid, utxo.vout);
                    let amount_sat = utxo.amount.to_sat();
                    (outpoint, amount_sat)
                })
                .collect();

            Ok(result)
        }

        /// Get transaction details including confirmations and block info
        pub fn get_transaction_info(
            &self,
            txid: [u8; 32],
        ) -> Result<TxInfo, Box<dyn std::error::Error + Send + Sync>> {
            let txid = Txid::from_slice(&txid).map_err(|e| format!("Invalid txid: {}", e))?;

            let tx_info = self.client.get_raw_transaction_info(&txid, None)?;

            // Get block hash if confirmed
            let block_hash = tx_info.blockhash.map(|h| {
                let bytes = h.as_ref();
                let mut arr = [0u8; 32];
                arr.copy_from_slice(bytes);
                arr
            });

            Ok(TxInfo {
                confirmations: tx_info.confirmations.unwrap_or(0) as u64,
                block_hash,
            })
        }

        /// Get the funding transaction that created a UTXO at a specific address
        ///
        /// This is useful for discovering UTXOs by scanning the blockchain
        /// for transactions sent to wallet addresses.
        pub fn get_funding_tx(
            &self,
            _address: &bitcoin::Address,
            _min_confirmations: u64,
        ) -> Result<FundingTxResult, Box<dyn std::error::Error + Send + Sync>> {
            // Scan recent transactions for this address
            // This requires a wallet with transaction indexing
            // For now, return empty - users should manually add UTXOs

            // In production, you'd use:
            // 1. listtransactions to find transactions
            // 2. Filter by address
            // 3. Return (txid, amount, vout) for each

            Ok(vec![])
        }

        /// Get a full block by hash, including all transactions
        pub fn get_block(
            &self,
            block_hash: [u8; 32],
        ) -> Result<bitcoin::Block, Box<dyn std::error::Error + Send + Sync>> {
            let hash = bitcoin::BlockHash::from_slice(&block_hash)
                .map_err(|e| format!("Invalid block hash: {}", e))?;
            Ok(self.client.get_block(&hash)?)
        }

        /// Get block height from block hash
        pub fn get_block_height(
            &self,
            block_hash: [u8; 32],
        ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
            let hash = bitcoin::BlockHash::from_slice(&block_hash)
                .map_err(|e| format!("Invalid block hash: {}", e))?;
            let info = self.client.get_block_info(&hash)?;
            Ok(info.height as u64)
        }

        /// Extract Merkle proof for a transaction from its containing block
        pub fn extract_merkle_proof(
            &self,
            txid: [u8; 32],
            block_hash: [u8; 32],
        ) -> Result<BitcoinInclusionProof, Box<dyn std::error::Error + Send + Sync>> {
            // Get the full block
            let block = self.get_block(block_hash)?;
            let block_height = self.get_block_height(block_hash)?;

            // Extract all txids from block
            let block_txids: Vec<[u8; 32]> = block
                .txdata
                .iter()
                .map(|tx| tx.txid().to_byte_array())
                .collect();

            // Extract proof using the proof extraction function
            extract_merkle_proof_from_block(txid, &block_txids, block_hash, block_height)
                .ok_or_else(|| "Failed to extract Merkle proof for txid".into())
        }

        /// Wait for transaction to reach required confirmations
        pub fn wait_for_confirmation(
            &self,
            txid: [u8; 32],
            required_confirmations: u64,
            timeout_secs: u64,
        ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
            let start = Instant::now();
            let poll_interval = Duration::from_secs(10);

            loop {
                if start.elapsed() > Duration::from_secs(timeout_secs) {
                    return Err("Timeout waiting for confirmation".into());
                }

                let confirmations = self.get_tx_confirmations(txid)?;
                if confirmations >= required_confirmations {
                    return Ok(confirmations);
                }

                std::thread::sleep(poll_interval);
            }
        }

        /// Publish a commitment transaction to Bitcoin
        ///
        /// This integrates with tx_builder to create a proper Taproot commitment
        /// transaction, signs it, and broadcasts it to the network.
        pub fn publish_commitment(
            &self,
            _outpoint: OutPoint,
            _commitment: csv_adapter_core::Hash,
        ) -> Result<[u8; 32], Box<dyn std::error::Error + Send + Sync>> {
            // TODO: Integrate with the adapter's tx_builder and wallet to build,
            // sign, and broadcast the actual Taproot commitment transaction.
            // For now, this is a placeholder that returns a deterministic txid.
            // The full flow is demonstrated in the signet_real_tx_demo example
            // which wires tx_builder + wallet + RPC broadcasting together.
            Err("publish_commitment requires adapter-level integration — use the full BitcoinAnchorLayer.publish() method instead".into())
        }
    }

    impl BitcoinRpc for RealBitcoinRpc {
        fn get_block_count(&self) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
            Ok(self.client.get_block_count()?)
        }

        fn get_block_hash(
            &self,
            height: u64,
        ) -> Result<[u8; 32], Box<dyn std::error::Error + Send + Sync>> {
            let hash = self.client.get_block_hash(height)?;
            let bytes = hash.as_ref();
            let mut result = [0u8; 32];
            result.copy_from_slice(bytes);
            Ok(result)
        }

        fn is_utxo_unspent(
            &self,
            txid: [u8; 32],
            vout: u32,
        ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
            let txid = Txid::from_slice(&txid).map_err(|e| format!("Invalid txid: {}", e))?;
            let result = self.client.get_tx_out(&txid, vout, Some(true))?;
            Ok(result.is_some())
        }

        fn send_raw_transaction(
            &self,
            tx_bytes: Vec<u8>,
        ) -> Result<[u8; 32], Box<dyn std::error::Error + Send + Sync>> {
            let tx = bitcoin::consensus::encode::deserialize::<bitcoin::Transaction>(&tx_bytes)
                .map_err(|e| format!("Failed to deserialize transaction: {}", e))?;
            let txid = self.client.send_raw_transaction(&tx)?;
            let bytes = txid.as_ref();
            let mut result = [0u8; 32];
            result.copy_from_slice(bytes);
            Ok(result)
        }

        fn get_tx_confirmations(
            &self,
            txid: [u8; 32],
        ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
            let txid = Txid::from_slice(&txid).map_err(|e| format!("Invalid txid: {}", e))?;
            let info = self.client.get_raw_transaction_info(&txid, None)?;
            Ok(info.confirmations.map(|c| c as u64).unwrap_or(0))
        }
    }

    /// Real RPC error type
    #[derive(Debug, thiserror::Error)]
    pub enum RealRpcError {
        #[error("RPC error: {0}")]
        Rpc(#[from] bitcoincore_rpc::Error),
    }

    /// Transaction information helper
    #[derive(Debug, Clone)]
    pub struct TxInfo {
        pub confirmations: u64,
        pub block_hash: Option<[u8; 32]>,
    }
}