Skip to content

Commit 8f6ffff

Browse files
committed
Add PayjoinHandler
Add two new structs `PayjoinHandler` and `PayjoinReceiver`. The first is public and holds the later as a property and can be used in order to serve incoming payjoin transactions. `PayjoinReceiver` implemented `lightning_payjoin::Receiver` trait to allow us to receive payjoin transactions or open a channel.
1 parent fc9b560 commit 8f6ffff

File tree

6 files changed

+200
-46
lines changed

6 files changed

+200
-46
lines changed

Cargo.toml

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,20 @@ panic = 'abort' # Abort on panic
2828
default = []
2929

3030
[dependencies]
31-
lightning = { version = "0.0.123-beta", features = ["std"] }
32-
lightning-invoice = { version = "0.31.0-beta" }
33-
lightning-net-tokio = { version = "0.0.123-beta" }
34-
lightning-persister = { version = "0.0.123-beta" }
35-
lightning-background-processor = { version = "0.0.123-beta", features = ["futures"] }
36-
lightning-rapid-gossip-sync = { version = "0.0.123-beta" }
37-
lightning-transaction-sync = { version = "0.0.123-beta", features = ["esplora-async-https", "time"] }
31+
lightning = { git = "https://github.com/jbesraa/rust-lightning",rev = "0cb3d57", features = ["std"] }
32+
lightning-invoice = { git = "https://github.com/jbesraa/rust-lightning",rev = "0cb3d57" }
33+
lightning-net-tokio = { git = "https://github.com/jbesraa/rust-lightning",rev = "0cb3d57" }
34+
lightning-persister = { git = "https://github.com/jbesraa/rust-lightning",rev = "0cb3d57" }
35+
lightning-background-processor = { git = "https://github.com/jbesraa/rust-lightning", rev = "0cb3d57", features = ["futures"] }
36+
lightning-rapid-gossip-sync = { git = "https://github.com/jbesraa/rust-lightning", rev = "0cb3d57" }
37+
lightning-transaction-sync = { git = "https://github.com/jbesraa/rust-lightning", rev = "0cb3d57", features = ["esplora-async-https", "time"] }
3838
#lightning-liquidity = { version = "0.1.0-alpha.1", features = ["std"] }
3939

40-
lightning-liquidity = { git = "https://github.com/tnull/lightning-liquidity", rev = "abf7088c0e03221c0f122e797f34802c9e99a3d4", features = ["std"] }
40+
# lightning-liquidity = {path = "../../lightning-liquidity" git = "https://github.com/jbesraa/lightning-liquidity", rev = "b6ac60d", features = ["std"] }
41+
lightning-liquidity = {path = "../../lightning-liquidity" , features = ["std"] }
42+
# lightning-liquidity = { git = "https://github.com/tnull/lightning-liquidity", rev = "abf7088c0e03221c0f122e797f34802c9e99a3d4", features = ["std"] }
4143

44+
lightning-payjoin = { path = "./lightning-payjoin" }
4245
#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std"] }
4346
#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" }
4447
#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" }
@@ -79,12 +82,14 @@ prost = { version = "0.11.6", default-features = false}
7982
winapi = { version = "0.3", features = ["winbase"] }
8083

8184
[dev-dependencies]
82-
lightning = { version = "0.0.123-beta", features = ["std", "_test_utils"] }
85+
lightning = { git = "https://github.com/jbesraa/rust-lightning",rev = "0cb3d57", features = ["std", "_test_utils"] }
86+
# lightning = { version = "0.0.123-beta", features = ["std", "_test_utils"] }
8387
#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] }
8488
electrum-client = { version = "0.15.1", default-features = true }
8589
bitcoincore-rpc = { version = "0.17.0", default-features = false }
8690
proptest = "1.0.0"
8791
regex = "1.5.6"
92+
reqwest = { version = "0.11", default-features = false, features = ["blocking"] }
8893

