Skip to content

Commit 3c8f7bf

Browse files
committed
Merge remote-tracking branch 'dangould/payjoin-fix' into payjoin-fix
2 parents b3a82ad + 7b5aa38 commit 3c8f7bf

3 files changed

Lines changed: 84 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ miniscript_crate = { package = "miniscript", version = "9.0.1", features = [
6060
] }
6161
nostr-sdk = "0.22.0"
6262
once_cell = "1.17.1"
63-
payjoin = { version = "0.7.0", features = ["sender"] }
63+
payjoin = { version = "0.8.0", features = ["send"] }
6464
percent-encoding = "2.2.0"
6565
postcard = { version = "1.0.4", features = ["alloc"] }
6666
pretty_env_logger = "0.5.0"

src/bitcoin/payment.rs

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +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::{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};
512

613
use crate::{
714
bitcoin::{
@@ -42,24 +49,57 @@ pub async fn create_payjoin(
4249
fee_rate: Option<FeeRate>,
4350
pj_uri: PjUri<'_>, // TODO specify Uri<PayJoinParams>
4451
) -> Result<TransactionDetails> {
52+
let enacted_fee_rate = fee_rate.unwrap_or_default();
4553
let (psbt, details) = {
4654
let locked_wallet = wallet.lock().await;
4755
let mut builder = locked_wallet.build_tx();
48-
for invoice in invoices {
56+
for invoice in &invoices {
4957
builder.add_recipient(invoice.address.script_pubkey(), invoice.amount);
5058
}
51-
builder.enable_rbf().fee_rate(fee_rate.unwrap_or_default());
59+
builder.enable_rbf().fee_rate(enacted_fee_rate);
5260
builder.finish()?
5361
};
5462

5563
debug!(format!("Request PayJoin transaction: {details:#?}"));
5664
debug!("Unsigned Original PSBT:", base64::encode(&serialize(&psbt)));
57-
let original_psbt = sign_original_psbt(wallet, psbt).await?;
65+
let original_psbt = sign_original_psbt(wallet, psbt.clone()).await?;
5866
info!("Original PSBT successfully signed");
5967

60-
// TODO use fee_rate
61-
let pj_params = payjoin::sender::Configuration::non_incentivizing();
62-
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)?;
63103
info!("Built PayJoin request");
64104
let response = reqwest::Client::new()
65105
.post(req.url)
@@ -76,7 +116,8 @@ pub async fn create_payjoin(
76116
return Err(anyhow!("Error performing payjoin: {res}"));
77117
}
78118

79-
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);
80121

81122
debug!(
82123
"Proposed PayJoin PSBT:",
@@ -87,3 +128,34 @@ pub async fn create_payjoin(
87128

88129
Ok(tx)
89130
}
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)