Skip to content

Commit 17c9aa2

Browse files
committed
example(payjoin): complete end-to-end implementation of payjoin v2 example
1 parent dd443d3 commit 17c9aa2

File tree

2 files changed

+172
-56
lines changed

2 files changed

+172
-56
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ std = ["miniscript/std"]
3333
[[example]]
3434
name = "synopsis"
3535

36+
[[example]]
37+
name = "payjoin_v2"
38+
3639
[[example]]
3740
name = "common"
3841
crate-type = ["lib"]

examples/payjoin_v2.rs

Lines changed: 169 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,9 @@ use bdk_tx::{
66
};
77
use bitcoin::{
88
consensus::encode::serialize_hex, key::Secp256k1, psbt, secp256k1::All, Amount, FeeRate, Psbt,
9-
Sequence, TxIn,
9+
Sequence, Transaction, TxIn,
1010
};
1111
use miniscript::{Descriptor, DescriptorPublicKey};
12-
use std::str::FromStr;
13-
use url::Url;
14-
15-
mod common;
16-
17-
use common::Wallet;
18-
1912
use payjoin::{
2013
io::fetch_ohttp_keys,
2114
persist::{NoopSessionPersister, OptionalTransitionOutcome},
@@ -26,6 +19,12 @@ use payjoin::{
2619
send::v2::SenderBuilder,
2720
ImplementationError, PjUri, Request, Uri, UriExt,
2821
};
22+
use std::str::FromStr;
23+
use url::Url;
24+
25+
mod common;
26+
27+
use common::Wallet;
2928

3029
#[tokio::main]
3130
async fn main() -> anyhow::Result<()> {
@@ -34,7 +33,7 @@ async fn main() -> anyhow::Result<()> {
3433
let payjoin_directory = Url::parse("https://payjo.in")?;
3534
let ohttp_keys = fetch_ohttp_keys(ohttp_relay.as_str(), payjoin_directory.as_str()).await?;
3635

37-
let (mut receiver_wallet, receiver_signer, env, sender_wallet, sender_signer, sender_desc) =
36+
let (mut receiver_wallet, receiver_signer, env, mut sender_wallet, sender_signer, sender_desc) =
3837
setup_wallets()?;
3938
let recv_persister = NoopSessionPersister::default();
4039
let send_persister = NoopSessionPersister::default();
@@ -63,14 +62,14 @@ async fn main() -> anyhow::Result<()> {
6362
let response_body = session
6463
.process_response(response.bytes().await?.to_vec().as_slice(), ctx)
6564
.save(&recv_persister)?;
66-
// No proposal yet since sender has not responded
65+
6766
let session = if let OptionalTransitionOutcome::Stasis(current_state) = response_body {
6867
current_state
6968
} else {
7069
panic!("Should still be in initialized state")
7170
};
7271

73-
// SENDER PARSE THE PAYJOIN URI, BUILD PSBT AND SEND PAYJOIN REQUEST
72+
// SENDER PARSE THE PAYJOIN URI, BUILD PSBT
7473
let pj_uri = Uri::from_str(&session.pj_uri().to_string())
7574
.map_err(|e| anyhow::anyhow!("{e}"))?
7675
.assume_checked()
@@ -85,36 +84,25 @@ async fn main() -> anyhow::Result<()> {
8584
&sender_desc,
8685
&secp,
8786
)?;
88-
89-
dbg!(&psbt.clone().extract_tx());
90-
9187
let req_ctx = SenderBuilder::new(psbt, pj_uri)
9288
.build_recommended(FeeRate::BROADCAST_MIN)?
9389
.save(&send_persister)?;
9490

95-
let (
96-
Request {
97-
url,
98-
body,
99-
content_type,
100-
..
101-
},
102-
send_ctx,
103-
) = req_ctx.create_v2_post_request(ohttp_relay.as_str())?;
91+
// SENDER SENDS PAYJOIN REQUEST
92+
let (req, send_ctx) = req_ctx.create_v2_post_request(ohttp_relay.as_str())?;
10493
let response = http
105-
.post(url)
106-
.header("Content-Type", content_type)
107-
.body(body)
94+
.post(req.url)
95+
.header("Content-Type", req.content_type)
96+
.body(req.body)
10897
.send()
10998
.await?;
110-
println!("Response: {response:?}");
11199

112100
assert!(
113101
response.status().is_success(),
114102
"error response: {}",
115103
response.status()
116104
);
117-
let _send_ctx = req_ctx
105+
let send_ctx = req_ctx
118106
.process_response(&response.bytes().await?, send_ctx)
119107
.save(&send_persister)?;
120108

@@ -135,12 +123,123 @@ async fn main() -> anyhow::Result<()> {
135123
_ => return Err(anyhow::anyhow!("Expected a payjoin proposal!")),
136124
};
137125

138-
let _payjoin_proposal =
126+
let payjoin_proposal =
139127
handle_directory_proposal(&receiver_wallet, &env, proposal, &receiver_signer, &secp)?;
128+
let (req, _) = payjoin_proposal.create_post_request(ohttp_relay.as_str())?;
129+
130+
let _response = http
131+
.post(req.url)
132+
.header("Content-Type", req.content_type)
133+
.body(req.body)
134+
.send()
135+
.await?;
136+
137+
// SENDER SIGNS, FINALIZES AND BROADCASTS THE PAYJOIN TRANSACTION
138+
let (
139+
Request {
140+
url,
141+
body,
142+
content_type,
143+
..
144+
},
145+
ohttp_ctx,
146+
) = send_ctx.create_poll_request(ohttp_relay.as_str())?;
147+
let response = http
148+
.post(url)
149+
.header("Content-Type", content_type)
150+
.body(body)
151+
.send()
152+
.await?;
153+
println!("Response: {:#?}", &response);
154+
let response = send_ctx
155+
.process_response(&response.bytes().await?, ohttp_ctx)
156+
.save(&send_persister)
157+
.expect("psbt should exist");
158+
159+
let checked_payjoin_proposal_psbt = if let OptionalTransitionOutcome::Progress(psbt) = response
160+
{
161+
psbt
162+
} else {
163+
panic!("psbt should exist");
164+
};
165+
let network_fees = checked_payjoin_proposal_psbt.fee()?;
166+
167+
let payjoin_tx = extract_pj_tx(
168+
&sender_wallet,
169+
checked_payjoin_proposal_psbt,
170+
&sender_signer,
171+
&secp,
172+
)?;
173+
let txid = env.rpc_client().send_raw_transaction(&payjoin_tx)?;
174+
println!("Sent: {}", txid);
175+
176+
assert_eq!(payjoin_tx.input.len(), 2);
177+
assert_eq!(payjoin_tx.output.len(), 2); // A CHANGE OUTPUT IS EXPECTED
178+
179+
// MINE A BLOCK TO CONFIRM THE TRANSACTION
180+
env.mine_blocks(1, None)?;
181+
receiver_wallet.sync(&env)?;
182+
sender_wallet.sync(&env)?;
183+
184+
// RECEIVER WALLET SHOULD NOW SEE THE TRANSACTION
185+
if let Some(tx_node) = receiver_wallet.graph.graph().get_tx(txid) {
186+
let tx = tx_node.as_ref();
187+
dbg!(tx);
188+
} else {
189+
println!("Transaction not in receiver's graph yet");
190+
}
191+
192+
assert_eq!(
193+
sender_wallet.balance().confirmed,
194+
Amount::from_btc(45.0)? - network_fees
195+
);
196+
assert_eq!(receiver_wallet.balance().confirmed, Amount::from_btc(55.0)?);
140197

141198
Ok(())
142199
}
143200

201+
fn extract_pj_tx(
202+
wallet: &Wallet,
203+
mut psbt: Psbt,
204+
signer: &Signer,
205+
secp: &Secp256k1<All>,
206+
) -> anyhow::Result<Transaction> {
207+
let assets = wallet.assets();
208+
let mut plans = Vec::new();
209+
210+
for (index, input) in psbt.unsigned_tx.input.iter().enumerate() {
211+
let outpoint = input.previous_output;
212+
213+
if let Some(plan) = wallet.plan_of_output(outpoint, &assets) {
214+
let psbt_input = &mut psbt.inputs[index];
215+
216+
// Only update if not already finalized
217+
if psbt_input.final_script_sig.is_none() && psbt_input.final_script_witness.is_none() {
218+
plan.update_psbt_input(psbt_input);
219+
220+
if let Some(prev_tx) = wallet.graph.graph().get_tx(outpoint.txid) {
221+
psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone());
222+
if let Some(txout) = prev_tx.output.get(outpoint.vout as usize) {
223+
psbt_input.witness_utxo = Some(txout.clone());
224+
}
225+
}
226+
}
227+
228+
plans.push((outpoint, plan));
229+
}
230+
}
231+
232+
let finalizer = Finalizer::new(plans);
233+
let _ = psbt.sign(signer, secp);
234+
let finalize_map = finalizer.finalize(&mut psbt);
235+
236+
if !finalize_map.is_finalized() {
237+
return Err(anyhow!("Failed to finalize PSBT: {finalize_map:?}"));
238+
}
239+
240+
Ok(psbt.extract_tx()?)
241+
}
242+
144243
fn handle_directory_proposal(
145244
wallet: &Wallet,
146245
env: &TestEnv,
@@ -202,16 +301,20 @@ fn handle_directory_proposal(
202301

203302
let payjoin = payjoin
204303
.apply_fee_range(
205-
Some(FeeRate::from_sat_per_vb_unchecked(1)),
304+
Some(FeeRate::BROADCAST_MIN),
206305
Some(FeeRate::from_sat_per_vb_unchecked(2)),
207306
)
208307
.save(&noop_persister)?;
209308

210-
//Sign anf finalize proposal PSBT
309+
// Sign and finalize proposal PSBT
211310
let payjoin = payjoin
212311
.finalize_proposal(|psbt: &Psbt| {
213-
finalize_psbt(psbt, wallet, signer, secp)
214-
.map_err(|e| ImplementationError::from(e.to_string().as_str()))
312+
let mut psbt = psbt.clone();
313+
314+
finalize_psbt(&mut psbt, wallet, signer, secp)
315+
.map_err(|e| ImplementationError::from(e.to_string().as_str()))?;
316+
317+
Ok(psbt)
215318
})
216319
.save(&noop_persister)?;
217320

@@ -229,8 +332,8 @@ fn build_psbt(
229332
let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?;
230333

231334
let target_amount = Amount::from_btc(5.0)?;
232-
let target_feerate = FeeRate::from_sat_per_vb_unchecked(5);
233-
// let longterm_feerate = FeeRate::from_sat_per_vb_unchecked(1);
335+
let target_feerate = FeeRate::from_sat_per_vb_unchecked(2);
336+
let longterm_feerate = FeeRate::from_sat_per_vb_unchecked(1);
234337

235338
let target_outputs = vec![Output::with_script(
236339
pj_uri.address.script_pubkey(),
@@ -242,12 +345,16 @@ fn build_psbt(
242345
.regroup(group_by_spk())
243346
.filter(filter_unspendable_now(tip_height, tip_time))
244347
.into_selection(
245-
|selector| selector.select_until_target_met(),
348+
|selector| -> anyhow::Result<()> {
349+
selector.select_all();
350+
Ok(())
351+
},
352+
// selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000),
246353
SelectorParams::new(
247354
target_feerate,
248355
target_outputs,
249356
ScriptSource::Descriptor(Box::new(desc.at_derivation_index(0)?)),
250-
ChangePolicyType::NoDust,
357+
ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
251358
wallet.change_weight(),
252359
),
253360
)?;
@@ -269,13 +376,11 @@ fn build_psbt(
269376
}
270377

271378
fn finalize_psbt(
272-
psbt: &Psbt,
379+
psbt: &mut Psbt,
273380
wallet: &Wallet,
274381
signer: &Signer,
275382
secp: &Secp256k1<All>,
276-
) -> anyhow::Result<Psbt> {
277-
let mut psbt = psbt.clone();
278-
383+
) -> anyhow::Result<()> {
279384
let assets = wallet.assets();
280385
let mut plans = Vec::new();
281386

@@ -288,13 +393,9 @@ fn finalize_psbt(
288393

289394
let finalizer = Finalizer::new(plans);
290395
let _ = psbt.sign(signer, secp);
291-
let _finalize_map = finalizer.finalize(&mut psbt);
396+
finalizer.finalize(psbt);
292397

293-
// if !finalize_map.is_finalized() {
294-
// return Err(anyhow!("Failed to finalize PSBT: {:?}", res));
295-
// }
296-
297-
Ok(psbt)
398+
Ok(())
298399
}
299400

300401
fn select_inputs(
@@ -303,34 +404,46 @@ fn select_inputs(
303404
env: &TestEnv,
304405
) -> anyhow::Result<Vec<InputPair>> {
305406
let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?;
407+
let assets = wallet.assets();
306408

307409
let candidates = wallet
308410
.all_candidates()
309411
.filter(|input| input.is_spendable_now(tip_height, tip_time));
310412

311413
let inputs = candidates
312414
.inputs()
313-
.map(|input| {
415+
.filter_map(|input| {
416+
let outpoint = input.prev_outpoint();
417+
let plan = wallet.plan_of_output(outpoint, &assets)?;
418+
314419
let txin = TxIn {
315-
previous_output: input.prev_outpoint(),
316-
sequence: input.sequence().unwrap_or(Sequence::ENABLE_RBF_NO_LOCKTIME),
420+
previous_output: outpoint,
421+
// sequence: input.sequence().unwrap_or(Sequence::ENABLE_RBF_NO_LOCKTIME),
317422
..Default::default()
318423
};
319424

320-
let psbt_input = psbt::Input {
425+
let mut psbt_input = psbt::Input {
321426
witness_utxo: Some(input.prev_txout().clone()),
427+
// non_witness_utxo: input.prev_tx().cloned(),
322428
..Default::default()
323429
};
324-
InputPair::new(txin, psbt_input, None)
325-
.map_err(|e| anyhow!("Failed to create InputPair: {e:?}"))
430+
431+
// Update PSBT input with plan information
432+
plan.update_psbt_input(&mut psbt_input);
433+
434+
InputPair::new(txin, psbt_input, None).ok()
326435
})
327-
.collect::<Result<Vec<_>, _>>()?;
436+
.collect::<Vec<_>>();
437+
438+
if inputs.is_empty() {
439+
return Err(anyhow!("No suitable inputs available"));
440+
}
328441

329-
let selected_inputs = payjoin
442+
let selected_input = payjoin
330443
.try_preserving_privacy(inputs)
331444
.map_err(|e| anyhow!("Failed to make privacy preserving selection: {e:?}"))?;
332445

333-
Ok(vec![selected_inputs])
446+
Ok(vec![selected_input])
334447
}
335448

336449
// SETUP WALLET

0 commit comments

Comments
 (0)