8994
[target.'cfg(not(no_download))'.dev-dependencies]
9095
electrsd = { version = "0.26.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] }

lightning-payjoin/src/error.rs

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use lightning::util::errors::APIError;
2-
31
#[derive(Debug)]
42
pub enum Error {
53
Internal(InternalError),
@@ -73,39 +71,39 @@ impl std::fmt::Display for Error {
7371
}
7472
}
7573

76-
// Maybe this should live in LDK-Node instead?
77-
impl From<APIError> for Error {
78-
fn from(value: APIError) -> Self {
79-
match value {
80-
APIError::APIMisuseError { err } => {
81-
Self::Internal(InternalError::FundingTxGenerationFailed(err))
82-
},
83-
APIError::FeeRateTooHigh { err, feerate } => Self::Internal(
84-
InternalError::FundingTxGenerationFailed(format!("{} feerate: {}", err, feerate)),
85-
),
86-
APIError::InvalidRoute { err } => {
87-
Self::Internal(InternalError::FundingTxGenerationFailed(format!(
88-
"Invalid route provided: {}",
89-
err
90-
)))
91-
},
92-
APIError::ChannelUnavailable { err } => Self::Internal(
93-
InternalError::FundingTxGenerationFailed(format!("Channel unavailable: {}", err)),
94-
),
95-
APIError::MonitorUpdateInProgress => {
96-
Self::Internal(InternalError::FundingTxGenerationFailed(
97-
"Client indicated a channel monitor update is in progress but not yet complete"
98-
.to_string(),
99-
))
100-
},
101-
APIError::IncompatibleShutdownScript { script } => {
102-
Self::Internal(InternalError::FundingTxGenerationFailed(format!(
103-
"Provided a scriptpubkey format not accepted by peer: {}",
104-
script
105-
)))
106-
},
107-
}
108-
}
109-
}
74+
// // Maybe this should live in LDK-Node instead?
75+
// impl From<APIError> for Error {
76+
// fn from(value: APIError) -> Self {
77+
// match value {
78+
// APIError::APIMisuseError { err } => {
79+
// Self::Internal(InternalError::FundingTxGenerationFailed(err))
80+
// },
81+
// APIError::FeeRateTooHigh { err, feerate } => Self::Internal(
82+
// InternalError::FundingTxGenerationFailed(format!("{} feerate: {}", err, feerate)),
83+
// ),
84+
// APIError::InvalidRoute { err } => {
85+
// Self::Internal(InternalError::FundingTxGenerationFailed(format!(
86+
// "Invalid route provided: {}",
87+
// err
88+
// )))
89+
// },
90+
// APIError::ChannelUnavailable { err } => Self::Internal(
91+
// InternalError::FundingTxGenerationFailed(format!("Channel unavailable: {}", err)),
92+
// ),
93+
// APIError::MonitorUpdateInProgress => {
94+
// Self::Internal(InternalError::FundingTxGenerationFailed(
95+
// "Client indicated a channel monitor update is in progress but not yet complete"
96+
// .to_string(),
97+
// ))
98+
// },
99+
// APIError::IncompatibleShutdownScript { script } => {
100+
// Self::Internal(InternalError::FundingTxGenerationFailed(format!(
101+
// "Provided a scriptpubkey format not accepted by peer: {}",
102+
// script
103+
// )))
104+
// },
105+
// }
106+
// }
107+
// }
110108

111109
impl std::error::Error for Error {}

src/error.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,19 @@ impl From<lightning_transaction_sync::TxSyncError> for Error {
139139
Self::TxSyncFailed
140140
}
141141
}
142+
143+
impl From<Error> for lightning_payjoin::error::Error {
144+
fn from(e: Error) -> Self {
145+
match e {
146+
Error::WalletOperationFailed => lightning_payjoin::error::Error::Internal(
147+
lightning_payjoin::error::InternalError::WalletOperationFailed,
148+
),
149+
Error::OnchainTxSigningFailed => lightning_payjoin::error::Error::Internal(
150+
lightning_payjoin::error::InternalError::OnchainTxSigningFailed,
151+
),
152+
_ => lightning_payjoin::error::Error::Internal(
153+
lightning_payjoin::error::InternalError::NodeFailed,
154+
),
155+
}
156+
}
157+
}

