Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit 7855489

Browse files
committed
Track pending payjoin transactions in wallet
A receiver payjoin proposal PSBT are tracked as pending since it awaits a sender signature. This lets the pending TX display in the UI as an ActivityItem.
1 parent 1a2f135 commit 7855489

3 files changed

Lines changed: 62 additions & 12 deletions

File tree

mutiny-core/src/nodemanager.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,6 @@ impl<S: MutinyStorage> NodeManager<S> {
718718
.map_err(|_| MutinyError::IncorrectNetwork)?;
719719
let address = uri.address.clone();
720720
let original_psbt = self.wallet.create_signed_psbt(address, amount, fee_rate)?;
721-
722721
let fee_rate = if let Some(rate) = fee_rate {
723722
FeeRate::from_sat_per_vb(rate)
724723
} else {
@@ -803,11 +802,18 @@ impl<S: MutinyStorage> NodeManager<S> {
803802
let http_client = reqwest::Client::builder()
804803
.build()
805804
.map_err(PayjoinError::Reqwest)?;
806-
let proposal: payjoin::receive::v2::UncheckedProposal =
807-
Self::poll_for_fallback_psbt(stop, storage, &http_client, &mut session).await?;
805+
let proposal: payjoin::receive::v2::UncheckedProposal = Self::poll_for_fallback_psbt(
806+
stop,
807+
wallet.clone(),
808+
storage.clone(),
809+
&http_client,
810+
&mut session,
811+
)
812+
.await?;
808813
let original_tx = proposal.extract_tx_to_schedule_broadcast();
809814
let mut payjoin_proposal = match wallet
810815
.process_payjoin_proposal(proposal)
816+
.await
811817
.map_err(|e| PayjoinError::ReceiverStateMachine(e.to_string()))
812818
{
813819
Ok(p) => p,
@@ -832,11 +838,23 @@ impl<S: MutinyStorage> NodeManager<S> {
832838
let _res = payjoin_proposal
833839
.deserialize_res(res.to_vec(), ohttp_ctx)
834840
.map_err(|e| PayjoinError::ReceiverStateMachine(e.to_string()))?;
835-
Ok(payjoin_proposal.psbt().clone().extract_tx().txid())
841+
let payjoin_tx = payjoin_proposal.psbt().clone().extract_tx();
842+
let payjoin_txid = payjoin_tx.txid();
843+
wallet
844+
.insert_tx(
845+
payjoin_tx.clone(),
846+
ConfirmationTime::unconfirmed(utils::now().as_secs()),
847+
None,
848+
)
849+
.await?;
850+
session.payjoin_tx = Some(payjoin_tx);
851+
storage.update_recv_session(session)?;
852+
Ok(payjoin_txid)
836853
}
837854

838855
async fn poll_for_fallback_psbt(
839856
stop: Arc<AtomicBool>,
857+
wallet: Arc<OnChainWallet<S>>,
840858
storage: Arc<S>,
841859
client: &reqwest::Client,
842860
session: &mut crate::payjoin::RecvSession,
@@ -847,6 +865,11 @@ impl<S: MutinyStorage> NodeManager<S> {
847865
}
848866

849867
if session.expiry < utils::now() {
868+
if let Some(payjoin_tx) = &session.payjoin_tx {
869+
wallet
870+
.cancel_tx(payjoin_tx)
871+
.map_err(|_| crate::payjoin::Error::CancelPayjoinTx)?;
872+
}
850873
let _ = storage.delete_recv_session(&session.enrolled.pubkey());
851874
return Err(crate::payjoin::Error::SessionExpired);
852875
}

mutiny-core/src/onchain.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ impl<S: MutinyStorage> OnChainWallet<S> {
355355
Ok(())
356356
}
357357

358+
pub(crate) fn cancel_tx(&self, tx: &Transaction) -> Result<(), MutinyError> {
359+
let mut wallet = self.wallet.try_write()?;
360+
wallet.cancel_tx(tx);
361+
Ok(())
362+
}
363+
358364
fn is_mine(&self, script: &Script) -> Result<bool, MutinyError> {
359365
Ok(self.wallet.try_read()?.is_mine(script))
360366
}
@@ -363,7 +369,7 @@ impl<S: MutinyStorage> OnChainWallet<S> {
363369
Ok(self.wallet.try_read()?.list_unspent().collect())
364370
}
365371

366-
pub fn process_payjoin_proposal(
372+
pub async fn process_payjoin_proposal(
367373
&self,
368374
proposal: payjoin::receive::v2::UncheckedProposal,
369375
) -> Result<payjoin::receive::v2::PayjoinProposal, payjoin::Error> {
@@ -407,21 +413,26 @@ impl<S: MutinyStorage> OnChainWallet<S> {
407413
let payjoin_proposal = provisional_payjoin.finalize_proposal(
408414
|psbt| {
409415
let mut psbt = psbt.clone();
410-
let wallet = self
411-
.wallet
412-
.try_read()
413-
.map_err(|_| Error::Server(MutinyError::WalletSigningFailed.into()))?;
414-
wallet
416+
self.wallet
417+
.try_write()
418+
.map_err(|_| Error::Server(MutinyError::WalletSigningFailed.into()))?
415419
.sign(&mut psbt, SignOptions::default())
416420
.map_err(|_| Error::Server(MutinyError::WalletSigningFailed.into()))?;
417421
Ok(psbt)
418422
},
419423
Some(min_pj_fee_rate),
420424
)?;
421-
let payjoin_proposal_psbt = payjoin_proposal.psbt();
425+
let payjoin_psbt_tx = payjoin_proposal.psbt().clone().extract_tx();
426+
self.insert_tx(
427+
payjoin_psbt_tx,
428+
ConfirmationTime::unconfirmed(crate::utils::now().as_secs()),
429+
None,
430+
)
431+
.await
432+
.map_err(|_| Error::Server(MutinyError::WalletOperationFailed.into()))?;
422433
log::debug!(
423434
"Receiver's Payjoin proposal PSBT Rsponse: {:#?}",
424-
payjoin_proposal_psbt
435+
payjoin_proposal.psbt()
425436
);
426437
Ok(payjoin_proposal)
427438
}

mutiny-core/src/payjoin.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::collections::HashMap;
22

33
use crate::error::MutinyError;
44
use crate::storage::MutinyStorage;
5+
use bitcoin::Transaction;
56
use core::time::Duration;
67
use hex_conservative::DisplayHex;
78
use once_cell::sync::Lazy;
@@ -29,6 +30,7 @@ pub(crate) static PAYJOIN_DIR: Lazy<Url> =
2930
pub struct RecvSession {
3031
pub enrolled: Enrolled,
3132
pub expiry: Duration,
33+
pub payjoin_tx: Option<Transaction>,
3234
}
3335

3436
impl RecvSession {
@@ -39,6 +41,7 @@ impl RecvSession {
3941
pub trait PayjoinStorage {
4042
fn list_recv_sessions(&self) -> Result<Vec<RecvSession>, MutinyError>;
4143
fn store_new_recv_session(&self, session: Enrolled) -> Result<RecvSession, MutinyError>;
44+
fn update_recv_session(&self, session: RecvSession) -> Result<(), MutinyError>;
4245
fn delete_recv_session(&self, id: &[u8; 33]) -> Result<(), MutinyError>;
4346
}
4447

@@ -59,11 +62,16 @@ impl<S: MutinyStorage> PayjoinStorage for S {
5962
let session = RecvSession {
6063
enrolled,
6164
expiry: in_24_hours,
65+
payjoin_tx: None,
6266
};
6367
self.set_data(get_payjoin_key(&session.pubkey()), session.clone(), None)
6468
.map(|_| session)
6569
}
6670

71+
fn update_recv_session(&self, session: RecvSession) -> Result<(), MutinyError> {
72+
self.set_data(get_payjoin_key(&session.pubkey()), session, None)
73+
}
74+
6775
fn delete_recv_session(&self, id: &[u8; 33]) -> Result<(), MutinyError> {
6876
self.delete(&[get_payjoin_key(id)])
6977
}
@@ -89,6 +97,10 @@ pub enum Error {
8997
OhttpDecodeFailed,
9098
Shutdown,
9199
SessionExpired,
100+
BadDirectoryHost,
101+
BadOhttpWsHost,
102+
RequestFailed(String),
103+
CancelPayjoinTx,
92104
}
93105

94106
impl std::error::Error for Error {}
@@ -102,6 +114,10 @@ impl std::fmt::Display for Error {
102114
Error::OhttpDecodeFailed => write!(f, "Failed to decode ohttp keys"),
103115
Error::Shutdown => write!(f, "Payjoin stopped by application shutdown"),
104116
Error::SessionExpired => write!(f, "Payjoin session expired. Create a new payment request and have the sender try again."),
117+
Error::BadDirectoryHost => write!(f, "Bad directory host"),
118+
Error::BadOhttpWsHost => write!(f, "Bad ohttp ws host"),
119+
Error::RequestFailed(e) => write!(f, "Request failed: {}", e),
120+
Error::CancelPayjoinTx => write!(f, "Failed to cancel payjoin tx in wallet"),
105121
}
106122
}
107123
}

0 commit comments

Comments
 (0)