Skip to content
Open
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
05f1c6c
[PDK] Initial approach.
arturgontijo May 10, 2025
2e4c035
[PDK] Use rust-payjoin branch that accepts AddressType::P2wsh
arturgontijo May 12, 2025
adad5df
[PDK] Stable sender workflow.
arturgontijo May 14, 2025
097f4a1
[PDK] payjoin mod.
arturgontijo May 15, 2025
cb6cb4b
[PDK] Use try_preserving_privacy()
arturgontijo May 21, 2025
40c0500
[PDK] Use SLE
arturgontijo May 28, 2025
b664083
[PDK] SEL working...
arturgontijo Jun 4, 2025
8e8b0a2
Merge branch 'refs/heads/master' into payjoin-v3
arturgontijo Jun 4, 2025
b1c446a
[PDK] Sync master + OHttpKeys in DB.
arturgontijo Jun 5, 2025
82c2287
[PDK] Better receiver code structure + clippy.
arturgontijo Jun 5, 2025
7d69acc
[PDK] fix
arturgontijo Jun 5, 2025
b275f7b
Code review
arturgontijo Jun 11, 2025
df4d285
[PDK] Use SEL remote branch + contribute_inputs_with_weights()
arturgontijo Jun 15, 2025
b7e55e4
Using SEL with p2wsh hack.
arturgontijo Jul 7, 2025
4cded07
Impl payjoin methods on lianad
arminsabouri Jul 8, 2025
a9da817
Should save amount and address from bip21
arminsabouri Jul 8, 2025
64a490b
Remove redundant results in db interface
arminsabouri Jul 9, 2025
50981d7
Handle ohttp and finalize sender psbt error in rpc endpoints
arminsabouri Jul 9, 2025
7465e63
Run project wide fmt
arminsabouri Jul 9, 2025
357138b
Remove get payjoin info endpoint
arminsabouri Jul 9, 2025
afdbacd
Simplify status to wrap only one enum
arminsabouri Jul 9, 2025
c949090
Remove unused receiver sender status enums
arminsabouri Jul 9, 2025
1aec1a9
Update pdk branch to use-witness-for-expected-weight
arminsabouri Jul 10, 2025
cabadb1
Cleanup receiver unwraps and error propogation
benalleng Jul 9, 2025
a907cb2
Refactor pj db tables to use seperate table for events
arminsabouri Jul 9, 2025
c9ba5cb
Remove unused pj json-rpc endpoints
arminsabouri Jul 9, 2025
1e358b3
Revert "Remove get payjoin info endpoint"
arminsabouri Jul 9, 2025
abf9c87
Do update existing pj proposal
arminsabouri Jul 10, 2025
cf9ad77
Fix getting active payjoins sessions
arminsabouri Jul 10, 2025
ed22c01
gui: path is ready if all owned inputs are signed
arminsabouri Jul 10, 2025
04a7511
Add expected weight to inputs
arminsabouri Jul 11, 2025
2bcdefe
Resolve clippy linting warnings
arminsabouri Jul 11, 2025
2f2b64d
hack: workaround getting payjoin status
arminsabouri Jul 17, 2025
164b834
Save psbt with fees applied
arminsabouri Jul 15, 2025
3eb48ee
Refactor schema to remove redundant session id
arminsabouri Jul 16, 2025
7186747
Update payjoin persistance related test utils
arminsabouri Jul 16, 2025
69aa6f0
Add unit tests for payjoin db utils
arminsabouri Jul 17, 2025
5b16466
Rename address struct payjoin-uri -> bip21
arminsabouri Jul 22, 2025
309ba07
Replace bip21 string with url type
arminsabouri Jul 22, 2025
de8da92
Add support for bip21 qr codes on the receiver
benalleng Jul 23, 2025
dc9aefd
[WIP] add somre receiver state checks
benalleng Jul 14, 2025
a99c8da
Receiver closure should be mutable refrences
arminsabouri Jul 24, 2025
18b597b
Return errors from p2wsh hot signer
arminsabouri Jul 24, 2025
429d16f
Breakout payjoin status from bip21
arminsabouri Jul 24, 2025
d6cc409
Get payjoin info if spend is original proposal or ready to be signed …
arminsabouri Jul 24, 2025
1f0cf93
Rename get all session ids to get all active session ids
arminsabouri Jul 24, 2025
d560626
Return only payjoin status from json rpc endpoint
arminsabouri Jul 24, 2025
d74a605
Seperate out bip21 from spendtx model
arminsabouri Jul 24, 2025
6167b0f
Clean up unused code in get payjoin info
arminsabouri Jul 24, 2025
e03b9d1
Store original and proposed txids for pj sender
arminsabouri Jul 25, 2025
5848d9e
Store original and proposed txids for pj receiver
arminsabouri Jul 25, 2025
a0dd289
Resolve missing `has_bip21` flag
arminsabouri Jul 25, 2025
24013d7
todo!() remaining test utils methods
arminsabouri Jul 28, 2025
61e1e4c
Fix payjoin visibility in helpers.rs
arminsabouri Jul 28, 2025
c673fa6
Consolidate use of OHTTP_RELAY const
arminsabouri Jul 28, 2025
056049e
Consolidate use of PAYJOIN_DIR const
arminsabouri Jul 28, 2025
872b1fb
Add insert_inputs_seen_before database method
benalleng Jul 16, 2025
f1d726e
Cleanup clippy warnings for CI
benalleng Jul 29, 2025
d206772
Clean up unused error fields in sendpayjoin modal
arminsabouri Jul 29, 2025
fbd916e
Remove unneeded debug logs
arminsabouri Jul 29, 2025
58757e1
Fix visibility in payjoin sub mod
arminsabouri Jul 29, 2025
27663ad
Remove unused db error variant
arminsabouri Jul 29, 2025
690bfff
Lazily deserialize session events in load()
arminsabouri Jul 29, 2025
69a6dec
Add rustdocs to finalize_psbt()
arminsabouri Jul 29, 2025
69b6743
Restore psbt input fields after finalizing psbt
arminsabouri Jul 30, 2025
eec4ea3
Let rust-payjoin restore psbt inputs
arminsabouri Jul 30, 2025
39ee2e7
Remove unused db deser error variant
arminsabouri Jul 30, 2025
6f74743
Remove buggy sender fallback tx check
arminsabouri Aug 14, 2025
2534935
Add todo insert_inputs_seen dummmy test utils method
benalleng Jul 30, 2025
7b5896b
Process Receiver sessions is no longer halted by a failed session
benalleng Aug 14, 2025
d7f1bff
Sender checks skip failed sessions
benalleng Aug 14, 2025
e62a806
Prevent expired sessions from panicing
benalleng Aug 18, 2025
b62fe49
handle replay session events errors
benalleng Aug 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
828 changes: 757 additions & 71 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions liana-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ reqwest = { version = "0.11", default-features=false, features = ["json", "rustl
rust-ini = "0.19.0"
rfd = "0.15.1"

# Payjoin
payjoin = { git = "https://github.com/payjoin/rust-payjoin.git", branch = "master", features = ["v2", "io"] }


[target.'cfg(windows)'.dependencies]
zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] }
Expand Down
5 changes: 4 additions & 1 deletion liana-gui/src/app/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use liana::miniscript::bitcoin::{
Address, Txid,
};
use lianad::config::Config as DaemonConfig;
use payjoin::Url;

