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

payjoin / rust-payjoin / 13350991407

16 Feb 2025 02:43AM UTC coverage: 79.468% (+0.2%) from 79.269%
13350991407

Pull #538

github

web-flow
Merge 0e46f80b5 into 2627ef20f
Pull Request #538: Make payjoin-cli v1 / v2 features additive

363 of 422 new or added lines in 6 files covered. (86.02%)

2 existing lines in 1 file now uncovered.

4122 of 5187 relevant lines covered (79.47%)

887.41 hits per line

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

94.12
/payjoin-cli/src/app/wallet.rs
1
use std::collections::HashMap;
2
use std::str::FromStr;
3
use std::sync::Arc;
4

5
use anyhow::{anyhow, Context, Result};
6
use bitcoincore_rpc::json::WalletCreateFundedPsbtOptions;
7
use bitcoincore_rpc::{Auth, Client, RpcApi};
8
use payjoin::bitcoin::consensus::encode::{deserialize, serialize_hex};
9
use payjoin::bitcoin::consensus::Encodable;
10
use payjoin::bitcoin::psbt::{Input, Psbt};
11
use payjoin::bitcoin::{
12
    Address, Amount, Denomination, FeeRate, Network, OutPoint, Script, Transaction, TxIn, TxOut,
13
    Txid,
14
};
15
use payjoin::receive::InputPair;
16

17
/// Implementation of PayjoinWallet for bitcoind
18
#[derive(Clone, Debug)]
19
pub struct BitcoindWallet {
20
    pub bitcoind: std::sync::Arc<Client>,
21
}
22

23
impl BitcoindWallet {
24
    pub fn new(config: &crate::app::config::BitcoindConfig) -> Result<Self> {
8✔
25
        let client = match &config.cookie {
8✔
26
            Some(cookie) => Client::new(config.rpchost.as_str(), Auth::CookieFile(cookie.into())),
8✔
NEW
27
            None => Client::new(
×
NEW
28
                config.rpchost.as_str(),
×
NEW
29
                Auth::UserPass(config.rpcuser.clone(), config.rpcpassword.clone()),
×
NEW
30
            ),
×
NEW
31
        }?;
×
32
        Ok(Self { bitcoind: Arc::new(client) })
8✔
33
    }
8✔
34
}
35

