[WIP] Payjoin receiver - #2011
Conversation
6c93104 to
943eb03
Compare
f24e841 to
00c9c51
Compare
b6b17ff to
f53d60a
Compare
d0f60d0 to
da7ff64
Compare
xstoicunicornx
left a comment
There was a problem hiding this comment.
I know this is still a WIP and my comments may be a bit premature but just wanted to brain dump some of the notes I took as part of the BOSS Payjoin Showcase session.
Some additional general comments:
- should amount be added to URI?
- should adjusting expiration will be added to settings?
- when I ran this PR Liana panicked with this error when I tried to send a payjoin to the receiver:
2026-04-09T16:19:40.194941Z ERROR liana_gui:82: panic occurred at line 508 of file lianad/src/database/sqlite/mod.rs: Some("database must be available: SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some(\"table payjoin_outpoints has no column named added_at\"))") 0: backtrace::backtrace::libunwind::trace - in the GUI when payjoin is listed under previously generated addresses it shows the address rather than URI (I'm guessing this is related to this comment)
| /* Payjoin OHttpKeys */ | ||
| CREATE TABLE payjoin_ohttp_keys ( | ||
| id INTEGER PRIMARY KEY NOT NULL, | ||
| relay_url TEXT UNIQUE NOT NULL, |
There was a problem hiding this comment.
Should this be the payjoin directory url instead? Aren't OHttpKeys specific to a payjoin directory, not relay?
If sopayjoin_save_ohttp_keys and payjoin_get_ohttp_keys also would need to be updated.
There was a problem hiding this comment.
Bumping this, aren't the ohttp keys specific to the payjoin directory?
| } | ||
| } | ||
|
|
||
| pub(crate) fn fetch_ohttp_keys( |
There was a problem hiding this comment.
Is it possible to reuse PDK's implementation of fetch_ohttp_keys?
There was a problem hiding this comment.
Yes, but the current PDK impl is async so we would need to add some blocking capability and without it the async stuff would cascade. So i think its best to keep it manually re-implemented. though It might be nice to add sync versions for situations like this
| db_conn: &mut Box<dyn DatabaseConnection>, | ||
| secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>, | ||
| ) -> Result<(), Box<dyn Error>> { | ||
| let proposal = proposal.apply_fee_range(None, None).save(persister)?; |
There was a problem hiding this comment.
No fee range actually being applied.
|
Thanks for the feedback, Regarding the UI notes.
Since it is not a mandatory addition in the payjoin spec I thought that for a desktop on-chain only wallet the UX of adding an amount on the receive side is sort of unfamiliar, especially given the current liana receiver UI
I think again perhaps this is worth some user research but with the ability to do a fallback tx at any point I am not sure that extending the expiration as an option is great to put in the user's direct control
This was something we landed on due wanting to discourage reuse of payjoin sessions. I could share this bip21 with as many people as I want similar to address reuse but instead of just bad privacy you would get a bunch of payjoin failures The method again is a holdout as the last time I gave this some serious focus I transitioned from storing the bip21 to the session ID directly and I previously wanted to have access to the bip21 in the db where I just generate it in the gui now and don't store it directly |
This is definitely an artifact of me playing in the database between naming the timestamp |
Familiarity might not be an issue IMO, since Liana users presumably also use other (esp mobile) wallets that have this. IMO it's more about the use case or user type: how often do they want to receive an specific exact amount? That said, this seems like a reasonable starting point.
My hunch is, Liana users are likely to be sophisticated enough to use such a feature properly. Perhaps there could be appropriate messaging/guidance about using the feature. |
aac7f6e to
06623c7
Compare
| let txid = psbt.unsigned_tx.compute_txid(); | ||
| if let Some(psbt) = db_conn.spend_tx(&txid) { | ||
| let mut is_signed = false; | ||
| for psbtin in &psbt.inputs { | ||
| if !psbtin.partial_sigs.is_empty() { | ||
| log::debug!("[Payjoin] PSBT is signed!"); | ||
| is_signed = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if is_signed { | ||
| let proposal = proposal | ||
| .finalize_proposal(|_| { | ||
| let mut psbt = psbt.clone(); | ||
| finalize_psbt(&mut psbt, secp); | ||
| Ok(psbt) | ||
| }) | ||
| .save(persister)?; | ||
|
|
||
| send_payjoin_proposal(proposal, persister)?; | ||
| } |
There was a problem hiding this comment.
I couldn't quite figure out what the intention was here with is_signed, however during my testing is_signed never gets set to true and the proposal never gets finalized. Can you explain what validation is being done here?
Also, when I replaced |_| with |psbt| in the finalize_psbt closure it was still possible to clone as mut, whats the rationale for calling psbt_to_sign outside of this closure?
Something else that may be related is that currently psbt_to_sign doesn't return a psbt with sender sigs removed, this is something I am fixing in payjoin/rust-payjoin#1446. Let me know if it would good for me to create a separate smaller PR for this just to get this fixed sooner.
I did also try hardcoding is_signed to true so the receiver does send out a psbt proposal, but its then erroring on the sender side during validation. So the finalize_psbt isn't quite working right, but that may be because the psbt thats being finalized has the leftover sender sigs still.
Apologies this was due to my incomplete understanding of the PSBT signing workflow
3c14fca to
075355f
Compare
a010c72 to
cd348bf
Compare
ad2a6ab to
6cb8dcf
Compare
Instead of relying on the DB for this critical method in the state machine we should instead create a map up to a set amount to better ensure that there is correct validation on owned inputs for the receiver.
6cb8dcf to
9be334c
Compare
| .update_change_psbt_out(&mut psbt_out); | ||
| } | ||
|
|
||
| // TODO: shuffle once we have Taproot |
There was a problem hiding this comment.
this TODO ties output shuffle to Taproot, but the two are independent: shuffling output position is a pure reorder, no Taproot needed, shippable now. Taproot only helps change script-type indistinguishability, a separate fingerprint.
suggest shuffling
ref: payjoin comment
There was a problem hiding this comment.
ping @darosior , I guess it was a real reason even it it's not obvious now
There was a problem hiding this comment.
I guess if we add shuffling we should have a way to opt out, as I think our integrations tests rely on ordering for many cases, and imo it's good to keep this logic deterministicaly testable.
| ) -> Result<(), Box<dyn Error>> { | ||
| let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); | ||
|
|
||
| let mut candidate_inputs_map = HashMap::<OutPoint, (Coin, TxIn, Input, Weight)>::new(); |
There was a problem hiding this comment.
makes sense to use BTreeMap here so the contributed input is deterministic, aligning with payjoin/rust-payjoin#1597 (comment) ?
There was a problem hiding this comment.
can you open a separate issue for this?


This adds support for payjoin receive in liana.
This is currently WIP and has the following TODOs