• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 24905054634

24 Apr 2026 06:18PM UTC coverage: 85.022% (+0.07%) from 84.953%
24905054634

Pull #808

github

web-flow
Merge 44eab44f0 into e22e37249
Pull Request #808: Enforce content-length validation on sender and size limits on payjoin-cli

73 of 82 new or added lines in 5 files covered. (89.02%)

1 existing line in 1 file now uncovered.

11410 of 13420 relevant lines covered (85.02%)

399.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.47
/payjoin-cli/src/app/mod.rs
1
use std::collections::HashMap;
2

3
#[cfg(feature = "v1")]
4
use anyhow::anyhow;
5
use anyhow::Result;
6
#[cfg(feature = "v1")]
7
use futures::{Stream, StreamExt};
8
#[cfg(feature = "v1")]
9
use hyper::body::Bytes;
10
use payjoin::bitcoin::psbt::Psbt;
11
use payjoin::bitcoin::{self, Address, Amount, FeeRate};
12
use tokio::signal;
13
use tokio::sync::watch;
14

15
pub mod config;
16
pub mod wallet;
17
use crate::app::config::Config;
18
use crate::app::wallet::BitcoindWallet;
19

20
#[cfg(feature = "v1")]
21
pub(crate) mod v1;
22
#[cfg(feature = "v2")]
23
pub(crate) mod v2;
24

25
#[async_trait::async_trait]
26
pub trait App: Send + Sync {
27
    async fn new(config: Config) -> Result<Self>
28
    where
29
        Self: Sized;
30
    fn wallet(&self) -> BitcoindWallet;
31
    async fn send_payjoin(&self, bip21: &str, fee_rate: FeeRate) -> Result<()>;
32
    async fn receive_payjoin(&self, amount: Amount) -> Result<()>;
33
    #[cfg(feature = "v2")]
34
    async fn resume_payjoins(&self) -> Result<()>;
35
    #[cfg(feature = "v2")]
36
    async fn history(&self) -> Result<()>;
37

38
    fn create_original_psbt(
4✔
39
        &self,
4✔
40
        address: &Address,
4✔
41
        amount: Amount,
4✔
42
        fee_rate: FeeRate,
4✔
43
    ) -> Result<Psbt> {
4✔
44
        // Check if wallet has spendable UTXOs before attempting to create PSBT
45
        if !self.wallet().has_spendable_utxos()? {
4✔
46
            return Err(anyhow::anyhow!(
×
47
                "No spendable UTXOs available in wallet. Please ensure your wallet has confirmed funds."
×
48
            ));
×
49
        }
4✔
50

51
        // wallet_create_funded_psbt requires a HashMap<address: String, Amount>
52
        let mut outputs = HashMap::with_capacity(1);
4✔
53
        outputs.insert(address.to_string(), amount);
4✔
54

55
        self.wallet().create_psbt(outputs, fee_rate, true)
4✔
56
    }
4✔
57

58
    fn process_pj_response(&self, psbt: Psbt) -> Result<bitcoin::Txid> {
4✔
59
        tracing::trace!("Proposed psbt: {psbt:#?}");
4✔
60

61
        let signed = self.wallet().process_psbt(&psbt)?;
4✔
62
        let tx = signed.extract_tx()?;
4✔
63

64
        let txid = self.wallet().broadcast_tx(&tx)?;
4✔
65

66
        println!("Payjoin sent. TXID: {txid}");
4✔
67
        Ok(txid)
4✔
68
    }
4✔
69
}
70

71
#[cfg(feature = "_manual-tls")]
72
fn http_agent(config: &Config) -> Result<reqwest::Client> {
9✔
73
    Ok(http_agent_builder(config.root_certificate.as_ref())?.build()?)
9✔
74
}
9✔
75

76
#[cfg(not(feature = "_manual-tls"))]
77
fn http_agent(_config: &Config) -> Result<reqwest::Client> {
78
    Ok(reqwest::Client::builder().http1_only().build()?)
79
}
80

81
#[cfg(feature = "_manual-tls")]
82
fn http_agent_builder(
9✔
83
    root_cert_path: Option<&std::path::PathBuf>,
9✔
84
) -> Result<reqwest::ClientBuilder> {
9✔
85
    let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().http1_only();
9✔
86

87
    if let Some(root_cert_path) = root_cert_path {
9✔
88
        let cert_der = std::fs::read(root_cert_path)?;
9✔
89
        builder =
9✔
90
            builder.add_root_certificate(reqwest::tls::Certificate::from_der(cert_der.as_slice())?)
9✔
91
    }
×
92
    Ok(builder)
9✔
93
}
9✔
94

95
async fn handle_interrupt(tx: watch::Sender<()>) {
13✔
96
    if let Err(e) = signal::ctrl_c().await {
13✔
97
        eprintln!("Error setting up Ctrl-C handler: {e}");
×
98
    }
8✔
99
    let _ = tx.send(());
8✔
100
}
8✔
101

102
#[cfg(feature = "v1")]
103
pub async fn read_limited_body<S, E>(mut stream: S, expected_len: usize) -> Result<Vec<u8>>
5✔
104
where
5✔
105
    S: Stream<Item = Result<Bytes, E>> + Unpin,
5✔
106
    E: std::error::Error + Send + Sync + 'static,
5✔
107
{
5✔
108
    let mut body = Vec::with_capacity(expected_len);
5✔
109

110
    while let Some(chunk) = stream.next().await {
10✔
111
        let chunk = chunk.map_err(|e| anyhow!("Error reading body chunk: {}", e))?;
5✔
112
        if body.len() + chunk.len() > expected_len {
5✔
NEW
113
            return Err(anyhow!("Body exceeds expected size of {expected_len} bytes"));
×
114
        }
5✔
115
        body.extend_from_slice(&chunk);
5✔
116
    }
117

118
    Ok(body)
5✔
119
}
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc