-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathpayment.rs
More file actions
161 lines (140 loc) · 5.55 KB
/
payment.rs
File metadata and controls
161 lines (140 loc) · 5.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use anyhow::{anyhow, Result};
use bdk::{wallet::tx_builder::TxOrdering, FeeRate, TransactionDetails};
use bitcoin::{
consensus::serialize,
psbt::{Input, Psbt},
Amount, TxIn,
};
use payjoin::{send::Configuration, PjUri, PjUriExt};
use crate::{
bitcoin::{
psbt::{sign_original_psbt, sign_psbt},
wallet::MemoryWallet,
},
debug, info,
structs::SatsInvoice,
};
pub async fn create_transaction(
invoices: Vec<SatsInvoice>,
wallet: &MemoryWallet,
fee_rate: Option<FeeRate>,
) -> Result<TransactionDetails> {
let (psbt, details) = {
let locked_wallet = wallet.lock().await;
let mut builder = locked_wallet.build_tx();
for invoice in invoices {
builder.add_recipient(invoice.address.script_pubkey(), invoice.amount);
}
builder.ordering(TxOrdering::Untouched); // TODO: Remove after implementing wallet persistence
builder.enable_rbf().fee_rate(fee_rate.unwrap_or_default());
builder.finish()?
};
debug!(format!("Create transaction: {details:#?}"));
debug!("Unsigned PSBT:", base64::encode(&serialize(&psbt)));
let details = sign_psbt(wallet, psbt).await?;
info!("PSBT successfully signed");
Ok(details)
}
pub async fn create_payjoin(
invoices: Vec<SatsInvoice>,
wallet: &MemoryWallet,
fee_rate: Option<FeeRate>,
pj_uri: PjUri<'_>, // TODO specify Uri<PayJoinParams>
) -> Result<TransactionDetails> {
let enacted_fee_rate = fee_rate.unwrap_or_default();
let (psbt, details) = {
let locked_wallet = wallet.lock().await;
let mut builder = locked_wallet.build_tx();
for invoice in &invoices {
builder.add_recipient(invoice.address.script_pubkey(), invoice.amount);
}
builder.enable_rbf().fee_rate(enacted_fee_rate);
builder.finish()?
};
debug!(format!("Request PayJoin transaction: {details:#?}"));
debug!("Unsigned Original PSBT:", base64::encode(&serialize(&psbt)));
let original_psbt = sign_original_psbt(wallet, psbt.clone()).await?;
info!("Original PSBT successfully signed");
let additional_fee_index = psbt
.outputs
.clone()
.into_iter()
.enumerate()
.find(|(_, output)| {
invoices.iter().all(|invoice| {
output.redeem_script != Some(invoice.address.script_pubkey())
&& output.witness_script != Some(invoice.address.script_pubkey())
})
})
.map(|(i, _)| i);
let pj_params = match additional_fee_index {
Some(index) => {
let amount_available = psbt
.clone()
.unsigned_tx
.output
.get(index)
.map(|o| Amount::from_sat(o.value))
.unwrap_or_default();
const P2TR_INPUT_WEIGHT: usize = 58; // bitmask is taproot only
let recommended_fee = Amount::from_sat(enacted_fee_rate.fee_wu(P2TR_INPUT_WEIGHT));
let max_additional_fee = std::cmp::min(
recommended_fee,
amount_available, // offer amount available if recommendation is not
);
Configuration::with_fee_contribution(max_additional_fee, Some(index))
}
None => Configuration::non_incentivizing(),
};
let (req, ctx) = pj_uri.create_pj_request(original_psbt.clone(), pj_params)?;
info!("Built PayJoin request");
let response = reqwest::Client::new()
.post(req.url)
.header("Content-Type", "text/plain")
.body(reqwest::Body::from(req.body))
.send()
.await?;
info!("Got PayJoin response");
let res = response.text().await?;
info!(format!("Response: {res}"));
if res.contains("errorCode") {
return Err(anyhow!("Error performing payjoin: {res}"));
}
let payjoin_psbt = ctx.process_response(&mut res.as_bytes())?;
let payjoin_psbt = add_back_original_input(&original_psbt, payjoin_psbt);
debug!(
"Proposed PayJoin PSBT:",
base64::encode(&serialize(&payjoin_psbt))
);
// sign_psbt also broadcasts;
let tx = sign_psbt(wallet, payjoin_psbt).await?;
Ok(tx)
}
/// Unlike Bitcoin Core's walletprocesspsbt RPC, BDK's finalize_psbt only checks
/// if the script in the PSBT input map matches the descriptor and does not
/// check whether it has control of the OutPoint specified in the unsigned_tx's
/// TxIn. So the original_psbt input data needs to be added back into
/// payjoin_psbt without overwriting receiver input.
fn add_back_original_input(original_psbt: &Psbt, payjoin_psbt: Psbt) -> Psbt {
// input_pairs is only used here. It may be added to payjoin, rust-bitcoin, or BDK in time.
fn input_pairs(psbt: &Psbt) -> Box<dyn Iterator<Item = (TxIn, Input)> + '_> {
Box::new(
psbt.unsigned_tx
.input
.iter()
.cloned() // Clone each TxIn for better ergonomics than &muts
.zip(psbt.inputs.iter().cloned()), // Clone each Input too
)
}
let mut original_inputs = input_pairs(original_psbt).peekable();
for (proposed_txin, mut proposed_psbtin) in input_pairs(&payjoin_psbt) {
if let Some((original_txin, original_psbtin)) = original_inputs.peek() {
if proposed_txin.previous_output == original_txin.previous_output {
proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone();
proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone();
}
original_inputs.next();
}
}
payjoin_psbt
}