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

payjoin / rust-payjoin / 13504401239

24 Feb 2025 05:47PM UTC coverage: 79.107% (+0.1%) from 79.002%
13504401239

Pull #546

github

web-flow
Merge a95291157 into 3b95ff295
Pull Request #546: fix: corrected the pjos parameter

15 of 17 new or added lines in 1 file covered. (88.24%)

14 existing lines in 3 files now uncovered.

4055 of 5126 relevant lines covered (79.11%)

897.47 hits per line

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

98.88
/payjoin-cli/src/app/mod.rs
1
use std::collections::HashMap;
2
use std::str::FromStr;
3

4
use anyhow::{anyhow, Context, Result};
5
use bitcoin::psbt::Input as PsbtInput;
6
use bitcoin::TxIn;
7
use bitcoincore_rpc::bitcoin::Amount;
8
use bitcoincore_rpc::RpcApi;
9
use payjoin::bitcoin::psbt::Psbt;
10
use payjoin::bitcoin::FeeRate;
11
use payjoin::receive::InputPair;
12
use payjoin::{bitcoin, PjUri};
13
use tokio::signal;
14
use tokio::sync::watch;
15

16
pub mod config;
17
use crate::app::config::AppConfig;
18

19
#[cfg(all(not(feature = "v2"), feature = "v1"))]
20
pub(crate) mod v1;
21
#[cfg(feature = "v2")]
22
pub(crate) mod v2;
23

24
#[cfg(feature = "_danger-local-https")]
25
pub const LOCAL_CERT_FILE: &str = "localhost.der";
26

27
#[async_trait::async_trait]
28
pub trait App {
29
    fn new(config: AppConfig) -> Result<Self>
30
    where
31
        Self: Sized;
32
    fn bitcoind(&self) -> Result<bitcoincore_rpc::Client>;
33
    async fn send_payjoin(&self, bip21: &str, fee_rate: FeeRate) -> Result<()>;
34
    async fn receive_payjoin(self, amount: Amount) -> Result<()>;
35

36
    fn create_original_psbt(&self, uri: &PjUri, fee_rate: FeeRate) -> Result<Psbt> {
2✔
37
        let amount = uri.amount.ok_or_else(|| anyhow!("please specify the amount in the Uri"))?;
2✔
38

39
        // wallet_create_funded_psbt requires a HashMap<address: String, Amount>
40
        let mut outputs = HashMap::with_capacity(1);
2✔
41
        outputs.insert(uri.address.to_string(), amount);
2✔
42
        let fee_sat_per_kvb =
2✔
43
            fee_rate.to_sat_per_kwu().checked_mul(4).ok_or(anyhow!("Invalid fee rate"))?;
2✔
44
        let fee_per_kvb = Amount::from_sat(fee_sat_per_kvb);
2✔
45
        log::debug!("Fee rate sat/kvb: {}", fee_per_kvb.display_in(bitcoin::Denomination::Satoshi));
2✔
46
        let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions {
2✔
47
            lock_unspent: Some(true),
2✔
48
            fee_rate: Some(fee_per_kvb),
2✔
49
            ..Default::default()
2✔
50
        };
2✔
51
        let psbt = self
2✔
52
            .bitcoind()?
2✔
53
            .wallet_create_funded_psbt(
2✔
54
                &[], // inputs
2✔
55
                &outputs,
2✔
56
                None, // locktime
2✔
57
                Some(options),
2✔
58
                None,
2✔
59
            )
2✔
60
            .context("Failed to create PSBT")?
2✔
61
            .psbt;
62
        let psbt = self
2✔
63
            .bitcoind()?
2✔
64
            .wallet_process_psbt(&psbt, None, None, None)
2✔
65
            .with_context(|| "Failed to process PSBT")?
2✔
66
            .psbt;
67
        let psbt = Psbt::from_str(&psbt).with_context(|| "Failed to load PSBT from base64")?;
2✔
68
        log::debug!("Original psbt: {:#?}", psbt);
2✔
69
        Ok(psbt)
2✔
70
    }
2✔
71

72
    fn process_pj_response(&self, psbt: Psbt) -> Result<bitcoin::Txid> {
2✔
73
        log::debug!("Proposed psbt: {:#?}", psbt);
2✔
74
        let psbt = self
2✔
75
            .bitcoind()?
2✔
76
            .wallet_process_psbt(&psbt.to_string(), None, None, None)
2✔
77
            .with_context(|| "Failed to process PSBT")?
2✔
78
            .psbt;
79
        let tx = self
2✔
80
            .bitcoind()?
2✔
81
            .finalize_psbt(&psbt, Some(true))
2✔
82
            .with_context(|| "Failed to finalize PSBT")?
2✔
83
            .hex
84
            .ok_or_else(|| anyhow!("Incomplete PSBT"))?;
2✔
85
        let txid = self
2✔
86
            .bitcoind()?
2✔
87
            .send_raw_transaction(&tx)
2✔
88
            .with_context(|| "Failed to send raw transaction")?;
2✔
89
        println!("Payjoin sent. TXID: {}", txid);
2✔
90
        Ok(txid)
2✔
91
    }
2✔
92
}
93