src/event.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ where
706706
}
707707
},
708708
LdkEvent::SpendableOutputs { outputs, channel_id } => {
709-
self.output_sweeper.track_spendable_outputs(outputs, channel_id, true, None)
709+
self.output_sweeper.track_spendable_outputs(outputs, channel_id, true, None);
710710
},
711711
LdkEvent::OpenChannelRequest {
712712
temporary_channel_id,

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ use error::Error;
109109

110110
pub use event::Event;
111111
pub use types::ChannelConfig;
112+
mod payjoin_handler;
112113

113114
pub use io::utils::generate_entropy_mnemonic;
114115

src/payjoin_handler.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use crate::types::{ChannelManager, Wallet};
2+
use bitcoin::secp256k1::PublicKey;
3+
use lightning::ln::ChannelId;
4+
use lightning_payjoin::error::Error as PayjoinError;
5+
use lightning_payjoin::scheduler::{ChannelScheduler, ScheduledChannel};
6+
use lightning_payjoin::{LightningPayjoin, ProvisionalProposal, UncheckedProposal};
7+
use std::sync::Arc;
8+
use tokio::sync::Mutex;
9+
10+
#[derive(Clone)]
11+
pub(crate) struct PayjoinHandler {
12+
inner: Arc<Mutex<LightningPayjoin>>,
13+
scheduler: Arc<Mutex<ChannelScheduler>>,
14+
wallet: Arc<Wallet>,
15+
channel_manager: Arc<ChannelManager>,
16+
}
17+
18+
impl PayjoinHandler {
19+
pub(crate) fn new(
20+
wallet: Arc<Wallet>,
21+
channel_manager: Arc<ChannelManager>,
22+
scheduler: Arc<Mutex<ChannelScheduler>>,
23+
payjoin_directory: lightning_payjoin::Url,
24+
payjoin_relay: lightning_payjoin::Url,
25+
) -> Self {
26+
let lightning_payjoin = LightningPayjoin::new(
27+
payjoin_directory,
28+
payjoin_relay,
29+
);
30+
let inner = Arc::new(Mutex::new(lightning_payjoin));
31+
Self { inner, scheduler, wallet, channel_manager }
32+
}
33+
34+
pub(crate) async fn process_request(&self) -> Result<(), PayjoinError> {
35+
let mut inner = self.inner.lock().await;
36+
let proposal = match inner.fetch_payjoin_proposal().await {
37+
Ok(Some(proposal)) => proposal,
38+
Ok(None) => return Ok(()),
39+
Err(e) => return Err(e),
40+
};
41+
let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1);
42+
let provisional_proposal = self.validate_payjoin_request(proposal, min_fee_rate).await.unwrap();
43+
let amt = bitcoin::Amount::from_sat(provisional_proposal.payjoin_amount());
44+
let channel = self.scheduler.lock().await.get_next_channel(amt).cloned();
45+
let finalized_proposal = match channel {
46+
Some(channel) => {
47+
let finalized_proposal = inner
48+
.accept_payjoin_with_channel_opening(provisional_proposal, channel.clone())
49+
.await
50+
.unwrap();
51+
self.scheduler.lock().await.set_funding_tx_created(
52+
channel.channel_id(),
53+
finalized_proposal.psbt().clone(),
54+
);
55+
let tx = finalized_proposal.psbt().clone().extract_tx();
56+
let _ = self.channel_manager.funding_transaction_generated(
57+
&ChannelId::from_bytes(channel.temporary_channel_id().unwrap()),
58+
&channel.counterparty_node_id(),
59+
tx.clone(),
60+
);
61+
let _ = self.wait_for_funding_tx_signed(tx.txid());
62+
finalized_proposal
63+
},
64+
None => inner.accept_payjoin_without_channel_opening(provisional_proposal).unwrap(),
65+
};
66+
inner.send_response(finalized_proposal).await
67+
}
68+
69+
pub(crate) async fn payjoin_uri(&self, amount: bitcoin::Amount) -> String {
70+
let address = self.wallet.get_new_address().unwrap();
71+
self.inner.lock().await.construct_payjoin_uri(amount, address)
72+
}
73+
74+
pub(crate) async fn schedule_channel(
75+
&self, amount: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128,
76+
) {
77+
let channel = ScheduledChannel::new(amount, counterparty_node_id, channel_id);
78+
self.scheduler.lock().await.schedule(
79+
channel.channel_value_satoshi(),
80+
channel.counterparty_node_id(),
81+
channel.channel_id(),
82+
);
83+
}
84+
85+
pub(crate) async fn list_scheduled_channels(&self) -> Vec<ScheduledChannel> {
86+
self.scheduler.lock().await.list_channels().clone()
87+
}
88+
89+
async fn wait_for_funding_tx_signed(&self, txid: bitcoin::Txid)-> Result<(), PayjoinError> {
90+
let channel_scheduler = self.scheduler.clone();
91+
let is_funding_signed =
92+
tokio::time::timeout(tokio::time::Duration::from_secs(3), async move {
93+
loop {
94+
if channel_scheduler.lock().await.is_funding_tx_signed(&txid) {
95+
break;
96+
}
97+
}
98+
})
99+
.await;
100+
match is_funding_signed {
101+
Ok(_) => {
102+
return Ok(());
103+
},
104+
Err(_) => {
105+
panic!("Funding tx not signed")
106+
},
107+
}
108+
}
109+
110+
pub async fn validate_payjoin_request(
111+
&self,
112+
proposal: UncheckedProposal,
113+
min_fee_rate: Option<bitcoin::FeeRate>,
114+
) -> Result<ProvisionalProposal, PayjoinError> {
115+
let proposal = {
116+
let tx = proposal.extract_tx_to_schedule_broadcast();
117+
let verified = match self.wallet.verify_tx(tx).await {
118+
Ok(_verified) => true,
119+
Err(_e) => false
120+
};
121+
let proposal = proposal.check_broadcast_suitability(min_fee_rate, |_t| { Ok(verified) }).unwrap();
122+
proposal
123+
};
124+
let proposal = proposal.check_inputs_not_owned(|script| {
125+
Ok(self.wallet.is_mine(&script.to_owned()).unwrap())
126+
})?;
127+
let proposal = proposal.check_no_mixed_input_scripts()?;
128+
let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false))?;
129+
let original_proposal = proposal.clone().identify_receiver_outputs(|script| {
130+
Ok(self.wallet.is_mine(&script.to_owned()).unwrap())
131+
})?;
132+
Ok(original_proposal)
133+
}
134+
}

0 commit comments

Comments
 (0)