Skip to content

Commit bec9439

Browse files
committed
Allow to send payjoin transactions
Implements the payjoin sender as describe in BIP77. This would allow the on chain wallet linked to LDK node to send payjoin transactions.
1 parent 8dd3790 commit bec9439

File tree

14 files changed

+838
-6
lines changed

14 files changed

+838
-6
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr
6868
esplora-client = { version = "0.6", default-features = false }
6969
libc = "0.2"
7070
uniffi = { version = "0.26.0", features = ["build"], optional = true }
71+
payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2"] }
7172

7273
[target.'cfg(vss)'.dependencies]
7374
vss-client = "0.2"

bindings/ldk_node.udl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ interface Node {
6363
Bolt12Payment bolt12_payment();
6464
SpontaneousPayment spontaneous_payment();
6565
OnchainPayment onchain_payment();
66+
PayjoinPayment payjoin_payment();
6667
[Throws=NodeError]
6768
void connect(PublicKey node_id, SocketAddress address, boolean persist);
6869
[Throws=NodeError]
@@ -148,6 +149,13 @@ interface OnchainPayment {
148149
Txid send_all_to_address([ByRef]Address address);
149150
};
150151

152+
interface PayjoinPayment {
153+
[Throws=NodeError]
154+
void send(string payjoin_uri);
155+
[Throws=NodeError]
156+
void send_with_amount(string payjoin_uri, u64 amount_sats);
157+
};
158+
151159
[Error]
152160
enum NodeError {
153161
"AlreadyRunning",
@@ -196,6 +204,12 @@ enum NodeError {
196204
"InsufficientFunds",
197205
"LiquiditySourceUnavailable",
198206
"LiquidityFeeTooHigh",
207+
"PayjoinUnavailable",
208+
"PayjoinUriInvalid",
209+
"PayjoinRequestMissingAmount",
210+
"PayjoinRequestCreationFailed",
211+
"PayjoinRequestSendingFailed",
212+
"PayjoinResponseProcessingFailed",
199213
};
200214

201215
dictionary NodeStatus {
@@ -227,6 +241,7 @@ enum BuildError {
227241
"KVStoreSetupFailed",
228242
"WalletSetupFailed",
229243
"LoggerSetupFailed",
244+
"InvalidPayjoinConfig",
230245
};
231246

232247
[Enum]
@@ -238,6 +253,9 @@ interface Event {
238253
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
239254
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
240255
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
256+
PayjoinPaymentPending(Txid txid, u64 amount, ScriptBuf receipient);
257+
PayjoinPaymentSuccess(Txid txid, u64 amount, ScriptBuf receipient);
258+
PayjoinPaymentFailed(Txid? txid, u64 amount, ScriptBuf receipient, PayjoinPaymentFailureReason reason);
241259
};
242260

243261
enum PaymentFailureReason {
@@ -249,6 +267,12 @@ enum PaymentFailureReason {
249267
"UnexpectedError",
250268
};
251269

270+
enum PayjoinPaymentFailureReason {
271+
"Timeout",
272+
"TransactionFinalisationFailed",
273+
"InvalidReceiverResponse",
274+
};
275+
252276
[Enum]
253277
interface ClosureReason {
254278
CounterpartyForceClosed(UntrustedString peer_msg);
@@ -274,6 +298,7 @@ interface PaymentKind {
274298
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id);
275299
Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret);
276300
Spontaneous(PaymentHash hash, PaymentPreimage? preimage);
301+
Payjoin();
277302
};
278303

279304
enum PaymentDirection {
@@ -499,3 +524,6 @@ typedef string Mnemonic;
499524

500525
[Custom]
501526
typedef string UntrustedString;
527+
528+
[Custom]
529+
typedef string ScriptBuf;

src/builder.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::io::sqlite_store::SqliteStore;
1111
use crate::liquidity::LiquiditySource;
1212
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
1313
use crate::message_handler::NodeCustomMessageHandler;
14+
use crate::payment::payjoin::handler::PayjoinHandler;
1415
use crate::payment::store::PaymentStore;
1516
use crate::peer_store::PeerStore;
1617
use crate::tx_broadcaster::TransactionBroadcaster;
@@ -93,6 +94,11 @@ struct LiquiditySourceConfig {
9394
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
9495
}
9596

97+
#[derive(Debug, Clone)]
98+
struct PayjoinConfig {
99+
payjoin_relay: payjoin::Url,
100+
}
101+
96102
impl Default for LiquiditySourceConfig {
97103
fn default() -> Self {
98104
Self { lsps2_service: None }
@@ -132,6 +138,8 @@ pub enum BuildError {
132138
WalletSetupFailed,
133139
/// We failed to setup the logger.
134140
LoggerSetupFailed,
141+
/// Invalid Payjoin configuration.
142+
InvalidPayjoinConfig,
135143
}
136144

137145
impl fmt::Display for BuildError {
@@ -152,6 +160,10 @@ impl fmt::Display for BuildError {
152160
Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."),
153161
Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."),
154162
Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
163+
Self::InvalidPayjoinConfig => write!(
164+
f,
165+
"Invalid Payjoin configuration. Make sure the provided arguments are valid URLs."
166+
),
155167
}
156168
}
157169
}
@@ -172,6 +184,7 @@ pub struct NodeBuilder {
172184
chain_data_source_config: Option<ChainDataSourceConfig>,
173185
gossip_source_config: Option<GossipSourceConfig>,
174186
liquidity_source_config: Option<LiquiditySourceConfig>,
187+
payjoin_config: Option<PayjoinConfig>,
175188
}
176189

177190
impl NodeBuilder {
@@ -187,12 +200,14 @@ impl NodeBuilder {
187200
let chain_data_source_config = None;
188201
let gossip_source_config = None;
189202
let liquidity_source_config = None;
203+
let payjoin_config = None;
190204
Self {
191205
config,
192206
entropy_source_config,
193207
chain_data_source_config,
194208
gossip_source_config,
195209
liquidity_source_config,
210+
payjoin_config,
196211
}
197212
}
198213

@@ -247,6 +262,14 @@ impl NodeBuilder {
247262
self
248263
}
249264

265+
/// Configures the [`Node`] instance to enable payjoin transactions.
266+
pub fn set_payjoin_config(&mut self, payjoin_relay: String) -> Result<&mut Self, BuildError> {
267+
let payjoin_relay =
268+
payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?;
269+
self.payjoin_config = Some(PayjoinConfig { payjoin_relay });
270+
Ok(self)
271+
}
272+
250273
/// Configures the [`Node`] instance to source its inbound liquidity from the given
251274
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
252275
/// service.
@@ -365,6 +388,7 @@ impl NodeBuilder {
365388
self.chain_data_source_config.as_ref(),
366389
self.gossip_source_config.as_ref(),
367390
self.liquidity_source_config.as_ref(),
391+
self.payjoin_config.as_ref(),
368392
seed_bytes,
369393
logger,
370394
vss_store,
@@ -386,6 +410,7 @@ impl NodeBuilder {
386410
self.chain_data_source_config.as_ref(),
387411
self.gossip_source_config.as_ref(),
388412
self.liquidity_source_config.as_ref(),
413+
self.payjoin_config.as_ref(),
389414
seed_bytes,
390415
logger,
391416
kv_store,
@@ -453,6 +478,11 @@ impl ArcedNodeBuilder {
453478
self.inner.write().unwrap().set_gossip_source_p2p();
454479
}
455480

481+
/// Configures the [`Node`] instance to enable payjoin transactions.
482+
pub fn set_payjoin_config(&self, payjoin_relay: String) -> Result<(), BuildError> {
483+
self.inner.write().unwrap().set_payjoin_config(payjoin_relay).map(|_| ())
484+
}
485+
456486
/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
457487
/// server.
458488
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -521,8 +551,9 @@ impl ArcedNodeBuilder {
521551
fn build_with_store_internal(
522552
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
523553
gossip_source_config: Option<&GossipSourceConfig>,
524-
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
525-
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
554+
liquidity_source_config: Option<&LiquiditySourceConfig>,
555+
payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc<FilesystemLogger>,
556+
kv_store: Arc<DynStore>,
526557
) -> Result<Node, BuildError> {
527558
// Initialize the on-chain wallet and chain access
528559
let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
@@ -966,6 +997,16 @@ fn build_with_store_internal(
966997
let (stop_sender, _) = tokio::sync::watch::channel(());
967998
let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(());
968999

1000+
let payjoin_handler = payjoin_config.map(|pj_config| {
1001+
Arc::new(PayjoinHandler::new(
1002+
Arc::clone(&tx_sync),
1003+
Arc::clone(&event_queue),
1004+
pj_config.payjoin_relay.clone(),
1005+
Arc::clone(&payment_store),
1006+
Arc::clone(&wallet),
1007+
))
1008+
});
1009+
9691010
let is_listening = Arc::new(AtomicBool::new(false));
9701011
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
9711012
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -987,6 +1028,7 @@ fn build_with_store_internal(
9871028
channel_manager,
9881029
chain_monitor,
9891030
output_sweeper,
1031+
payjoin_handler,
9901032
peer_manager,
9911033
connection_manager,
9921034
keys_manager,

src/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ pub(crate) const RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL: u32 = 6;
4040
// The time in-between peer reconnection attempts.
4141
pub(crate) const PEER_RECONNECTION_INTERVAL: Duration = Duration::from_secs(10);
4242

43+
// The time before payjoin sender requests timeout.
44+
pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
45+
46+
// The time before payjoin sender try to send the next request.
47+
pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3);
48+
49+
// The total time payjoin sender try to send a request.
50+
pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
51+
4352
// The time in-between RGS sync attempts.
4453
pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60);
4554

src/error.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ pub enum Error {
9595
LiquiditySourceUnavailable,
9696
/// The given operation failed due to the LSP's required opening fee being too high.
9797
LiquidityFeeTooHigh,
98+
/// Failed to access Payjoin object.
99+
PayjoinUnavailable,
100+
/// Payjoin URI is invalid.
101+
PayjoinUriInvalid,
102+
/// Amount is neither user-provided nor defined in the URI.
103+
PayjoinRequestMissingAmount,
104+
/// Failed to build a Payjoin request.
105+
PayjoinRequestCreationFailed,
106+
/// Failed to send Payjoin request.
107+
PayjoinRequestSendingFailed,
108+
/// Payjoin response processing failed.
109+
PayjoinResponseProcessingFailed,
98110
}
99111

100112
impl fmt::Display for Error {
@@ -162,6 +174,27 @@ impl fmt::Display for Error {
162174
Self::LiquidityFeeTooHigh => {
163175
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
164176
},
177+
Self::PayjoinUnavailable => {
178+
write!(
179+
f,
180+
"Failed to access Payjoin object. Make sure you have enabled Payjoin support."
181+
)
182+
},
183+
Self::PayjoinRequestMissingAmount => {
184+
write!(f, "Amount is neither user-provided nor defined in the URI.")
185+
},
186+
Self::PayjoinRequestCreationFailed => {
187+
write!(f, "Failed construct a Payjoin request")
188+
},
189+
Self::PayjoinUriInvalid => {
190+
write!(f, "The provided Payjoin URI is invalid")
191+
},
192+
Self::PayjoinRequestSendingFailed => {
193+
write!(f, "Failed to send Payjoin request")
194+
},
195+
Self::PayjoinResponseProcessingFailed => {
196+
write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored")
197+
},
165198
}
166199
}
167200
}

src/event.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use lightning::events::{ClosureReason, PaymentPurpose};
2424
use lightning::events::{Event as LdkEvent, PaymentFailureReason};
2525
use lightning::impl_writeable_tlv_based_enum;
2626
use lightning::ln::channelmanager::PaymentId;
27+
use lightning::ln::msgs::DecodeError;
2728
use lightning::ln::{ChannelId, PaymentHash};
2829
use lightning::routing::gossip::NodeId;
2930
use lightning::util::errors::APIError;
@@ -143,6 +144,73 @@ pub enum Event {
143144
/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
144145
reason: Option<ClosureReason>,
145146
},
147+
/// Failed to send Payjoin transaction.
148+
///
149+
/// This event is emitted when our attempt to send Payjoin transaction fail.
150+
PayjoinPaymentPending {
151+
/// Transaction ID of the successfully sent Payjoin transaction.
152+
txid: bitcoin::Txid,
153+
/// docs
154+
amount: u64,
155+
/// docs
156+
receipient: bitcoin::ScriptBuf,
157+
},
158+
/// A Payjoin transaction has been successfully sent.
159+
///
160+
/// This event is emitted when we send a Payjoin transaction and it was accepted by the
161+
/// receiver, and then finalised and broadcasted by us.
162+
PayjoinPaymentSuccess {
163+
/// Transaction ID of the successfully sent Payjoin transaction.
164+
txid: bitcoin::Txid,
165+
/// docs
166+
amount: u64,
167+
/// docs
168+
receipient: bitcoin::ScriptBuf,
169+
},
170+
/// Failed to send Payjoin transaction.
171+
///
172+
/// This event is emitted when our attempt to send Payjoin transaction fail.
173+
PayjoinPaymentFailed {
174+
/// Transaction ID of the successfully sent Payjoin transaction.
175+
txid: Option<bitcoin::Txid>,
176+
/// docs
177+
amount: u64,
178+
/// docs
179+
receipient: bitcoin::ScriptBuf,
180+
/// Reason for the failure.
181+
reason: PayjoinPaymentFailureReason,
182+
},
183+
}
184+
185+
#[derive(Debug, Clone, PartialEq, Eq)]
186+
pub enum PayjoinPaymentFailureReason {
187+
Timeout,
188+
TransactionFinalisationFailed,
189+
InvalidReceiverResponse,
190+
RequestFailed,
191+
}
192+
193+
impl Readable for PayjoinPaymentFailureReason {
194+
fn read<R: std::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
195+
match u8::read(reader)? {
196+
0 => Ok(Self::Timeout),
197+
1 => Ok(Self::TransactionFinalisationFailed),
198+
2 => Ok(Self::InvalidReceiverResponse),
199+
3 => Ok(Self::RequestFailed),
200+
_ => Err(DecodeError::InvalidValue),
201+
}
202+
}
203+
}
204+
205+
impl Writeable for PayjoinPaymentFailureReason {
206+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), std::io::Error> {
207+
match *self {
208+
Self::Timeout => 0u8.write(writer),
209+
Self::TransactionFinalisationFailed => 1u8.write(writer),
210+
Self::InvalidReceiverResponse => 2u8.write(writer),
211+
Self::RequestFailed => 3u8.write(writer),
212+
}
213+
}
146214
}
147215

148216
impl_writeable_tlv_based_enum!(Event,
@@ -184,6 +252,22 @@ impl_writeable_tlv_based_enum!(Event,
184252
(2, payment_id, required),
185253
(4, claimable_amount_msat, required),
186254
(6, claim_deadline, option),
255+
},
256+
(7, PayjoinPaymentPending) => {
257+
(0, txid, required),
258+
(2, amount, required),
259+
(4, receipient, required),
260+
},
261+
(8, PayjoinPaymentSuccess) => {
262+
(0, txid, required),
263+
(2, amount, required),
264+
(4, receipient, required),
265+
},
266+
(9, PayjoinPaymentFailed) => {
267+
(0, amount, required),
268+
(1, txid, option),
269+
(2, receipient, required),
270+
(4, reason, required),
187271
};
188272
);
189273

0 commit comments

Comments
 (0)