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

payjoin / rust-payjoin / 20621567323

31 Dec 2025 03:03PM UTC coverage: 82.903%. Remained the same
20621567323

Pull #1224

github

web-flow
Merge 2c07c0f48 into 013615b16
Pull Request #1224: Adding flake check and weekly flake.lock maintence job in git workflow

12 of 13 new or added lines in 4 files covered. (92.31%)

24 existing lines in 3 files now uncovered.

9669 of 11663 relevant lines covered (82.9%)

450.42 hits per line

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

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

4
use anyhow::{anyhow, Context, Result};
5
use bitcoind_async_client::corepc_types::model::ListUnspentItem;
6
use bitcoind_async_client::traits::{Broadcaster, Reader, Signer, Wallet};
7
use bitcoind_async_client::types::{CreateRawTransactionOutput, WalletCreateFundedPsbtOptions};
8
use bitcoind_async_client::{Auth, Client as AsyncBitcoinRpc};
9
use payjoin::bitcoin::psbt::{Input, Psbt};
10
use payjoin::bitcoin::{
11
    Address, Amount, FeeRate, Network, OutPoint, Script, Transaction, TxIn, TxOut, Txid,
12
};
13
use payjoin::receive::InputPair;
14

15
/// Implementation of PayjoinWallet for bitcoind using async RPC client
16
#[derive(Clone)]
17
pub struct BitcoindWallet {
18
    rpc: Arc<AsyncBitcoinRpc>,
19
}
20

21
impl BitcoindWallet {
22
    pub async fn new(config: &crate::app::config::BitcoindConfig) -> Result<Self> {
13✔
23
        let auth = match &config.cookie {
13✔
24
            Some(cookie) if cookie.as_os_str().is_empty() =>
13✔
25
                return Err(anyhow!(
×
26
                    "Cookie authentication enabled but no cookie path provided in config.toml"
×
UNCOV
27
                )),
×
28
            Some(cookie) => Auth::CookieFile(cookie.into()),
13✔
UNCOV
29
            None => Auth::UserPass(config.rpcuser.clone(), config.rpcpassword.clone()),
×
30
        };
31

32
        let rpc = AsyncBitcoinRpc::new(config.rpchost.to_string(), auth, None, None, None)?;
13✔
33

34
        Ok(Self { rpc: Arc::new(rpc) })
13✔
35
    }
13✔
36
}
37

