Skip to content

Replace bitcoincore-rpc with custom reqwest client#945

Merged
spacebear21 merged 1 commit intopayjoin:masterfrom
bc1cindy:replace-bitcoincore-rpc-350
Aug 14, 2025
Merged

Replace bitcoincore-rpc with custom reqwest client#945
spacebear21 merged 1 commit intopayjoin:masterfrom
bc1cindy:replace-bitcoincore-rpc-350

Conversation

@bc1cindy
Copy link
Contributor

@bc1cindy bc1cindy commented Aug 9, 2025

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 #350

@bc1cindy bc1cindy marked this pull request as ready for review August 9, 2025 21:50
@bc1cindy bc1cindy force-pushed the replace-bitcoincore-rpc-350 branch 2 times, most recently from 5940110 to a94498d Compare August 10, 2025 12:11
Copy link
Collaborator

@spacebear21 spacebear21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking this on @bc1cindy!

Concept ACK, there are some unrelated formatting changes that should be dropped (see below), and @zealsham 's comment about the cookie file is still outstanding, but it looks great otherwise.

I would like to hear @nothingmuch 's thoughts about the runtime blocker approach. IIRC we used a similar approach in the Boltz POC since they had an async RPC client and our state machine only supports sync.

@bc1cindy bc1cindy force-pushed the replace-bitcoincore-rpc-350 branch from a94498d to d3449be Compare August 11, 2025 13:43
@bc1cindy
Copy link
Contributor Author

@spacebear21 @zealsham Thank you for the reviews!

I've addressed both issues in the latest commit (d3449be):

  1. Cookie caching: Fixed - credentials are now read once in AsyncBitcoinRpc::new() and stored as struct fields (username/password), eliminating file I/O on every RPC call.
  2. Formatting: Reverted - removed the unrelated formatting change in mod.rs line 62

Regarding the runtime blocker approach using tokio::runtime::Handle::block_on - this is indeed the same pattern used in the Boltz POC. It allows us to bridge the async RPC client with the sync state machine without requiring a major refactor. Happy to discuss alternative approaches if @nothingmuch has other suggestions.

The rustup show output confirms I'm using the correct nightly toolchain.

@zealsham
Copy link
Collaborator

Thank you @bc1cindy . we'll have to wait for @nothingmuch to take look before it gets merge

@arminsabouri
Copy link
Collaborator

arminsabouri commented Aug 12, 2025

@bc1cindy have you considered using an off the shelf async client?
I've used bitcoind-async-client in some other projects. Looks like they are also using reqwest.

@bc1cindy
Copy link
Contributor Author

@bc1cindy have you considered using an off the shelf async client? I've used bitcoind-async-client in some other projects. Looks like they are also using reqwest.

@arminsabouri Thanks for the suggestion! I did evaluate bitcoind-async-client but found it doesn't meet our requirements:

  1. PSBT support: The library lacks wallet_create_funded_psbt, wallet_process_psbt, and finalize_psbt , essential for Payjoin functionality
  2. Requires Bitcoin Core 28.0+ while we support v26
  3. Missing RPC methods: No get_address_info or list_unspent implementations

Our custom implementation:

  • Provides exactly the 9 RPC methods we need
  • Maintains Bitcoin Core v26 compatibility
  • Supports both cookie and user/pass auth
  • Uses the same reqwest dependency (avoiding extra deps)
  • Preserves the existing sync API through wrappers

Adding bitcoind-async-client would require forking it to add missing methods, essentially creating another custom implementation. The current approach is simpler and more maintainable for our specific needs.

Happy to discuss further if you see any concerns with this approach or have other suggestions!

@arminsabouri
Copy link
Collaborator

arminsabouri commented Aug 12, 2025

@bc1cindy have you considered using an off the shelf async client? I've used bitcoind-async-client in some other projects. Looks like they are also using reqwest.

