Skip to content

Commit 01f13ba

Browse files
authored
Merge pull request #269 from diba-io/payjoin-fix
Fix Payjoin: Add back original PSBT input to payjoin proposal
2 parents 23da169 + 79d3e8c commit 01f13ba

1 file changed

Lines changed: 81 additions & 10 deletions

File tree

src/bitcoin/payment.rs

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
use anyhow::{anyhow, Result};
2+
23
use bdk::{wallet::tx_builder::TxOrdering, FeeRate, TransactionDetails};
3-
use bitcoin::consensus::serialize;
4-
use payjoin::send::Configuration;
5-
use payjoin::{PjUri, PjUriExt};
4+
5+
use bitcoin::{
6+
consensus::serialize,
7+
psbt::{Input, Psbt},
8+
Amount, TxIn,
9+
};
10+
11+
use payjoin::{send::Configuration, PjUri, PjUriExt};
612

713
use crate::{
814
bitcoin::{
@@ -43,24 +49,57 @@ pub async fn create_payjoin(
4349
fee_rate: Option<FeeRate>,
4450
pj_uri: PjUri<'_>, // TODO specify Uri<PayJoinParams>
4551
) -> Result<TransactionDetails> {
52+
let enacted_fee_rate = fee_rate.unwrap_or_default();
4653
let (psbt, details) = {
4754
let locked_wallet = wallet.lock().await;
4855
let mut builder = locked_wallet.build_tx();
49-
for invoice in invoices {
56+
for invoice in &invoices {
5057
builder.add_recipient(invoice.address.script_pubkey(), invoice.amount);
5158
}
52-
builder.enable_rbf().fee_rate(fee_rate.unwrap_or_default());
59+
builder.enable_rbf().fee_rate(enacted_fee_rate);
5360
builder.finish()?
5461
};
5562

5663
debug!(format!("Request PayJoin transaction: {details:#?}"));
5764
debug!("Unsigned Original PSBT:", base64::encode(&serialize(&psbt)));
58-
let original_psbt = sign_original_psbt(wallet, psbt).await?;
65+
let original_psbt = sign_original_psbt(wallet, psbt.clone()).await?;
5966
info!("Original PSBT successfully signed");
6067

61-
// TODO use fee_rate
62-
let pj_params = Configuration::non_incentivizing();
63-
let (req, ctx) = pj_uri.create_pj_request(original_psbt, pj_params)?;
68+
let additional_fee_index = psbt
69+
.outputs
70+
.clone()
71+
.into_iter()
72+
.enumerate()
73+
.find(|(_, output)| {
74+
invoices.iter().all(|invoice| {
75+
output.redeem_script != Some(invoice.address.script_pubkey())
76+
&& output.witness_script != Some(invoice.address.script_pubkey())
77+
})
78+
})
79+
.map(|(i, _)| i);
80+
81+
let pj_params = match additional_fee_index {
82+
Some(index) => {
83+
let amount_available = psbt
84+
.clone()
85+
.unsigned_tx
86+
.output
87+
.get(index)
88+
.map(|o| Amount::from_sat(o.value))
89+
.unwrap_or_default();
90+
const P2TR_INPUT_WEIGHT: usize = 58; // bitmask is taproot only
91+
let recommended_fee = Amount::from_sat(enacted_fee_rate.fee_wu(P2TR_INPUT_WEIGHT));
92+
let max_additional_fee = std::cmp::min(
93+
recommended_fee,
94+
amount_available, // offer amount available if recommendation is not
95+
);
96+
97+
Configuration::with_fee_contribution(max_additional_fee, Some(index))
98+
}
99+
None => Configuration::non_incentivizing(),
100+
};
101+
102+
let (req, ctx) = pj_uri.create_pj_request(original_psbt.clone(), pj_params)?;
64103
info!("Built PayJoin request");
65104
let response = reqwest::Client::new()
66105
.post(req.url)
@@ -77,7 +116,8 @@ pub async fn create_payjoin(
77116
return Err(anyhow!("Error performing payjoin: {res}"));
78117
}
79118

80-
let payjoin_psbt = ctx.process_response(res.as_bytes())?;
119+
let payjoin_psbt = ctx.process_response(&mut res.as_bytes())?;
120+
let payjoin_psbt = add_back_original_input(&original_psbt, payjoin_psbt);
81121

82122
debug!(
83123
"Proposed PayJoin PSBT:",
@@ -88,3 +128,34 @@ pub async fn create_payjoin(
88128

89129
Ok(tx)
90130
}
131+
132+
/// Unlike Bitcoin Core's walletprocesspsbt RPC, BDK's finalize_psbt only checks
133+
/// if the script in the PSBT input map matches the descriptor and does not
134+
/// check whether it has control of the OutPoint specified in the unsigned_tx's
135+
/// TxIn. So the original_psbt input data needs to be added back into
136+
/// payjoin_psbt without overwriting receiver input.
137+
fn add_back_original_input(original_psbt: &Psbt, payjoin_psbt: Psbt) -> Psbt {
138+
// input_pairs is only used here. It may be added to payjoin, rust-bitcoin, or BDK in time.
139+
fn input_pairs(psbt: &Psbt) -> Box<dyn Iterator<Item = (TxIn, Input)> + '_> {
140+
Box::new(
141+
psbt.unsigned_tx
142+
.input
143+
.iter()
144+
.cloned() // Clone each TxIn for better ergonomics than &muts
145+
.zip(psbt.inputs.iter().cloned()), // Clone each Input too
146+
)
147+
}
148+
149+
let mut original_inputs = input_pairs(original_psbt).peekable();
150+
151+
for (proposed_txin, mut proposed_psbtin) in input_pairs(&payjoin_psbt) {
152+
if let Some((original_txin, original_psbtin)) = original_inputs.peek() {
153+
if proposed_txin.previous_output == original_txin.previous_output {
154+
proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone();
155+
proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone();
156+
}
157+
original_inputs.next();
158+
}
159+
}
160+
payjoin_psbt
161+
}

0 commit comments

Comments
 (0)