use crate::{
app::{cache::Cache, error::Error, view, wallet::Wallet},
Expand All @@ -25,7 +26,7 @@ pub enum Message {
DaemonConfigLoaded(Result<(), Error>),
LoadWallet(Wallet),
Info(Result<GetInfoResult, Error>),
ReceiveAddress(Result<(Address, ChildNumber), Error>),
ReceiveAddress(Result<(Address, ChildNumber, Option<Url>), Error>),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If bip21 is get shipped as its own PR. We may want to use bitcoin_uri for this type instead

/// Revealed addresses. The second element contains the start index used for the request.
RevealedAddresses(
Result<ListRevealedAddressesResult, Error>,
Expand Down Expand Up @@ -55,6 +56,8 @@ pub enum Message {
BroadcastModal(Result<HashSet<Txid>, Error>),
RbfModal(Box<HistoryTransaction>, bool, Result<HashSet<Txid>, Error>),
Export(ImportExportMessage),
SendPayjoin(Result<(), Error>),
PayjoinInitiated(Result<String, Error>),
Comment on lines +59 to +60
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could have better names to differenciate between these

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the former could be sentPayjoin

}

impl From<ImportExportMessage> for Message {
Expand Down
54 changes: 53 additions & 1 deletion liana-gui/src/app/state/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub enum PsbtModal {
Broadcast(BroadcastModal),
Delete(DeleteModal),
Export(ExportModal),
SendPayjoin(SendPayjoinModal),
}

impl<'a> AsRef<dyn Modal + 'a> for PsbtModal {
Expand All @@ -68,6 +69,7 @@ impl<'a> AsRef<dyn Modal + 'a> for PsbtModal {
Self::Broadcast(a) => a,
Self::Delete(a) => a,
Self::Export(a) => a,
Self::SendPayjoin(a) => a,
}
}
}
Expand All @@ -80,6 +82,7 @@ impl<'a> AsMut<dyn Modal + 'a> for PsbtModal {
Self::Broadcast(a) => a,
Self::Delete(a) => a,
Self::Export(a) => a,
Self::SendPayjoin(a) => a,
}
}
}
Expand All @@ -88,21 +91,23 @@ pub struct PsbtState {
pub wallet: Arc<Wallet>,
pub desc_policy: LianaPolicy,
pub tx: SpendTx,
pub bip21: Option<String>,
pub saved: bool,
pub warning: Option<Error>,
pub labels_edited: LabelsEdited,
pub modal: Option<PsbtModal>,
}

impl PsbtState {
pub fn new(wallet: Arc<Wallet>, tx: SpendTx, saved: bool) -> Self {
pub fn new(wallet: Arc<Wallet>, tx: SpendTx, saved: bool, bip21: Option<String>) -> Self {
Self {
desc_policy: wallet.main_descriptor.policy(),
wallet,
labels_edited: LabelsEdited::default(),
warning: None,
modal: None,
tx,
bip21,
saved,
}
}
Expand Down Expand Up @@ -182,6 +187,30 @@ impl PsbtState {
Message::View(view::Message::Spend(view::SpendTxMessage::Delete)) => {
self.modal = Some(PsbtModal::Delete(DeleteModal::default()));
}
Message::View(view::Message::Spend(view::SpendTxMessage::SendPayjoin)) => {
let modal = SendPayjoinModal::new();
let cmd = modal.load(daemon);
self.modal = Some(PsbtModal::SendPayjoin(modal));
return cmd;
}
Message::View(view::Message::Spend(view::SpendTxMessage::PayjoinInitiated)) => {
self.tx.status = SpendStatus::PayjoinInitiated;
self.modal = None;
if let Some(_payjoin_info) = self.tx.payjoin_status.clone() {
let psbt = self.tx.psbt.clone();
// TODO: should this be an error?
let bip21 = self.bip21.clone().expect("bip21 should be set");
return Task::perform(
async move {
daemon
.send_payjoin(bip21, &psbt)
.await
.map_err(|e| e.into())
},
Message::SendPayjoin,
);
}
}
Message::View(view::Message::Spend(view::SpendTxMessage::Sign)) => {
if let Some(PsbtModal::Sign(SignModal { display_modal, .. })) = &mut self.modal {
*display_modal = true;
Expand Down Expand Up @@ -301,6 +330,28 @@ impl PsbtState {
}
}

#[derive(Default)]
pub struct SendPayjoinModal {
_error: Option<Error>,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If errors cannot occur lets remove this

}

impl SendPayjoinModal {
pub fn new() -> Self {
Self { _error: None }
}
}

impl Modal for SendPayjoinModal {
fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message> {
modal::Modal::new(content, view::psbt::payjoin_send_success_view())
// On blur, show the psbts view
.on_blur(Some(view::Message::Spend(
view::SpendTxMessage::PayjoinInitiated,
)))
.into()
}
}

#[derive(Default)]
pub struct SaveModal {
saved: bool,
Expand Down Expand Up @@ -537,6 +588,7 @@ impl Modal for SignModal {
self.signed.insert(fingerprint);
let daemon = daemon.clone();
merge_signatures(&mut tx.psbt, &psbt);

if self.is_saved {
return Task::perform(
async move { daemon.update_spend_tx(&psbt).await.map_err(|e| e.into()) },
Expand Down
7 changes: 4 additions & 3 deletions liana-gui/src/app/state/psbts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl PsbtsPanel {
}

pub fn preselect(&mut self, spend_tx: SpendTx) {
let psbt_state = psbt::PsbtState::new(self.wallet.clone(), spend_tx, true);
let psbt_state = psbt::PsbtState::new(self.wallet.clone(), spend_tx, true, None);
self.selected_tx = Some(psbt_state);
self.warning = None;
self.modal = None;
Expand Down Expand Up @@ -81,7 +81,8 @@ impl State for PsbtsPanel {
spend_tx.psbt.unsigned_tx.compute_txid()
== tx.tx.psbt.unsigned_tx.compute_txid()
}) {
let tx = psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true);
let tx =
psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true, None);
let cmd = tx.load(daemon);
self.selected_tx = Some(tx);
return cmd;
Expand Down Expand Up @@ -119,7 +120,7 @@ impl State for PsbtsPanel {
}
Message::View(view::Message::Select(i)) => {
if let Some(tx) = self.spend_txs.get(i) {
let tx = psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true);
let tx = psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true, None);
let cmd = tx.load(daemon);
self.selected_tx = Some(tx);
return cmd;
Expand Down
66 changes: 62 additions & 4 deletions liana-gui/src/app/state/receive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use liana::miniscript::bitcoin::{
Address, Network,
};
use liana_ui::{component::modal, widget::*};
use payjoin::Url;

use crate::daemon::model::LabelsLoader;
use crate::dir::LianaDirectory;
Expand All @@ -33,12 +34,14 @@ const PREV_ADDRESSES_PAGE_SIZE: usize = 20;
pub enum Modal {
VerifyAddress(VerifyAddressModal),
ShowQrCode(ShowQrCodeModal),
ShowBip21QrCode(ShowBip21QrCodeModal),
None,
}

#[derive(Debug, Default)]
pub struct Addresses {
list: Vec<Address>,
bip21s: HashMap<Address, Url>,
derivation_indexes: Vec<ChildNumber>,
labels: HashMap<String, String>,
}
Expand Down Expand Up @@ -121,6 +124,7 @@ impl State for ReceivePanel {
self.warning.as_ref(),
view::receive::receive(
&self.addresses.list,
&self.addresses.bip21s,
&self.addresses.labels,
&self.prev_addresses.list,
&self.prev_addresses.labels,
Expand All @@ -139,6 +143,9 @@ impl State for ReceivePanel {
Modal::ShowQrCode(m) => modal::Modal::new(content, m.view())
.on_blur(Some(view::Message::Close))
.into(),
Modal::ShowBip21QrCode(m) => modal::Modal::new(content, m.view())
.on_blur(Some(view::Message::Close))
.into(),
Modal::None => content,
}
}
Expand Down Expand Up @@ -175,10 +182,13 @@ impl State for ReceivePanel {
}
Message::ReceiveAddress(res) => {
match res {
Ok((address, derivation_index)) => {
Ok((address, derivation_index, bip21)) => {
self.warning = None;
self.addresses.list.push(address);
self.addresses.list.push(address.clone());
self.addresses.derivation_indexes.push(derivation_index);
if let Some(bip21) = bip21 {
self.addresses.bip21s.insert(address, bip21);
}
}
Err(e) => self.warning = Some(e),
}
Expand Down Expand Up @@ -209,7 +219,7 @@ impl State for ReceivePanel {
daemon
.get_new_address()
.await
.map(|res| (res.address, res.derivation_index))
.map(|res| (res.address, res.derivation_index, res.bip21))
.map_err(|e| e.into())
},
Message::ReceiveAddress,
Expand Down Expand Up @@ -294,6 +304,33 @@ impl State for ReceivePanel {
}
Task::none()
}
Message::View(view::Message::ShowBip21QrCode(i)) => {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feature needed: add amount field. Currently the bip21 is missing this and our refrence implementation will complain

if let (Some(bip21), Some(index)) = (
&self
.addresses
.bip21s
.get(self.address(i).expect("Address should be in bip21")),
self.derivation_index(i),
) {
if let Some(modal) = ShowBip21QrCodeModal::new(bip21, *index) {
self.modal = Modal::ShowBip21QrCode(modal);
}
}
Task::none()
}
Message::View(view::Message::PayjoinInitiate) => {
let daemon = daemon.clone();
Task::perform(
async move {
daemon
.receive_payjoin()
.await
.map(|res| (res.address, res.derivation_index, res.bip21))
.map_err(|e| e.into())
},
Message::ReceiveAddress,
)
}
_ => {
if let Modal::VerifyAddress(ref mut m) = self.modal {
m.update(daemon, cache, message)
Expand Down Expand Up @@ -434,6 +471,26 @@ impl ShowQrCodeModal {
}
}

pub struct ShowBip21QrCodeModal {
qr_code: qr_code::Data,
bip21: String,
}

impl ShowBip21QrCodeModal {
pub fn new(bip21: &payjoin::Url, _index: ChildNumber) -> Option<Self> {
qr_code::Data::new(format!("{}", bip21))
.ok()
.map(|qr_code| Self {
qr_code,
bip21: bip21.to_string(),
})
}

fn view(&self) -> Element<view::Message> {
view::receive::qr_modal(&self.qr_code, &self.bip21)
}
}

async fn verify_address(
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
index: ChildNumber,
Expand Down Expand Up @@ -484,7 +541,8 @@ mod tests {
Some(json!({"method": "getnewaddress", "params": Option::<Request>::None})),
Ok(json!(GetAddressResult::new(
addr.clone(),
ChildNumber::from_normal_idx(0).unwrap()
ChildNumber::from_normal_idx(0).unwrap(),
None,
))),
),
]);
Expand Down
Loading