@arminsabouri Thanks for the suggestion! I did evaluate bitcoind-async-client but found it doesn't meet our requirements:

  1. PSBT support: The library lacks wallet_create_funded_psbt, wallet_process_psbt, and finalize_psbt , essential for Payjoin functionality
    ...
  1. Missing RPC methods: No get_address_info or list_unspent implementations

Yes, you are correct. It is missing some methods we need. However, we can upstream the changes and that way the entire rust Bitcoin ecosystem benefits. And a we finally get an fully working async. client that many projects in this space need! In my experience the maintainers of bitcoind-async-client are quite responsive and looking for these kinds of contributions. Its a young library. It was created a couple months ago.

  1. Requires Bitcoin Core 28.0+ while we support v26

Where is this requirement coming from? AFAICT we don't explicitly pin payjoin-cli to a particular bitcoin-core version. Payjoin-cli is a refrence implementation not a production implementation. The aim is to demonstrate to application devs what RPC methods they may need to call to do Payjoins. However, rust-payjoin is block source agnostic. Applications may chose to use Bitcoin core v28, v26, electrum, mempool.space, etc...

You have done a great job creating a super light weight async. client so I dont want to block this PR but I do think we should weight the trade-offs here a bit more.

As an aside, given that this is a refrence impl does such a low MSRV even make sense for rust-payjoin-cli? Could we be potentially making life harder for our selves?

Copy link
Collaborator

@nothingmuch nothingmuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cACK, the code is nice and readable.

in line with Armin's suggestion, i don't think we support core v26 for any particular reason other than it was the recent version at the time the repo started, so maybe it still make sense to PR upstream to add support for the methods we require and still use the existing crate rather than a custom implementation.

and the same kinda goes for payjoin-cli's MSRV, the important crate to preserve MSRV for is payjoin, see #774 for some discussion about that

i think my preference would be to address the minor feedback and merge this, use this as motivational springboard to address our async callback story, and in time see if we can remove it if/when bitcoind-async-client gets all the functionality we need

@bc1cindy bc1cindy force-pushed the replace-bitcoincore-rpc-350 branch 2 times, most recently from b89d2e3 to 3e43c85 Compare August 12, 2025 21:27
@bc1cindy
Copy link
Contributor Author

@arminsabouri @nothingmuch Thank you both for the thoughtful reviews and guidance!

I've implemented all the requested feedback from @nothingmuch:

  • Made AsyncBitcoinRpc::new() async and switched to tokio::fs::read_to_string().await
  • Used fully-qualified serde_json::Value for better readability
  • Fixed import formatting to use use payjoin::bitcoin::{Amount, FeeRate};
  • Applied nightly rustfmt formatting
  • Implemented NetworkUnchecked suggestion: get_new_address() now returns Address<NetworkUnchecked> for better future-proofing, with assume_checked() called at the wallet layer

On the Bitcoin Core version discussion: You're absolutely right that I was conflating the corepc-types::v26 with a hard requirement. Thank you for the clarification!

On bitcoind-async-client and ecosystem contributions: I completely agree with your long-term vision. As @nothingmuch suggested, let's use this as motivation to improve async story. I'm committed to contributing the missing PSBT methods (wallet_create_funded_psbt, wallet_process_psbt, finalize_psbt) plus get_address_info and list_unspent to bitcoind-async-client as a follow-up effort.

@DanGould
Copy link
Contributor

DanGould commented Aug 13, 2025

@nothingmuch

and the same kinda goes for payjoin-cli's MSRV, the important crate to preserve MSRV for is payjoin, see https://github.com/orgs/payjoin/discussions/774 for some discussion about that

I disagree somewhat. In order to show payjoin works with a given MSRV, don't we need to show that the rust ecosystem has tooling that makes it possible to actually implement with a reference to the same MSRV? That was my thinking when setting MSRV for payjoin-cli in the first place.

Copy link
Collaborator

