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

Commit e84d435

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 5c17520 commit e84d435

3 files changed

Lines changed: 62 additions & 13 deletions

File tree

mutiny-core/src/nodemanager.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,6 @@ impl<S: MutinyStorage> NodeManager<S> {
715715
.map_err(|_| MutinyError::IncorrectNetwork)?;
716716
let address = uri.address.clone();
717717
let original_psbt = self.wallet.create_signed_psbt(address, amount, fee_rate)?;
718-
719718
let fee_rate = if let Some(rate) = fee_rate {
720719
FeeRate::from_sat_per_vb(rate)
721720
} else {
@@ -800,10 +799,16 @@ impl<S: MutinyStorage> NodeManager<S> {
800799
let http_client = reqwest::Client::builder()
801800
.build()
802801
.map_err(PayjoinError::Reqwest)?;
803-
let proposal: payjoin::receive::v2::UncheckedProposal =
804-
Self::poll_for_fallback_psbt(stop, storage, &http_client, &mut session).await?;
802+
let proposal: payjoin::receive::v2::UncheckedProposal = Self::poll_for_fallback_psbt(
803+
stop,
804+
wallet.clone(),
805+
storage.clone(),
806+
&http_client,
807+
&mut session,
808+
)
809+
.await?;
805810
let original_tx = proposal.extract_tx_to_schedule_broadcast();
806-
let mut payjoin_proposal = match wallet.process_payjoin_proposal(proposal) {
811+
let mut payjoin_proposal = match wallet.process_payjoin_proposal(proposal).await {
807812
Ok(p) => p,
808813
Err(e) => {
809814
wallet.broadcast_transaction(original_tx).await?;
@@ -826,11 +831,23 @@ impl<S: MutinyStorage> NodeManager<S> {
826831
let _res = payjoin_proposal
827832
.deserialize_res(res.to_vec(), ohttp_ctx)
828833
.map_err(|e| PayjoinError::ReceiverStateMachine(e.to_string()))?;
829-
Ok(payjoin_proposal.psbt().clone().extract_tx().txid())
834+
let payjoin_tx = payjoin_proposal.psbt().clone().extract_tx();
835+
let payjoin_txid = payjoin_tx.txid();
836+
wallet
837+
.insert_tx(
838+
payjoin_tx.clone(),
839+
ConfirmationTime::unconfirmed(utils::now().as_secs()),
840+
None,
841+
)
842+
.await?;
843+
session.payjoin_tx = Some(payjoin_tx);
844+
storage.update_recv_session(session)?;
845+
Ok(payjoin_txid)
830846
}
831847

832848
async fn poll_for_fallback_psbt(
833849
stop: Arc<AtomicBool>,
850+
wallet: Arc<OnChainWallet<S>>,
834851
storage: Arc<S>,
835852
client: &reqwest::Client,
836853
session: &mut crate::payjoin::RecvSession,
@@ -841,6 +858,11 @@ impl<S: MutinyStorage> NodeManager<S> {
841858
}
842859

843860
if session.expiry < utils::now() {
861+
if let Some(payjoin_tx) = &session.payjoin_tx {
862+
wallet
863+
.cancel_tx(payjoin_tx)
864+
.map_err(|_| crate::payjoin::Error::CancelPayjoinTx)?;
865+
}
844866
let _ = storage.delete_recv_session(&session.enrolled.pubkey());
845867
return Err(crate::payjoin::Error::SessionExpired);
846868
}

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)