From a5a4bdf7c85e7132db3e17d0511dd64215438c9a Mon Sep 17 00:00:00 2001 From: 0xcindyv Date: Wed, 13 Aug 2025 16:22:54 -0300 Subject: [PATCH] Replace bitcoincore-rpc with custom reqwest client Eliminates minreq dependency conflict and adds HTTPS support by implementing async RPC client using reqwest and corepc-types. Uses project-wide reqwest instead of minreq Enables HTTPS connections via rustls Implements 9 required RPC methods with sync wrapper Supports cookie file and username/password auth Fixes nested runtime error by making initialization async Fixes #350 --- Cargo-minimal.lock | 15 +- Cargo-recent.lock | 15 +- payjoin-cli/Cargo.toml | 6 +- payjoin-cli/src/app/mod.rs | 6 +- payjoin-cli/src/app/rpc.rs | 258 ++++++++++++++++++++++++++++++++++ payjoin-cli/src/app/v1.rs | 9 +- payjoin-cli/src/app/v2/mod.rs | 4 +- payjoin-cli/src/app/wallet.rs | 197 ++++++++++++++------------ payjoin-cli/src/db/error.rs | 2 - payjoin-cli/src/db/v2.rs | 1 - payjoin-cli/src/main.rs | 8 +- 11 files changed, 410 insertions(+), 111 deletions(-) create mode 100644 payjoin-cli/src/app/rpc.rs diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index d6127d66d..032b5dc82 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -611,6 +611,17 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "corepc-types" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7f2faedffc7c654e348e2da6c6416090525c6072979fee9681d620d1d398a4" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + [[package]] name = "cpufeatures" version = "0.1.5" @@ -1672,11 +1683,12 @@ version = "0.2.0" dependencies = [ "anyhow", "async-trait", - "bitcoincore-rpc", "clap", "config", + "corepc-types", "dirs", "env_logger", + "futures", "http-body-util", "hyper", "hyper-rustls", @@ -1689,6 +1701,7 @@ dependencies = [ "reqwest", "rustls 0.22.4", "serde", + "serde_json", "sled", "tempfile", "tokio", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index d6127d66d..032b5dc82 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -611,6 +611,17 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "corepc-types" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7f2faedffc7c654e348e2da6c6416090525c6072979fee9681d620d1d398a4" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + [[package]] name = "cpufeatures" version = "0.1.5" @@ -1672,11 +1683,12 @@ version = "0.2.0" dependencies = [ "anyhow", "async-trait", - "bitcoincore-rpc", "clap", "config", + "corepc-types", "dirs", "env_logger", + "futures", "http-body-util", "hyper", "hyper-rustls", @@ -1689,6 +1701,7 @@ dependencies = [ "reqwest", "rustls 0.22.4", "serde", + "serde_json", "sled", "tempfile", "tokio", diff --git a/payjoin-cli/Cargo.toml b/payjoin-cli/Cargo.toml index dceac6e09..18098ee95 100644 --- a/payjoin-cli/Cargo.toml +++ b/payjoin-cli/Cargo.toml @@ -28,10 +28,11 @@ v2 = ["payjoin/v2", "payjoin/io"] [dependencies] anyhow = "1.0.70" async-trait = "0.1" -bitcoincore-rpc = "0.19.0" clap = { version = "~4.0.32", features = ["derive"] } config = "0.13.3" +corepc-types = "0.8.0" env_logger = "0.9.0" +futures = "0.3" http-body-util = { version = "0.1", optional = true } hyper = { version = "1", features = ["http1", "server"], optional = true } hyper-rustls = { version = "0.26", optional = true } @@ -39,7 +40,8 @@ hyper-util = { version = "0.1", optional = true } log = "0.4.7" payjoin = { version = "0.24.0", default-features = false } rcgen = { version = "0.11.1", optional = true } -reqwest = { version = "0.12", default-features = false } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +serde_json = "1.0" rustls = { version = "0.22.4", optional = true } serde = { version = "1.0.160", features = ["derive"] } sled = "0.34" diff --git a/payjoin-cli/src/app/mod.rs b/payjoin-cli/src/app/mod.rs index 041c99210..adeee219f 100644 --- a/payjoin-cli/src/app/mod.rs +++ b/payjoin-cli/src/app/mod.rs @@ -1,14 +1,14 @@ use std::collections::HashMap; use anyhow::{anyhow, Result}; -use bitcoincore_rpc::bitcoin::Amount; use payjoin::bitcoin::psbt::Psbt; -use payjoin::bitcoin::FeeRate; +use payjoin::bitcoin::{Amount, FeeRate}; use payjoin::{bitcoin, PjUri}; use tokio::signal; use tokio::sync::watch; pub mod config; +pub mod rpc; pub mod wallet; use crate::app::config::Config; use crate::app::wallet::BitcoindWallet; @@ -20,7 +20,7 @@ pub(crate) mod v2; #[async_trait::async_trait] pub trait App: Send + Sync { - fn new(config: Config) -> Result + async fn new(config: Config) -> Result where Self: Sized; fn wallet(&self) -> BitcoindWallet; diff --git a/payjoin-cli/src/app/rpc.rs b/payjoin-cli/src/app/rpc.rs new file mode 100644 index 000000000..884fc866f --- /dev/null +++ b/payjoin-cli/src/app/rpc.rs @@ -0,0 +1,258 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use anyhow::{anyhow, Context, Result}; +use corepc_types::v26::{WalletCreateFundedPsbt, WalletProcessPsbt}; +use payjoin::bitcoin::{Address, Amount, Network, Txid}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +/// Authentication method for Bitcoin Core RPC +#[derive(Clone, Debug)] +pub enum Auth { + UserPass(String, String), + CookieFile(PathBuf), +} + +/// Internal async Bitcoin RPC client using reqwest +pub struct AsyncBitcoinRpc { + client: Client, + url: String, + username: String, + password: String, +} + +impl AsyncBitcoinRpc { + pub async fn new(url: String, auth: Auth) -> Result { + let client = + Client::builder().use_rustls_tls().build().context("Failed to create HTTP client")?; + + // Load credentials once at initialization - no repeated file I/O + let (username, password) = match auth { + Auth::UserPass(user, pass) => (user, pass), + Auth::CookieFile(path) => { + let cookie = tokio::fs::read_to_string(&path) + .await + .with_context(|| format!("Failed to read cookie file: {path:?}"))?; + let parts: Vec<&str> = cookie.trim().split(':').collect(); + if parts.len() != 2 { + return Err(anyhow!("Invalid cookie format in file: {path:?}")); + } + (parts[0].to_string(), parts[1].to_string()) + } + }; + + Ok(Self { client, url, username, password }) + } + + /// Get base URL without wallet path for blockchain-level calls + fn get_base_url(&self) -> String { + if let Some(pos) = self.url.find("/wallet/") { + self.url[..pos].to_string() + } else { + self.url.clone() + } + } + + /// Make a JSON-RPC call to Bitcoin Core + async fn call_rpc(&self, method: &str, params: serde_json::Value) -> Result + where + T: for<'de> Deserialize<'de>, + { + // Determine which URL to use based on the method + // Blockchain/network calls go to base URL, wallet calls go to wallet URL + let url = match method { + "getblockchaininfo" | "getnetworkinfo" | "getmininginfo" | "getblockcount" + | "getbestblockhash" | "getblock" | "getblockhash" | "gettxout" => self.get_base_url(), + _ => self.url.clone(), + }; + + let request_body = json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": 1 + }); + + let request = self + .client + .post(&url) + .json(&request_body) + .basic_auth(&self.username, Some(&self.password)); + + let response = request.send().await.context("Failed to send RPC request")?; + + if !response.status().is_success() { + return Err(anyhow!("RPC request failed with status: {}", response.status())); + } + + let json: RpcResponse = response.json().await.context("Failed to parse RPC response")?; + + match json { + RpcResponse::Success { result, .. } => Ok(result), + RpcResponse::Error { error, .. } => Err(anyhow!("RPC error: {:?}", error)), + } + } + + pub async fn wallet_create_funded_psbt( + &self, + inputs: &[Value], + outputs: &HashMap, + locktime: Option, + options: Option, + bip32derivs: Option, + ) -> Result { + let outputs_btc: HashMap = + outputs.iter().map(|(addr, amount)| (addr.clone(), amount.to_btc())).collect(); + + let locktime = locktime.unwrap_or(0); + let options = options.unwrap_or_else(|| json!({})); + let bip32derivs = bip32derivs.unwrap_or(true); + + let params = json!([inputs, outputs_btc, locktime, options, bip32derivs]); + self.call_rpc("walletcreatefundedpsbt", params).await + } + + pub async fn wallet_process_psbt( + &self, + psbt: &str, + sign: Option, + sighash_type: Option, + bip32derivs: Option, + ) -> Result { + let sign = sign.unwrap_or(true); + let sighash_type = sighash_type.unwrap_or_else(|| "ALL".to_string()); + let bip32derivs = bip32derivs.unwrap_or(true); + + let params = json!([psbt, sign, sighash_type, bip32derivs]); + self.call_rpc("walletprocesspsbt", params).await + } + + pub async fn finalize_psbt( + &self, + psbt: &str, + extract: Option, + ) -> Result { + let extract = extract.unwrap_or(true); + let params = json!([psbt, extract]); + self.call_rpc("finalizepsbt", params).await + } + + pub async fn test_mempool_accept( + &self, + rawtxs: &[String], + ) -> Result> { + let params = json!([rawtxs]); + self.call_rpc("testmempoolaccept", params).await + } + + pub async fn send_raw_transaction(&self, hex: &[u8]) -> Result { + use payjoin::bitcoin::hex::DisplayHex; + let hex_string = hex.to_lower_hex_string(); + let params = json!([hex_string]); + let txid_string: String = self.call_rpc("sendrawtransaction", params).await?; + Ok(txid_string.parse()?) + } + + pub async fn get_address_info(&self, address: &Address) -> Result { + let params = json!([address.to_string()]); + self.call_rpc("getaddressinfo", params).await + } + + pub async fn get_new_address( + &self, + label: Option<&str>, + address_type: Option<&str>, + ) -> Result> { + let params = if label.is_none() && address_type.is_none() { + json!([]) + } else { + json!([label, address_type]) + }; + + let address_string: String = self.call_rpc("getnewaddress", params).await?; + let addr: payjoin::bitcoin::Address = + address_string.parse().context("Failed to parse address")?; + Ok(addr) + } + + pub async fn list_unspent( + &self, + minconf: Option, + maxconf: Option, + addresses: Option<&[Address]>, + include_unsafe: Option, + query_options: Option, + ) -> Result> { + let addresses_str: Option> = + addresses.map(|addrs| addrs.iter().map(|a| a.to_string()).collect()); + let params = json!([minconf, maxconf, addresses_str, include_unsafe, query_options]); + self.call_rpc("listunspent", params).await + } + + pub async fn get_blockchain_info(&self) -> Result { + let params = json!([]); + self.call_rpc("getblockchaininfo", params).await + } + + pub async fn network(&self) -> Result { + let info = self.get_blockchain_info().await?; + let chain = info["chain"].as_str().ok_or_else(|| anyhow!("Missing chain field"))?; + match chain { + "main" => Ok(Network::Bitcoin), + "test" => Ok(Network::Testnet), + "regtest" => Ok(Network::Regtest), + "signet" => Ok(Network::Signet), + other => Err(anyhow!("Unknown network: {}", other)), + } + } +} + +/// JSON-RPC response envelope +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +enum RpcResponse { + Success { result: T, error: Option, id: Value }, + Error { result: Option, error: RpcError, id: Value }, +} + +#[derive(Serialize, Deserialize, Debug)] +struct RpcError { + code: i32, + message: String, +} + +/// Result type for testmempoolaccept RPC call - minimal struct for our use case +#[derive(Debug, Deserialize)] +pub struct TestMempoolAcceptResult { + pub allowed: bool, + // Ignore additional fields that Bitcoin Core v29 may include +} + +/// Result type for getaddressinfo RPC call - minimal struct for our use case +#[derive(Debug, Deserialize)] +pub struct GetAddressInfoResult { + #[serde(rename = "ismine")] + pub is_mine: bool, +} + +/// Result type for listunspent RPC call - compatible with both v26 and v29+ +#[derive(Debug, Deserialize)] +pub struct ListUnspentResult { + pub txid: String, + pub vout: u32, + #[serde(rename = "scriptPubKey")] + pub script_pubkey: String, + pub amount: f64, + // Optional fields for compatibility with newer Bitcoin Core versions + #[serde(rename = "redeemScript")] + pub redeem_script: Option, + // Ignore additional fields that Bitcoin Core v29+ may include +} + +/// Result type for finalizepsbt RPC call - compatible with both v26 and v29+ +#[derive(Debug, Deserialize)] +pub struct FinalizePsbtResult { + pub hex: Option, +} diff --git a/payjoin-cli/src/app/v1.rs b/payjoin-cli/src/app/v1.rs index 9e6ccccbb..3051dface 100644 --- a/payjoin-cli/src/app/v1.rs +++ b/payjoin-cli/src/app/v1.rs @@ -4,7 +4,6 @@ use std::str::FromStr; use std::sync::Arc; use anyhow::{anyhow, Context, Result}; -use bitcoincore_rpc::bitcoin::Amount; use http_body_util::combinators::BoxBody; use http_body_util::{BodyExt, Full}; use hyper::body::{Bytes, Incoming}; @@ -13,7 +12,7 @@ use hyper::service::service_fn; use hyper::{Method, Request, Response, StatusCode}; use hyper_util::rt::TokioIo; use payjoin::bitcoin::psbt::Psbt; -use payjoin::bitcoin::FeeRate; +use payjoin::bitcoin::{Amount, FeeRate}; use payjoin::receive::v1::{PayjoinProposal, UncheckedProposal}; use payjoin::receive::ReplyableError::{self, Implementation, V1}; use payjoin::send::v1::SenderBuilder; @@ -44,11 +43,11 @@ pub(crate) struct App { #[async_trait::async_trait] impl AppTrait for App { - fn new(config: Config) -> Result { + async fn new(config: Config) -> Result { let db = Arc::new(Database::create(&config.db_path)?); let (interrupt_tx, interrupt_rx) = watch::channel(()); tokio::spawn(handle_interrupt(interrupt_tx)); - let wallet = BitcoindWallet::new(&config.bitcoind)?; + let wallet = BitcoindWallet::new(&config.bitcoind).await?; let app = Self { config, db, wallet, interrupt: interrupt_rx }; app.wallet() .network() @@ -147,7 +146,7 @@ impl App { "Listening at {}. Configured to accept payjoin at BIP 21 Payjoin Uri:", listener.local_addr()? ); - println!("{}", pj_uri_string); + println!("{pj_uri_string}"); let app = self.clone(); diff --git a/payjoin-cli/src/app/v2/mod.rs b/payjoin-cli/src/app/v2/mod.rs index 15a209598..6d1c88967 100644 --- a/payjoin-cli/src/app/v2/mod.rs +++ b/payjoin-cli/src/app/v2/mod.rs @@ -37,12 +37,12 @@ pub(crate) struct App { #[async_trait::async_trait] impl AppTrait for App { - fn new(config: Config) -> Result { + async fn new(config: Config) -> Result { let db = Arc::new(Database::create(&config.db_path)?); let relay_manager = Arc::new(Mutex::new(RelayManager::new())); let (interrupt_tx, interrupt_rx) = watch::channel(()); tokio::spawn(handle_interrupt(interrupt_tx)); - let wallet = BitcoindWallet::new(&config.bitcoind)?; + let wallet = BitcoindWallet::new(&config.bitcoind).await?; let app = Self { config, db, wallet, interrupt: interrupt_rx, relay_manager }; app.wallet() .network() diff --git a/payjoin-cli/src/app/wallet.rs b/payjoin-cli/src/app/wallet.rs index bb50d7dae..a96fa50b3 100644 --- a/payjoin-cli/src/app/wallet.rs +++ b/payjoin-cli/src/app/wallet.rs @@ -3,37 +3,38 @@ use std::str::FromStr; use std::sync::Arc; use anyhow::{anyhow, Context, Result}; -use bitcoincore_rpc::json::WalletCreateFundedPsbtOptions; -use bitcoincore_rpc::{Auth, Client, RpcApi}; use payjoin::bitcoin::consensus::encode::{deserialize, serialize_hex}; use payjoin::bitcoin::consensus::Encodable; use payjoin::bitcoin::psbt::{Input, Psbt}; use payjoin::bitcoin::{ - Address, Amount, Denomination, FeeRate, Network, OutPoint, Script, Transaction, TxIn, TxOut, + self, Address, Amount, FeeRate, Network, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, Txid, }; use payjoin::receive::InputPair; +use serde_json::json; -/// Implementation of PayjoinWallet for bitcoind -#[derive(Clone, Debug)] +use crate::app::rpc::{AsyncBitcoinRpc, Auth}; + +/// Implementation of PayjoinWallet for bitcoind using async RPC client +#[derive(Clone)] pub struct BitcoindWallet { - pub bitcoind: std::sync::Arc, + rpc: Arc, } impl BitcoindWallet { - pub fn new(config: &crate::app::config::BitcoindConfig) -> Result { - let client = match &config.cookie { + pub async fn new(config: &crate::app::config::BitcoindConfig) -> Result { + let auth = match &config.cookie { Some(cookie) if cookie.as_os_str().is_empty() => return Err(anyhow!( "Cookie authentication enabled but no cookie path provided in config.toml" )), - Some(cookie) => Client::new(config.rpchost.as_str(), Auth::CookieFile(cookie.into())), - None => Client::new( - config.rpchost.as_str(), - Auth::UserPass(config.rpcuser.clone(), config.rpcpassword.clone()), - ), - }?; - Ok(Self { bitcoind: Arc::new(client) }) + Some(cookie) => Auth::CookieFile(cookie.into()), + None => Auth::UserPass(config.rpcuser.clone(), config.rpcpassword.clone()), + }; + + let rpc = AsyncBitcoinRpc::new(config.rpchost.to_string(), auth).await?; + + Ok(Self { rpc: Arc::new(rpc) }) } } @@ -45,36 +46,36 @@ impl BitcoindWallet { fee_rate: FeeRate, lock_unspent: bool, ) -> Result { - let fee_sat_per_kvb = - fee_rate.to_sat_per_kwu().checked_mul(4).ok_or_else(|| anyhow!("Invalid fee rate"))?; - let fee_per_kvb = Amount::from_sat(fee_sat_per_kvb); - log::debug!("Fee rate sat/kvb: {}", fee_per_kvb.display_in(Denomination::Satoshi)); - - let options = WalletCreateFundedPsbtOptions { - lock_unspent: Some(lock_unspent), - fee_rate: Some(fee_per_kvb), - ..Default::default() - }; - - let psbt = self - .bitcoind - .wallet_create_funded_psbt( - &[], // inputs - &outputs, - None, // locktime - Some(options), - None, - ) - .context("Failed to create PSBT")? - .psbt; - - let psbt = self - .bitcoind - .wallet_process_psbt(&psbt, None, None, None) - .context("Failed to process PSBT")? - .psbt; - - Psbt::from_str(&psbt).context("Failed to load PSBT from base64") + let fee_sat_per_vb = fee_rate.to_sat_per_vb_ceil(); + log::debug!("Fee rate sat/vb: {}", fee_sat_per_vb); + + let options = json!({ + "lockUnspents": lock_unspent, + "fee_rate": fee_sat_per_vb + }); + + // Sync wrapper around async call - use tokio handle to avoid deadlock + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.rpc + .wallet_create_funded_psbt( + &[], // inputs + &outputs, + None, // locktime + Some(options), + None, + ) + .await + }) + })?; + + let processed = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.rpc.wallet_process_psbt(&result.psbt, None, None, None).await + }) + })?; + + Psbt::from_str(&processed.psbt).context("Failed to load PSBT from base64") } /// Process a PSBT, validating and signing inputs owned by this wallet @@ -82,49 +83,62 @@ impl BitcoindWallet { /// Does not include bip32 derivations in the PSBT pub fn process_psbt(&self, psbt: &Psbt) -> Result { let psbt_str = psbt.to_string(); - let processed = self - .bitcoind - .wallet_process_psbt(&psbt_str, None, None, Some(false)) - .context("Failed to process PSBT")? - .psbt; - Psbt::from_str(&processed).context("Failed to parse processed PSBT") + let processed = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.rpc.wallet_process_psbt(&psbt_str, None, None, Some(false)).await + }) + }) + .context("Failed to process PSBT")?; + Psbt::from_str(&processed.psbt).context("Failed to parse processed PSBT") } /// Finalize a PSBT and extract the transaction pub fn finalize_psbt(&self, psbt: &Psbt) -> Result { - let result = self - .bitcoind - .finalize_psbt(&psbt.to_string(), Some(true)) - .context("Failed to finalize PSBT")?; - let tx = deserialize(&result.hex.ok_or_else(|| anyhow!("Incomplete PSBT"))?)?; + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async { self.rpc.finalize_psbt(&psbt.to_string(), Some(true)).await }) + }) + .context("Failed to finalize PSBT")?; + let hex_str = result.hex.ok_or_else(|| anyhow!("Incomplete PSBT"))?; + use bitcoin::hex::FromHex; + let hex_bytes = Vec::::from_hex(&hex_str).context("Failed to decode hex")?; + let tx = deserialize(&hex_bytes)?; Ok(tx) } pub fn can_broadcast(&self, tx: &Transaction) -> Result { let raw_tx = serialize_hex(&tx); - let mempool_results = self.bitcoind.test_mempool_accept(&[raw_tx])?; - match mempool_results.first() { - Some(result) => Ok(result.allowed), - None => Err(anyhow!("No mempool results returned on broadcast check",)), - } + let mempool_results = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async { self.rpc.test_mempool_accept(&[raw_tx]).await }) + })?; + + mempool_results + .first() + .map(|result| result.allowed) + .ok_or_else(|| anyhow!("No mempool results returned on broadcast check")) } /// Broadcast a raw transaction pub fn broadcast_tx(&self, tx: &Transaction) -> Result { let mut serialized_tx = Vec::new(); tx.consensus_encode(&mut serialized_tx)?; - self.bitcoind - .send_raw_transaction(&serialized_tx) - .context("Failed to broadcast transaction") + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async { self.rpc.send_raw_transaction(&serialized_tx).await }) + }) + .context("Failed to broadcast transaction") } /// Check if a script belongs to this wallet pub fn is_mine(&self, script: &Script) -> Result { if let Ok(address) = Address::from_script(script, self.network()?) { - self.bitcoind - .get_address_info(&address) - .map(|info| info.is_mine.unwrap_or(false)) - .context("Failed to get address info") + let info = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async { self.rpc.get_address_info(&address).await }) + }) + .context("Failed to get address info")?; + Ok(info.is_mine) } else { Ok(false) } @@ -132,47 +146,50 @@ impl BitcoindWallet { /// Get a new address from the wallet pub fn get_new_address(&self) -> Result
{ - self.bitcoind - .get_new_address(None, None) - .context("Failed to get new address")? - .require_network(self.network()?) - .context("Invalid network for address") + let addr = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async { self.rpc.get_new_address(None, None).await }) + }) + .context("Failed to get new address")?; + Ok(addr.assume_checked()) } /// List unspent UTXOs pub fn list_unspent(&self) -> Result> { - let unspent = self - .bitcoind - .list_unspent(None, None, None, None, None) - .context("Failed to list unspent")?; - Ok(unspent.into_iter().map(input_pair_from_list_unspent).collect()) + let unspent = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async { self.rpc.list_unspent(None, None, None, None, None).await }) + }) + .context("Failed to list unspent")?; + Ok(unspent.into_iter().map(input_pair_from_corepc).collect()) } /// Get the network this wallet is operating on pub fn network(&self) -> Result { - self.bitcoind - .get_blockchain_info() - .map_err(|_| anyhow!("Failed to get blockchain info")) - .map(|info| info.chain) + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { self.rpc.network().await }) + }) + .map_err(|_| anyhow!("Failed to get blockchain info")) } } -pub fn input_pair_from_list_unspent( - utxo: bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry, -) -> InputPair { +pub fn input_pair_from_corepc(utxo: crate::app::rpc::ListUnspentResult) -> InputPair { let psbtin = Input { // NOTE: non_witness_utxo is not necessary because bitcoin-cli always supplies // witness_utxo, even for non-witness inputs witness_utxo: Some(TxOut { - value: utxo.amount, - script_pubkey: utxo.script_pub_key.clone(), + value: Amount::from_btc(utxo.amount).expect("Valid amount"), + script_pubkey: ScriptBuf::from_hex(&utxo.script_pubkey).expect("Valid script"), }), - redeem_script: utxo.redeem_script.clone(), - witness_script: utxo.witness_script.clone(), + redeem_script: utxo + .redeem_script + .as_ref() + .map(|s| ScriptBuf::from_hex(s).expect("Valid script")), + witness_script: None, // Not available in this version ..Default::default() }; let txin = TxIn { - previous_output: OutPoint { txid: utxo.txid, vout: utxo.vout }, + previous_output: OutPoint { txid: utxo.txid.parse().expect("Valid txid"), vout: utxo.vout }, ..Default::default() }; InputPair::new(txin, psbtin, None).expect("Input pair should be valid") diff --git a/payjoin-cli/src/db/error.rs b/payjoin-cli/src/db/error.rs index 390f2cbb9..00807fc8e 100644 --- a/payjoin-cli/src/db/error.rs +++ b/payjoin-cli/src/db/error.rs @@ -1,7 +1,5 @@ use std::fmt; -#[cfg(feature = "v2")] -use bitcoincore_rpc::jsonrpc::serde_json; use payjoin::ImplementationError; use sled::Error as SledError; diff --git a/payjoin-cli/src/db/v2.rs b/payjoin-cli/src/db/v2.rs index 3eabd32fe..02ea83d2b 100644 --- a/payjoin-cli/src/db/v2.rs +++ b/payjoin-cli/src/db/v2.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use std::time::SystemTime; -use bitcoincore_rpc::jsonrpc::serde_json; use payjoin::bitcoin::hex::DisplayHex; use payjoin::persist::SessionPersister; use payjoin::receive::v2::SessionEvent as ReceiverSessionEvent; diff --git a/payjoin-cli/src/main.rs b/payjoin-cli/src/main.rs index f7119e7a7..373875aae 100644 --- a/payjoin-cli/src/main.rs +++ b/payjoin-cli/src/main.rs @@ -22,7 +22,7 @@ async fn main() -> Result<()> { let app: Box = if cli.flags.bip78.unwrap_or(false) { #[cfg(feature = "v1")] { - Box::new(crate::app::v1::App::new(config)?) + Box::new(crate::app::v1::App::new(config).await?) } #[cfg(not(feature = "v1"))] { @@ -33,7 +33,7 @@ async fn main() -> Result<()> { } else if cli.flags.bip77.unwrap_or(false) { #[cfg(feature = "v2")] { - Box::new(crate::app::v2::App::new(config)?) + Box::new(crate::app::v2::App::new(config).await?) } #[cfg(not(feature = "v2"))] { @@ -44,11 +44,11 @@ async fn main() -> Result<()> { } else { #[cfg(feature = "v2")] { - Box::new(crate::app::v2::App::new(config)?) + Box::new(crate::app::v2::App::new(config).await?) } #[cfg(all(feature = "v1", not(feature = "v2")))] { - Box::new(crate::app::v1::App::new(config)?) + Box::new(crate::app::v1::App::new(config).await?) } #[cfg(not(any(feature = "v1", feature = "v2")))] {