use super::RpcHandler; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use zeroize::Zeroize; #[derive(Debug, Serialize)] struct BitcoinInfo { block_height: u64, sync_progress: f64, chain: String, difficulty: f64, mempool_size: u64, mempool_tx_count: u64, verification_progress: f64, } #[derive(Debug, Deserialize)] struct BitcoinRpcResponse { result: Option, error: Option, } #[derive(Debug, Deserialize)] struct BlockchainInfo { chain: Option, blocks: Option, difficulty: Option, #[serde(rename = "verificationprogress")] verification_progress: Option, } #[derive(Debug, Deserialize)] struct MempoolInfo { size: Option, bytes: Option, } impl RpcHandler { pub(super) async fn handle_bitcoin_getinfo(&self) -> Result { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() .context("Failed to create HTTP client")?; let blockchain_info = self .bitcoin_rpc_call::(&client, "getblockchaininfo", &[]) .await .context("Failed to query getblockchaininfo")?; let mempool_info = self .bitcoin_rpc_call::(&client, "getmempoolinfo", &[]) .await .unwrap_or(MempoolInfo { size: Some(0), bytes: Some(0), }); let info = BitcoinInfo { block_height: blockchain_info.blocks.unwrap_or(0), sync_progress: blockchain_info.verification_progress.unwrap_or(0.0), chain: blockchain_info.chain.unwrap_or_else(|| "unknown".into()), difficulty: blockchain_info.difficulty.unwrap_or(0.0), mempool_size: mempool_info.bytes.unwrap_or(0), mempool_tx_count: mempool_info.size.unwrap_or(0), verification_progress: blockchain_info.verification_progress.unwrap_or(0.0), }; Ok(serde_json::to_value(info)?) } async fn bitcoin_rpc_call( &self, client: &reqwest::Client, method: &str, params: &[serde_json::Value], ) -> Result { let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await; let body = serde_json::json!({ "jsonrpc": "1.0", "id": "archy", "method": method, "params": params, }); let resp = client .post(crate::constants::BITCOIN_RPC_URL) .basic_auth(&rpc_user, Some(&rpc_pass)) .json(&body) .send() .await .context("Bitcoin RPC connection failed")?; let rpc_resp: BitcoinRpcResponse = resp .json() .await .context("Failed to parse Bitcoin RPC response")?; if let Some(err) = rpc_resp.error { anyhow::bail!("Bitcoin RPC error: {}", err); } rpc_resp .result .ok_or_else(|| anyhow::anyhow!("Bitcoin RPC returned null result")) } /// Initialize a Bitcoin Core descriptor wallet with keys derived from the master seed. /// Creates a blank wallet and imports BIP-84 (native segwit) descriptors. /// Requires: password re-verification, encrypted seed on disk. pub(super) async fn handle_bitcoin_init_wallet_from_seed( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let password = params .get("password") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'password' for seed access"))?; let wallet_name = params .get("wallet_name") .and_then(|v| v.as_str()) .unwrap_or("archipelago"); // Verify user password. self.auth_manager .verify_password(password) .await .context("Password verification failed")?; // Load encrypted seed. let mnemonic = crate::seed::load_seed_encrypted(&self.config.data_dir, password) .await .context("Failed to load encrypted seed")?; let seed = crate::seed::MasterSeed::from_mnemonic(&mnemonic); // Derive BIP-84 account xprv. let xprv = crate::seed::derive_bitcoin_xprv(&seed)?; let mut xprv_str = xprv.to_string(); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .context("Failed to create HTTP client")?; // Step 1: Create a blank descriptor wallet. let create_result = self .bitcoin_rpc_call::( &client, "createwallet", &[ serde_json::json!(wallet_name), // wallet_name serde_json::json!(false), // disable_private_keys serde_json::json!(true), // blank serde_json::json!(""), // passphrase serde_json::json!(false), // avoid_reuse serde_json::json!(true), // descriptors ], ) .await; match create_result { Ok(_) => tracing::info!("Created blank descriptor wallet '{}'", wallet_name), Err(e) => { let msg = e.to_string(); if msg.contains("already exists") { tracing::info!( "Wallet '{}' already exists, importing descriptors", wallet_name ); } else { xprv_str.zeroize(); return Err(e.context("Failed to create wallet")); } } } // Step 2: Import BIP-84 descriptors (external + internal/change). // Format: wpkh(xprv/0/*) for receive, wpkh(xprv/1/*) for change. let external_desc = format!("wpkh({}/0/*)", xprv_str); let internal_desc = format!("wpkh({}/1/*)", xprv_str); // Get checksums from Bitcoin Core. let ext_info: serde_json::Value = self .bitcoin_rpc_call( &client, "getdescriptorinfo", &[serde_json::json!(external_desc)], ) .await .context("getdescriptorinfo failed for external descriptor")?; let int_info: serde_json::Value = self .bitcoin_rpc_call( &client, "getdescriptorinfo", &[serde_json::json!(internal_desc)], ) .await .context("getdescriptorinfo failed for internal descriptor")?; let ext_desc_with_checksum = ext_info .get("descriptor") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("No descriptor in getdescriptorinfo response"))?; let int_desc_with_checksum = int_info .get("descriptor") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("No descriptor in getdescriptorinfo response"))?; let import_params = serde_json::json!([ { "desc": ext_desc_with_checksum, "timestamp": "now", "active": true, "internal": false, "range": [0, 1000], }, { "desc": int_desc_with_checksum, "timestamp": "now", "active": true, "internal": true, "range": [0, 1000], } ]); let _import_result: serde_json::Value = self .bitcoin_rpc_call(&client, "importdescriptors", &[import_params]) .await .context("importdescriptors failed")?; // Zeroize the xprv string from memory. xprv_str.zeroize(); tracing::info!( "Bitcoin Core wallet '{}' initialized from master seed (BIP-84)", wallet_name ); Ok(serde_json::json!({ "initialized": true, "wallet_name": wallet_name, })) } }