38
impl BitcoindWallet {
39
    /// Create a PSBT with the given outputs and fee rate
40
    pub fn create_psbt(
4✔
41
        &self,
4✔
42
        outputs: HashMap<String, Amount>,
4✔
43
        fee_rate: FeeRate,
4✔
44
        lock_unspent: bool,
4✔
45
    ) -> Result<Psbt> {
4✔
46
        let fee_sat_per_vb = fee_rate.to_sat_per_vb_ceil();
4✔
47
        tracing::debug!("Fee rate sat/vb: {}", fee_sat_per_vb);
4✔
48

49
        let options = WalletCreateFundedPsbtOptions {
4✔
50
            fee_rate: Some(fee_sat_per_vb as f64),
4✔
51
            lock_unspents: Some(lock_unspent),
4✔
52
            replaceable: None,
4✔
53
            conf_target: None,
4✔
54
        };
4✔
55

56
        // Sync wrapper around async call - use tokio handle to avoid deadlock
57
        let result = tokio::task::block_in_place(|| {
4✔
58
            tokio::runtime::Handle::current().block_on(async {
4✔
59
                self.rpc
4✔
60
                    .wallet_create_funded_psbt(
4✔
61
                        &[], // inputs
4✔
62
                        &outputs
4✔
63
                            .iter()
4✔
64
                            .map(|(k, v)| CreateRawTransactionOutput::AddressAmount {
4✔
65
                                address: k.clone(),
4✔
66
                                amount: v.to_btc(),
4✔
67
                            })
4✔
68
                            .collect::<Vec<_>>(),
4✔
69
                        None, // locktime
4✔
70
                        Some(options),
4✔
71
                        None,
4✔
72
                    )
73
                    .await
4✔
74
            })
4✔
75
        })?;
4✔
76

77
        let processed = tokio::task::block_in_place(|| {
4✔
78
            tokio::runtime::Handle::current().block_on(async {
4✔
79
                self.rpc.wallet_process_psbt(&result.psbt.to_string(), None, None, None).await
4✔
80
            })
4✔
81
        })?
4✔
82
        .psbt;
83

84
        Ok(processed)
4✔
85
    }
4✔
86

87
    /// Process a PSBT, validating and signing inputs owned by this wallet
88
    ///
89
    /// Does not include bip32 derivations in the PSBT
90
    pub fn process_psbt(&self, psbt: &Psbt) -> Result<Psbt> {
8✔
91
        let psbt_str = psbt.to_string();
8✔
92
        let processed = tokio::task::block_in_place(|| {
8✔
93
            tokio::runtime::Handle::current().block_on(async {
8✔
94
                self.rpc.wallet_process_psbt(&psbt_str, Some(true), None, None).await
8✔
95
            })
8✔
96
        })?;
8✔
97
        Ok(processed.psbt)
8✔
98
    }
8✔
99

100
    pub fn can_broadcast(&self, tx: &Transaction) -> Result<bool> {
4✔
101
        let mempool_results = tokio::task::block_in_place(|| {
4✔
102
            tokio::runtime::Handle::current()
4✔
103
                .block_on(async { self.rpc.test_mempool_accept(tx).await })
4✔
104
        })?;
4✔
105

106
        mempool_results
4✔
107
            .results
4✔
108
            .first()
4✔
109
            .map(|result| result.reject_reason.is_none())
4✔
110
            .ok_or_else(|| anyhow!("No mempool results returned on broadcast check"))
4✔
111
    }
4✔
112

113
    /// Broadcast a raw transaction
114
    pub fn broadcast_tx(&self, tx: &Transaction) -> Result<Txid> {
4✔
115
        tokio::task::block_in_place(|| {
4✔
116
            tokio::runtime::Handle::current()
4✔
117
                .block_on(async { self.rpc.send_raw_transaction(tx).await })
4✔
118
        })
4✔
119
        .context("Failed to broadcast transaction")
4✔
120
    }
4✔
121

122
    /// Check if a script belongs to this wallet
123
    pub fn is_mine(&self, script: &Script) -> Result<bool> {
12✔
124
        if let Ok(address) = Address::from_script(script, self.network()?) {
12✔
125
            let info = tokio::task::block_in_place(|| {
12✔
126
                tokio::runtime::Handle::current()
12✔
127
                    .block_on(async { self.rpc.get_address_info(&address).await })
12✔
128
            })
12✔
129
            .context("Failed to get address info")?;
12✔
130
            Ok(info.is_mine)
12✔
131
        } else {
UNCOV
132
            Ok(false)
×
133
        }
134
    }
12✔
135

136
    #[cfg(feature = "v2")]
137
    pub fn get_raw_transaction(
1✔
138
        &self,
1✔
139
        txid: &Txid,
1✔
140
    ) -> Result<Option<payjoin::bitcoin::Transaction>> {
1✔
141
        let raw_tx = tokio::task::block_in_place(|| {
1✔
142
            tokio::runtime::Handle::current().block_on(async {
1✔
143
                match self.rpc.get_transaction(txid).await {
1✔
144
                    Ok(rpc_res) => Ok(Some(rpc_res.tx)),
1✔
145
                    Err(e) =>
×
146
                        if e.is_tx_not_found() {
×
UNCOV
147
                            Ok(None)
×
148
                        } else {
UNCOV
149
                            Err(e)
×
150
                        },
151
                }
152
            })
1✔
153
        })?;
1✔
154
        Ok(raw_tx)
1✔
155
    }
1✔
156

157
    /// Get a new address from the wallet
158
    pub fn get_new_address(&self) -> Result<Address> {
7✔
159
        let addr = tokio::task::block_in_place(|| {
7✔
160
            tokio::runtime::Handle::current().block_on(async { self.rpc.get_new_address().await })
7✔
161
        })
7✔
162
        .context("Failed to get new address")?;
7✔
163
        Ok(addr)
7✔
164
    }
7✔
165

166
    /// List unspent UTXOs
167
    pub fn list_unspent(&self) -> Result<Vec<InputPair>> {
8✔
168
        let unspent = tokio::task::block_in_place(|| {
8✔
169
            tokio::runtime::Handle::current()
8✔
170
                .block_on(async { self.rpc.list_unspent(None, None, None, None, None).await })
8✔
171
        })
8✔
172
        .context("Failed to list unspent")?;
8✔
173
        Ok(unspent.0.into_iter().map(input_pair_from_corepc).collect())
8✔
174
    }
8✔
175

176
    /// Check if wallet has any spendable UTXOs
177
    pub fn has_spendable_utxos(&self) -> Result<bool> {
4✔
178
        let unspent = self.list_unspent()?;
4✔
179
        Ok(!unspent.is_empty())
4✔
180
    }
4✔
181

182
    /// Get the network this wallet is operating on
183
    pub fn network(&self) -> Result<Network> {
25✔
184
        tokio::task::block_in_place(|| {
25✔
185
            tokio::runtime::Handle::current().block_on(async { self.rpc.network().await })
25✔
186
        })
25✔
187
        .map_err(|_| anyhow!("Failed to get blockchain info"))
25✔
188
    }
25✔
189
}
190

191
pub fn input_pair_from_corepc(utxo: ListUnspentItem) -> InputPair {
8✔
192
    let psbtin = Input {
8✔
193
        // NOTE: non_witness_utxo is not necessary because bitcoin-cli always supplies
8✔
194
        // witness_utxo, even for non-witness inputs
8✔
195
        witness_utxo: Some(TxOut {
8✔
196
            value: Amount::from_btc(utxo.amount.to_btc()).expect("Valid amount"),
8✔
197
            script_pubkey: utxo.script_pubkey,
8✔
198
        }),
8✔
199
        redeem_script: None,
8✔
200
        witness_script: None, // Not available in this version
8✔
201
        ..Default::default()
8✔
202
    };
8✔
203
    let txin = TxIn {
8✔
204
        previous_output: OutPoint { txid: utxo.txid, vout: utxo.vout },
8✔
205
        ..Default::default()
8✔
206
    };
8✔
207
    InputPair::new(txin, psbtin, None).expect("Input pair should be valid")
8✔
208
}
8✔
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