94
#[cfg(feature = "_danger-local-https")]
95
fn http_agent() -> Result<reqwest::Client> { Ok(http_agent_builder()?.build()?) }
9✔
96

97
#[cfg(not(feature = "_danger-local-https"))]
98
fn http_agent() -> Result<reqwest::Client> { Ok(reqwest::Client::new()) }
99

100
#[cfg(feature = "_danger-local-https")]
101
fn http_agent_builder() -> Result<reqwest::ClientBuilder> {
9✔
102
    use rustls::pki_types::CertificateDer;
103
    use rustls::RootCertStore;
104

105
    let cert_der = read_local_cert()?;
9✔
106
    let mut root_cert_store = RootCertStore::empty();
9✔
107
    root_cert_store.add(CertificateDer::from(cert_der.as_slice()))?;
9✔
108
    Ok(reqwest::ClientBuilder::new()
9✔
109
        .use_rustls_tls()
9✔
110
        .add_root_certificate(reqwest::tls::Certificate::from_der(cert_der.as_slice())?))
9✔
111
}
9✔
112

113
#[cfg(feature = "_danger-local-https")]
114
fn read_local_cert() -> Result<Vec<u8>> {
9✔
115
    let mut local_cert_path = std::env::temp_dir();
9✔
116
    local_cert_path.push(LOCAL_CERT_FILE);
9✔
117
    Ok(std::fs::read(local_cert_path)?)
9✔
118
}
9✔
119

120
pub fn input_pair_from_list_unspent(
2✔
121
    utxo: bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry,
2✔
122
) -> InputPair {
2✔
123
    let psbtin = PsbtInput {
2✔
124
        // NOTE: non_witness_utxo is not necessary because bitcoin-cli always supplies
2✔
125
        // witness_utxo, even for non-witness inputs
2✔
126
        witness_utxo: Some(bitcoin::TxOut {
2✔
127
            value: utxo.amount,
2✔
128
            script_pubkey: utxo.script_pub_key.clone(),
2✔
129
        }),
2✔
130
        redeem_script: utxo.redeem_script.clone(),
2✔
131
        witness_script: utxo.witness_script.clone(),
2✔
132
        ..Default::default()
2✔
133
    };
2✔
134
    let txin = TxIn {
2✔
135
        previous_output: bitcoin::OutPoint { txid: utxo.txid, vout: utxo.vout },
2✔
136
        ..Default::default()
2✔
137
    };
2✔
138
    InputPair::new(txin, psbtin).expect("Input pair should be valid")
2✔
139
}
2✔
140

141
async fn handle_interrupt(tx: watch::Sender<()>) {
6✔
142
    if let Err(e) = signal::ctrl_c().await {
6✔
UNCOV
143
        eprintln!("Error setting up Ctrl-C handler: {}", e);
×
144
    }
5✔
145
    let _ = tx.send(());
5✔
146
}
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