@spacebear21 spacebear21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was unable to run this locally due to nested tokio runtimes:

cargo run -- receive 10000
     Running `/Users/spacebear/Projects/rust-payjoin/target/debug/payjoin-cli receive 10000`

thread 'main' panicked at payjoin-cli/src/app/wallet.rs:38:28:
Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

@bc1cindy bc1cindy force-pushed the replace-bitcoincore-rpc-350 branch 3 times, most recently from f20f42e to 6c46c84 Compare August 13, 2025 19:23
@bc1cindy
Copy link
Contributor Author

I was unable to run this locally due to nested tokio runtimes:

cargo run -- receive 10000
     Running `/Users/spacebear/Projects/rust-payjoin/target/debug/payjoin-cli receive 10000`

thread 'main' panicked at payjoin-cli/src/app/wallet.rs:38:28:
Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

@spacebear21 Thanks for catching the nested runtime error! I've fixed it by making the wallet initialization async and using futures::executor::block_on instead of tokio::runtime::Handle::block_on. Ready for re-review!

@spacebear21
Copy link
Collaborator

I've fixed it by making the wallet initialization async and using futures::executor::block_on instead of tokio::runtime::Handle::block_on.

Cool, this is in line with @nothingmuch 's suggestion.

I'm still getting an error related to the bitcoin RPC auth:

❯ cargo run -- receive 10000
   Compiling payjoin-cli v0.2.0 (/Users/spacebear/Projects/rust-payjoin/payjoin-cli)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.88s
     Running `/Users/spacebear/Projects/rust-payjoin/target/debug/payjoin-cli receive 10000`
Error: Failed to connect to bitcoind. Check config RPC connection.

Caused by:
    Failed to get blockchain info

My config.toml:

[bitcoind]
rpcuser="payjoin"
rpcpassword="payjoin"
rpchost="http://localhost:18443/wallet/receiver"

My bitcoin.conf:

server=1
rpcuser=payjoin
rpcpassword=payjoin

@spacebear21
Copy link
Collaborator

spacebear21 commented Aug 13, 2025

Btw I ran cargo fmt locally with nightly and it applied formatting changes that aren't included in this PR, can you ensure these are applied in the next push? See detailed diff below.

Details diff --git a/payjoin-cli/src/app/wallet.rs b/payjoin-cli/src/app/wallet.rs index 526a284d..ca35ac6e 100644 --- a/payjoin-cli/src/app/wallet.rs +++ b/payjoin-cli/src/app/wallet.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc;
-use crate::app::rpc::{AsyncBitcoinRpc, Auth};
 use anyhow::{anyhow, Context, Result};
 use payjoin::bitcoin::consensus::encode::{deserialize, serialize_hex};
 use payjoin::bitcoin::consensus::Encodable;