36
impl BitcoindWallet {
37
    /// Create a PSBT with the given outputs and fee rate
38
    pub fn create_psbt(
3✔
39
        &self,
3✔
40
        outputs: HashMap<String, Amount>,
3✔
41
        fee_rate: FeeRate,
3✔
42
        lock_unspent: bool,
3✔
43
    ) -> Result<Psbt> {
3✔
44
        let fee_sat_per_kvb =
3✔
45
            fee_rate.to_sat_per_kwu().checked_mul(4).ok_or_else(|| anyhow!("Invalid fee rate"))?;
3✔
46
        let fee_per_kvb = Amount::from_sat(fee_sat_per_kvb);
3✔
47
        log::debug!("Fee rate sat/kvb: {}", fee_per_kvb.display_in(Denomination::Satoshi));
3✔
48

49
        let options = WalletCreateFundedPsbtOptions {
3✔
50
            lock_unspent: Some(lock_unspent),
3✔
51
            fee_rate: Some(fee_per_kvb),
3✔
52
            ..Default::default()
3✔
53
        };
3✔
54

55
        let psbt = self
3✔
56
            .bitcoind
3✔
57
            .wallet_create_funded_psbt(
3✔
58
                &[], // inputs
3✔
59
                &outputs,
3✔
60
                None, // locktime
3✔
61
                Some(options),
3✔
62
                None,
3✔
63
            )
3✔
64
            .context("Failed to create PSBT")?
3✔
65
            .psbt;
66

67
        let psbt = self
3✔
68
            .bitcoind
3✔
69
            .wallet_process_psbt(&psbt, None, None, None)
3✔
70
            .context("Failed to process PSBT")?
3✔
71
            .psbt;
72

73
        Psbt::from_str(&psbt).context("Failed to load PSBT from base64")
3✔
74
    }
3✔
75

76
    /// Process a PSBT, validating and signing inputs owned by this wallet
77
    ///
78
    /// Does not include bip32 derivations in the PSBT
79
    pub fn process_psbt(&self, psbt: &Psbt) -> Result<Psbt> {
6✔
80
        let psbt_str = psbt.to_string();
6✔
81
        let processed = self
6✔
82
            .bitcoind
6✔
83
            .wallet_process_psbt(&psbt_str, None, None, Some(false))
6✔
84
            .context("Failed to process PSBT")?
6✔
85
            .psbt;
86
        Psbt::from_str(&processed).context("Failed to parse processed PSBT")
6✔
87
    }
6✔
88

89
    /// Finalize a PSBT and extract the transaction
90
    pub fn finalize_psbt(&self, psbt: &Psbt) -> Result<Transaction> {
3✔
91
        let result = self
3✔
92
            .bitcoind
3✔
93
            .finalize_psbt(&psbt.to_string(), Some(true))
3✔
94
            .context("Failed to finalize PSBT")?;
3✔
95
        let tx = deserialize(&result.hex.ok_or_else(|| anyhow!("Incomplete PSBT"))?)?;
3✔
96
        Ok(tx)
3✔
97
    }
3✔
98

99
    pub fn can_broadcast(&self, tx: &Transaction) -> Result<bool> {
3✔
100
        let raw_tx = serialize_hex(&tx);
3✔
101
        let mempool_results = self.bitcoind.test_mempool_accept(&[raw_tx])?;
3✔
102
        match mempool_results.first() {
3✔
103
            Some(result) => Ok(result.allowed),
3✔
NEW
104
            None => Err(anyhow!("No mempool results returned on broadcast check",)),
×
105
        }
106
    }
3✔
107

108
    /// Broadcast a raw transaction
109
    pub fn broadcast_tx(&self, tx: &Transaction) -> Result<Txid> {
3✔
110
        let mut serialized_tx = Vec::new();
3✔
111
        tx.consensus_encode(&mut serialized_tx)?;
3✔
112
        self.bitcoind
3✔
113
            .send_raw_transaction(&serialized_tx)
3✔
114
            .context("Failed to broadcast transaction")
3✔
115
    }
3✔
116

117
    /// Check if a script belongs to this wallet
118
    pub fn is_mine(&self, script: &Script) -> Result<bool> {
9✔
119
        if let Ok(address) = Address::from_script(script, self.network()?) {
9✔
120
            self.bitcoind
9✔
121
                .get_address_info(&address)
9✔
122
                .map(|info| info.is_mine.unwrap_or(false))
9✔
123
                .context("Failed to get address info")
9✔
124
        } else {
NEW
125
            Ok(false)
×
126
        }
127
    }
9✔
128

129
    /// Get a new address from the wallet
130
    pub fn get_new_address(&self) -> Result<Address> {
5✔
131
        self.bitcoind
5✔
132
            .get_new_address(None, None)
5✔
133
            .context("Failed to get new address")?
5✔
134
            .require_network(self.network()?)
5✔
135
            .context("Invalid network for address")
5✔
136
    }
5✔
137

138
    /// List unspent UTXOs
139
    pub fn list_unspent(&self) -> Result<Vec<InputPair>> {
3✔
140
        let unspent = self
3✔
141
            .bitcoind
3✔
142
            .list_unspent(None, None, None, None, None)
3✔
143
            .context("Failed to list unspent")?;
3✔
144
        Ok(unspent.into_iter().map(input_pair_from_list_unspent).collect())
3✔
145
    }
3✔
146

147
    /// Get the network this wallet is operating on
148
    pub fn network(&self) -> Result<Network> {
22✔
149
        self.bitcoind
22✔
150
            .get_blockchain_info()
22✔
151
            .map_err(|_| anyhow!("Failed to get blockchain info"))
22✔
152
            .map(|info| info.chain)
22✔
153
    }
22✔
154
}
155

156
pub fn input_pair_from_list_unspent(
3✔
157
    utxo: bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry,
3✔
158
) -> InputPair {
3✔
159
    let psbtin = Input {
3✔
160
        // NOTE: non_witness_utxo is not necessary because bitcoin-cli always supplies
3✔
161
        // witness_utxo, even for non-witness inputs
3✔
162
        witness_utxo: Some(TxOut {
3✔
163
            value: utxo.amount,
3✔
164
            script_pubkey: utxo.script_pub_key.clone(),
3✔
165
        }),
3✔
166
        redeem_script: utxo.redeem_script.clone(),
3✔
167
        witness_script: utxo.witness_script.clone(),
3✔
168
        ..Default::default()
3✔
169
    };
3✔
170
    let txin = TxIn {
3✔
171
        previous_output: OutPoint { txid: utxo.txid, vout: utxo.vout },
3✔
172
        ..Default::default()
3✔
173
    };
3✔
174
    InputPair::new(txin, psbtin).expect("Input pair should be valid")
3✔
175
}
3✔
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