@@ -14,6 +13,8 @@ use payjoin::bitcoin::{
 use payjoin::receive::InputPair;
 use serde_json::json;
 
+use crate::app::rpc::{AsyncBitcoinRpc, Auth};
+
 /// Implementation of PayjoinWallet for bitcoind using async RPC client
 #[derive(Clone)]
 pub struct BitcoindWallet {
@@ -23,11 +24,10 @@ pub struct BitcoindWallet {
 impl BitcoindWallet {
     pub async fn new(config: &crate::app::config::BitcoindConfig) -> Result<Self> {
         let auth = match &config.cookie {
-            Some(cookie) if cookie.as_os_str().is_empty() => {
+            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) => Auth::CookieFile(cookie.into()),
             None => Auth::UserPass(config.rpcuser.clone(), config.rpcpassword.clone()),
         };
@@ -58,22 +58,19 @@ impl BitcoindWallet {
 
         // Sync wrapper around async call using futures executor
         let result = futures::executor::block_on(async {
-            self.rpc.wallet_create_funded_psbt(
-                &[], // inputs
-                &outputs,
-                None, // locktime
-                Some(options),
-                None,
-            ).await
+            self.rpc
+                .wallet_create_funded_psbt(
+                    &[], // inputs
+                    &outputs,
+                    None, // locktime
+                    Some(options),
+                    None,
+                )
+                .await
         })?;
 
         let processed = futures::executor::block_on(async {
-            self.rpc.wallet_process_psbt(
-                &result.psbt,
-                None,
-                None,
-                None,
-            ).await
+            self.rpc.wallet_process_psbt(&result.psbt, None, None, None).await
         })?;
 
         Psbt::from_str(&processed.psbt).context("Failed to load PSBT from base64")
@@ -86,7 +83,8 @@ impl BitcoindWallet {
         let psbt_str = psbt.to_string();
         let processed = futures::executor::block_on(async {
             self.rpc.wallet_process_psbt(&psbt_str, None, None, Some(false)).await
-        }).context("Failed to process PSBT")?;
+        })
+        .context("Failed to process PSBT")?;
         Psbt::from_str(&processed.psbt).context("Failed to parse processed PSBT")
     }
 
@@ -94,7 +92,8 @@ impl BitcoindWallet {
     pub fn finalize_psbt(&self, psbt: &Psbt) -> Result<Transaction> {
         let result = futures::executor::block_on(async {
             self.rpc.finalize_psbt(&psbt.to_string(), Some(true)).await
-        }).context("Failed to finalize PSBT")?;
+        })
+        .context("Failed to finalize PSBT")?;
         let hex_str = result.hex.ok_or_else(|| anyhow!("Incomplete PSBT"))?;
         use bitcoin::hex::FromHex;
         let hex_bytes = Vec::<u8>::from_hex(&hex_str).context("Failed to decode hex")?;
@@ -104,17 +103,15 @@ impl BitcoindWallet {
 
     pub fn can_broadcast(&self, tx: &Transaction) -> Result<bool> {
         let raw_tx = serialize_hex(&tx);
-        let mempool_results = futures::executor::block_on(async {
-            self.rpc.test_mempool_accept(&[raw_tx]).await
-        })?;
+        let mempool_results =
+            futures::executor::block_on(async { self.rpc.test_mempool_accept(&[raw_tx]).await })?;
         match mempool_results.first() {
-            Some(result) => {
+            Some(result) =>
                 if let Some(first_result) = result.results.first() {
                     Ok(first_result.allowed)
                 } else {
                     Ok(false)
-                }
-            }
+                },
             None => Err(anyhow!("No mempool results returned on broadcast check",)),
         }
     }
@@ -123,17 +120,16 @@ impl BitcoindWallet {
     pub fn broadcast_tx(&self, tx: &Transaction) -> Result<Txid> {
         let mut serialized_tx = Vec::new();
         tx.consensus_encode(&mut serialized_tx)?;
-        futures::executor::block_on(async {
-            self.rpc.send_raw_transaction(&serialized_tx).await
-        }).context("Failed to broadcast transaction")
+        futures::executor::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<bool> {
         if let Ok(address) = Address::from_script(script, self.network()?) {
-            let info = futures::executor::block_on(async {
-                self.rpc.get_address_info(&address).await
-            }).context("Failed to get address info")?;
+            let info =
+                futures::executor::block_on(async { self.rpc.get_address_info(&address).await })
+                    .context("Failed to get address info")?;
             Ok(info.is_mine)
         } else {
             Ok(false)
@@ -142,9 +138,9 @@ impl BitcoindWallet {
 
     /// Get a new address from the wallet
     pub fn get_new_address(&self) -> Result<Address> {
-        let addr = futures::executor::block_on(async {
-            self.rpc.get_new_address(None, None).await
-        }).context("Failed to get new address")?;
+        let addr =
+            futures::executor::block_on(async { self.rpc.get_new_address(None, None).await })
+                .context("Failed to get new address")?;
         Ok(addr.assume_checked())
     }
 
@@ -152,15 +148,16 @@ impl BitcoindWallet {
     pub fn list_unspent(&self) -> Result<Vec<InputPair>> {
         let unspent = futures::executor::block_on(async {
             self.rpc.list_unspent(None, None, None, None, None).await
-        }).context("Failed to list unspent")?;
+        })
+        .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<Network> {
-        futures::executor::block_on(async {
-            self.rpc.network().await
-        }).map_err(|_| anyhow!("Failed to get blockchain info"))
+        futures::executor::block_on(async { self.rpc.network().await })
+            .map_err(|_| anyhow!("Failed to get blockchain info"))
     }
 }

@bc1cindy bc1cindy force-pushed the replace-bitcoincore-rpc-350 branch 2 times, most recently from 6d8f429 to 04b1a10 Compare August 13, 2025 22:14
@bc1cindy
Copy link
Contributor Author

I've fixed it by making the wallet initialization async and using futures::executor::block_on instead of tokio::runtime::Handle::block_on.

Cool, this is in line with @nothingmuch 's suggestion.

I'm still getting an error related to the bitcoin RPC auth:

❯ cargo run -- receive 10000
   Compiling payjoin-cli v0.2.0 (/Users/spacebear/Projects/rust-payjoin/payjoin-cli)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.88s
     Running `/Users/spacebear/Projects/rust-payjoin/target/debug/payjoin-cli receive 10000`
Error: Failed to connect to bitcoind. Check config RPC connection.

Caused by:
    Failed to get blockchain info

My config.toml:

[bitcoind]
rpcuser="payjoin"
rpcpassword="payjoin"
rpchost="http://localhost:18443/wallet/receiver"

My bitcoin.conf:

server=1
rpcuser=payjoin
rpcpassword=payjoin

@spacebear21 Fixed all issues!

What was wrong:

  1. Nested runtime: Was using tokio::runtime::Handle::block_on inside async context
  2. RPC auth error: getblockchaininfo was being called on /wallet/receiver URL instead of base URL
  3. Version compatibility: corepc_types::v26 structs incompatible with Bitcoin Core v29

Fixes applied:

  1. Using futures::executor::block_on to avoid nested runtime
  2. Added smart routing in call_rpc() - blockchain methods go to base URL, wallet methods stay on wallet URL
  3. Changed get_blockchain_info() to return serde_json::Value for version-agnostic compatibility
  4. All rustfmt formatting applied

Tested locally with your exact config and works perfectly now!

Ready for re-review.

@spacebear21
Copy link
Collaborator

The receiver is now running, thank you for your perseverance @bc1cindy.

However there is an issue with the sender:

❯ export BIP21="bitcoin:bcrt1qp3th53uvrlxldw3jl2w7cmz60rpagh4a2h0qt9?amount=0.0001&pjos=0&pj=HTTPS://PAYJO.IN/NE23M2C4XC4RK%23EX1895FU6Q-OH1QYP87E2AVMDKXDTU6R25WCPQ5ZUF02XHNPA65JMD8ZA2W4YRQN6UUWG-RK1QGQEAXYYXUYT458RR3RJDQVCHGZSSS7KKCVZ0ECE3AWH4ULL7QM0Q"
❯ cargo run -- send $BIP21 --fee-rate=1
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
     Running `/Users/spacebear/Projects/rust-payjoin/target/debug/payjoin-cli send 'bitcoin:bcrt1qp3th53uvrlxldw3jl2w7cmz60rpagh4a2h0qt9?amount=0.0001&pjos=0&pj=HTTPS://PAYJO.IN/NE23M2C4XC4RK%23EX1895FU6Q-OH1QYP87E2AVMDKXDTU6R25WCPQ5ZUF02XHNPA65JMD8ZA2W4YRQN6UUWG-RK1QGQEAXYYXUYT458RR3RJDQVCHGZSSS7KKCVZ0ECE3AWH4ULL7QM0Q' --fee-rate=1`
Error: RPC error: RpcError { code: -4, message: "Insufficient funds" }

This is probably due to:

    pub async fn wallet_create_funded_psbt(
        &self,
        inputs: &[Value],
        outputs: &HashMap<String, Amount>,
        locktime: Option<u32>,
        options: Option<Value>,
        bip32derivs: Option<bool>,
    ) -> Result<WalletCreateFundedPsbt> {
        let params = json!([inputs, outputs, locktime, options, bip32derivs]);
        self.call_rpc("walletcreatefundedpsbt", params).await

My guess is the outputs Amount gets coerced to JSON as a u64 representing the amount in sats, but the RPC expects an amount in BTC as a float/string (this makes me sad...). We need to ensure the params are properly converted in all these function calls - it may help to look at what bitcoincore-rpc did internally.

Sidenote: I just realized the CI required approval to run which explains why these issues weren't being flagged in CI. Please refer to the failing checks for formatting/Cargo.lock issues.

@bc1cindy bc1cindy force-pushed the replace-bitcoincore-rpc-350 branch from 04b1a10 to 7ea44ba Compare August 14, 2025 01:44
@bc1cindy
Copy link
Contributor Author

The receiver is now running, thank you for your perseverance @bc1cindy.

However there is an issue with the sender:

❯ export BIP21="bitcoin:bcrt1qp3th53uvrlxldw3jl2w7cmz60rpagh4a2h0qt9?amount=0.0001&pjos=0&pj=HTTPS://PAYJO.IN/NE23M2C4XC4RK%23EX1895FU6Q-OH1QYP87E2AVMDKXDTU6R25WCPQ5ZUF02XHNPA65JMD8ZA2W4YRQN6UUWG-RK1QGQEAXYYXUYT458RR3RJDQVCHGZSSS7KKCVZ0ECE3AWH4ULL7QM0Q"
❯ cargo run -- send $BIP21 --fee-rate=1
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
     Running `/Users/spacebear/Projects/rust-payjoin/target/debug/payjoin-cli send 'bitcoin:bcrt1qp3th53uvrlxldw3jl2w7cmz60rpagh4a2h0qt9?amount=0.0001&pjos=0&pj=HTTPS://PAYJO.IN/NE23M2C4XC4RK%23EX1895FU6Q-OH1QYP87E2AVMDKXDTU6R25WCPQ5ZUF02XHNPA65JMD8ZA2W4YRQN6UUWG-RK1QGQEAXYYXUYT458RR3RJDQVCHGZSSS7KKCVZ0ECE3AWH4ULL7QM0Q' --fee-rate=1`
Error: RPC error: RpcError { code: -4, message: "Insufficient funds" }

This is probably due to:

    pub async fn wallet_create_funded_psbt(
        &self,
        inputs: &[Value],
        outputs: &HashMap<String, Amount>,
        locktime: Option<u32>,
        options: Option<Value>,
        bip32derivs: Option<bool>,
    ) -> Result<WalletCreateFundedPsbt> {
        let params = json!([inputs, outputs, locktime, options, bip32derivs]);
        self.call_rpc("walletcreatefundedpsbt", params).await

My guess is the outputs Amount gets coerced to JSON as a u64 representing the amount in sats, but the RPC expects an amount in BTC as a float/string (this makes me sad...). We need to ensure the params are properly converted in all these function calls - it may help to look at what bitcoincore-rpc did internally.

Sidenote: I just realized the CI required approval to run which explains why these issues weren't being flagged in CI. Please refer to the failing checks for formatting/Cargo.lock issues.

@spacebear21 Issue resolved, thanks!

You were absolutely correct about the Amount serialization problem. I've fixed the "Insufficient funds" error by implementing proper Amount→BTC conversion in wallet_create_funded_psbt().

What was wrong:

  • Amount was being serialized as u64 (satoshis): 10000
  • But RPC expects f64 (BTC): 0.0001
  • This made Bitcoin Core think we needed 10000 BTC instead of 0.0001 BTC

Fix applied:
// Convert amounts to BTC for RPC call (RPC expects BTC, not satoshis)
let outputs_btc: HashMap<String, f64> = outputs
.iter()
.map(|(addr, amount)| (addr.clone(), amount.to_btc()))
.collect();

Tested with your exact command:
❯ cargo run --features v1 -- --bip78 send 'bitcoin:bcrt1qp3th53uvrlxldw3jl2w7cmz60rpagh4a2h0qt9?amount=0.0001&...' --fee-rate=1
Sent fallback transaction txid: 1a72272a09e931d70d5a9f7fb8c40f490e81f95c0a1e4f9dd0f4b1920d1ad8bc
Transaction output: 0.00010000 BTC (correct amount)

Also fixed all CI issues:

  • Applied cargo fmt formatting
  • Fixed v1 feature conditional imports in tests
  • 125 tests passing locally

Ready for re-review! The send command should work perfectly now with funded wallets.

@bc1cindy
Copy link
Contributor Author

bc1cindy commented Aug 14, 2025

@nothingmuch

and the same kinda goes for payjoin-cli's MSRV, the important crate to preserve MSRV for is payjoin, see https://github.com/orgs/payjoin/discussions/774 for some discussion about that

I disagree somewhat. In order to show payjoin works with a given MSRV, don't we need to show that the rust ecosystem has tooling that makes it possible to actually implement with a reference to the same MSRV? That was my thinking when setting MSRV for payjoin-cli in the first place.

@DanGould Thanks for the MSRV feedback!

I'm currently working on alpenlabs/bitcoind-async-client#32 to add the missing PSBT methods to bitcoind-async-client:

  • wallet_create_funded_psbt - Creates funded PSBTs with configurable options
  • wallet_process_psbt - Signs PSBTs with wallet keys
  • finalize_psbt - Finalizes PSBTs and extracts transactions
  • get_address_info - Provides address validation and ownership information
  • list_unspent_with_options - Enhanced UTXO listing with filtering capabilities
  • plus psbtbumpfee - PSBT fee bumping

I'm happy to contribute the MSRV compatibility changes to bitcoind-async-client if the maintainers are interested, or alternatively we could use a fork with the necessary adjustments.

For now, the custom solution works and maintains our MSRV requirements.

Let me know which direction you'd prefer, I'm glad to help with whatever compatibility work is needed.

@storopoli
Copy link

As the lead maintained of bitcoind-async-client, I'm totally fine with moving it to a sane MSRV and having the rust-bitcoin approach to Cargo-{minimal,recent}.lock.

@spacebear21 spacebear21 force-pushed the replace-bitcoincore-rpc-350 branch 2 times, most recently from 7d73bfa to da659fb Compare August 14, 2025 10:15
@spacebear21
Copy link
Collaborator

Getting closer! There is one more error when the receiver is processing the sender request:

❯ cargo run -- receive 10000
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `/Users/spacebear/Projects/rust-payjoin/target/debug/payjoin-cli receive 10000`
Bootstrapping private network transport over Oblivious HTTP
Receive session established
Request Payjoin by sharing this Payjoin Uri:
bitcoin:bcrt1qy9cfjyrhtzvltutwuxl5fgdex2vaq0thaagp2u?amount=0.0001&pjos=0&pj=HTTPS://PAYJO.IN/3WEGCG4W4JS45%23EX1RQPF76Q-OH1QYP87E2AVMDKXDTU6R25WCPQ5ZUF02XHNPA65JMD8ZA2W4YRQN6UUWG-RK1Q20QFJYAV7PVXMSQHCDDGEDD7PY7M8W
XPLJP2SPHCVT5NP9P0MNSW
Polling receive request...
Got a request from the sender. Responding with a Payjoin proposal.
Error: Transient error: Internal Server Error: Failed to parse RPC response

@spacebear21 spacebear21 mentioned this pull request Aug 14, 2025
@bc1cindy bc1cindy force-pushed the replace-bitcoincore-rpc-350 branch from da659fb to 3a01eb1 Compare August 14, 2025 14:25
@bc1cindy
Copy link
Contributor Author

Getting closer! There is one more error when the receiver is processing the sender request:

❯ cargo run -- receive 10000
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `/Users/spacebear/Projects/rust-payjoin/target/debug/payjoin-cli receive 10000`
Bootstrapping private network transport over Oblivious HTTP
Receive session established
Request Payjoin by sharing this Payjoin Uri:
bitcoin:bcrt1qy9cfjyrhtzvltutwuxl5fgdex2vaq0thaagp2u?amount=0.0001&pjos=0&pj=HTTPS://PAYJO.IN/3WEGCG4W4JS45%23EX1RQPF76Q-OH1QYP87E2AVMDKXDTU6R25WCPQ5ZUF02XHNPA65JMD8ZA2W4YRQN6UUWG-RK1Q20QFJYAV7PVXMSQHCDDGEDD7PY7M8W
XPLJP2SPHCVT5NP9P0MNSW
Polling receive request...
Got a request from the sender. Responding with a Payjoin proposal.
Error: Transient error: Internal Server Error: Failed to parse RPC response

Thanks @spacebear21 for the force push and helping with the formatting!

I've now fixed the RPC compatibility issue you reported. The problem was that Bitcoin Core's testmempoolaccept returns a different format than what corepc-types expected, plus Bitcoin Core rejects null values for optional parameters.

Fixed:

  • Created custom TestMempoolAcceptResult struct that matches Bitcoin Core's actual response
  • Added proper default values for RPC parameters (null → sensible defaults)
  • Fixed sighash type ("DEFAULT" → "ALL")

All e2e tests are now passing! Both sh contrib/test_local.sh and manual CLI commands work perfectly.

The "Failed to parse RPC response" error is resolved and payjoin functionality works end-to-end for both v1 and v2.

@nothingmuch
Copy link
Collaborator

I disagree somewhat. In order to show payjoin works with a given MSRV, don't we need to show that the rust ecosystem has tooling that makes it possible to actually implement with a reference to the same MSRV? That was my thinking when setting MSRV for payjoin-cli in the first place.

Isn't it sufficiently demonstrated by the integration tests?

I agree that it's nice to have as low an MSRV as practical, but not at the expense of maintainability if the integration tests can do the job of proving it works with less of a burden

@DanGould
Copy link
Contributor

Looks like our MSRV problem can be resolved since the thing keeping us from upgrading to 1.85, debian stable's MSRV, has been released. see #954

@bc1cindy bc1cindy force-pushed the replace-bitcoincore-rpc-350 branch 6 times, most recently from aae50ea to 908ca28 Compare August 14, 2025 21:39
@spacebear21 spacebear21 force-pushed the replace-bitcoincore-rpc-350 branch from 908ca28 to 50dc10c Compare August 14, 2025 21:42
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 payjoin#350
@bc1cindy
Copy link
Contributor Author

Thanks @spacebear21! All checks passing now.

Related: my async RPC client PR was merged alpenlabs/bitcoind-async-client#32

This should complement the RPC improvements in this project

@spacebear21 spacebear21 force-pushed the replace-bitcoincore-rpc-350 branch from 50dc10c to a5a4bdf Compare August 14, 2025 22:00
Copy link
Collaborator

@spacebear21 spacebear21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK a5a4bdf

Confirmed with a smoke test locally and pushed formatting changes. Thanks @bc1cindy for persevering and all who helped review!

@spacebear21 spacebear21 merged commit 9ca2231 into payjoin:master Aug 14, 2025
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The bitcoincore-rpc dependency has problems in our specific use case

7 participants