Skip to content

Commit 936e968

Browse files
committed
Add async payments
1 parent 0527d72 commit 936e968

File tree

10 files changed

+365
-8
lines changed

10 files changed

+365
-8
lines changed

bindings/ldk_node.udl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ interface Node {
157157
boolean verify_signature([ByRef]sequence<u8> msg, [ByRef]string sig, [ByRef]PublicKey pkey);
158158
[Throws=NodeError]
159159
bytes export_pathfinding_scores();
160+
[Throws=NodeError]
161+
void set_paths_to_static_invoice_server(sequence<u8> paths);
162+
[Throws=NodeError]
163+
sequence<u8> blinded_paths_for_async_recipient(sequence<u8> recipient_id);
160164
};
161165

162166
[Enum]
@@ -209,6 +213,8 @@ interface Bolt12Payment {
209213
Bolt12Invoice request_refund_payment([ByRef]Refund refund);
210214
[Throws=NodeError]
211215
Refund initiate_refund(u64 amount_msat, u32 expiry_secs, u64? quantity, string? payer_note);
216+
[Throws=NodeError]
217+
Offer get_async_receive_offer();
212218
};
213219

214220
interface SpontaneousPayment {
@@ -311,6 +317,8 @@ enum NodeError {
311317
"InsufficientFunds",
312318
"LiquiditySourceUnavailable",
313319
"LiquidityFeeTooHigh",
320+
"InvalidBlindedPaths",
321+
"OperationFailed",
314322
};
315323

316324
dictionary NodeStatus {

src/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1455,7 +1455,7 @@ fn build_with_store_internal(
14551455
Arc::clone(&channel_manager),
14561456
message_router,
14571457
Arc::clone(&channel_manager),
1458-
IgnoringMessageHandler {},
1458+
Arc::clone(&channel_manager),
14591459
IgnoringMessageHandler {},
14601460
IgnoringMessageHandler {},
14611461
));

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ pub enum Error {
120120
LiquiditySourceUnavailable,
121121
/// The given operation failed due to the LSP's required opening fee being too high.
122122
LiquidityFeeTooHigh,
123+
/// The given blinded paths are invalid.
124+
InvalidBlindedPaths,
125+
/// The requested operation failed.
126+
OperationFailed,
123127
}
124128

125129
impl fmt::Display for Error {
@@ -193,6 +197,8 @@ impl fmt::Display for Error {
193197
Self::LiquidityFeeTooHigh => {
194198
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
195199
},
200+
Self::InvalidBlindedPaths => write!(f, "The given blinded paths are invalid."),
201+
Self::OperationFailed => write!(f, "The requested operation failed."),
196202
}
197203
}
198204
}

src/event.rs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
77

8+
use crate::static_invoice_store::StaticInvoiceStore;
89
use crate::types::{CustomTlvRecord, DynStore, PaymentStore, Sweeper, Wallet};
910

1011
use crate::{
@@ -458,6 +459,7 @@ where
458459
runtime: Arc<Runtime>,
459460
logger: L,
460461
config: Arc<Config>,
462+
static_invoice_store: StaticInvoiceStore,
461463
}
462464

463465
impl<L: Deref + Clone + Sync + Send + 'static> EventHandler<L>
@@ -470,8 +472,9 @@ where
470472
channel_manager: Arc<ChannelManager>, connection_manager: Arc<ConnectionManager<L>>,
471473
output_sweeper: Arc<Sweeper>, network_graph: Arc<Graph>,
472474
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
473-
payment_store: Arc<PaymentStore>, peer_store: Arc<PeerStore<L>>, runtime: Arc<Runtime>,
474-
logger: L, config: Arc<Config>,
475+
payment_store: Arc<PaymentStore>, peer_store: Arc<PeerStore<L>>,
476+
static_invoice_store: StaticInvoiceStore, runtime: Arc<Runtime>, logger: L,
477+
config: Arc<Config>,
475478
) -> Self {
476479
Self {
477480
event_queue,
@@ -487,6 +490,7 @@ where
487490
logger,
488491
runtime,
489492
config,
493+
static_invoice_store,
490494
}
491495
}
492496

@@ -1494,11 +1498,45 @@ where
14941498
LdkEvent::OnionMessagePeerConnected { .. } => {
14951499
debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted.");
14961500
},
1497-
LdkEvent::PersistStaticInvoice { .. } => {
1498-
debug_assert!(false, "We currently don't support static invoice persistence, so this event should never be emitted.");
1501+
1502+
LdkEvent::PersistStaticInvoice {
1503+
invoice,
1504+
invoice_slot,
1505+
recipient_id,
1506+
invoice_persisted_path,
1507+
} => {
1508+
match self
1509+
.static_invoice_store
1510+
.handle_persist_static_invoice(invoice, invoice_slot, recipient_id)
1511+
.await
1512+
{
1513+
Ok(_) => {},
1514+
Err(e) => {
1515+
log_error!(self.logger, "Failed to persist static invoice: {}", e);
1516+
},
1517+
};
1518+
1519+
self.channel_manager.static_invoice_persisted(invoice_persisted_path);
14991520
},
1500-
LdkEvent::StaticInvoiceRequested { .. } => {
1501-
debug_assert!(false, "We currently don't support static invoice persistence, so this event should never be emitted.");
1521+
LdkEvent::StaticInvoiceRequested { recipient_id, invoice_slot, reply_path } => {
1522+
let invoice = self
1523+
.static_invoice_store
1524+
.handle_static_invoice_requested(recipient_id, invoice_slot)
1525+
.await;
1526+
1527+
match invoice {
1528+
Ok(Some(invoice)) => {
1529+
if let Err(_) =
1530+
self.channel_manager.send_static_invoice(invoice, reply_path)
1531+
{
1532+
log_error!(self.logger, "Failed to send static invoice");
1533+
}
1534+
},
1535+
Ok(None) => {},
1536+
Err(e) => {
1537+
log_error!(self.logger, "Failed to retrieve static invoice: {}", e);
1538+
},
1539+
}
15021540
},
15031541
LdkEvent::FundingTransactionReadyForSigning { .. } => {
15041542
debug_assert!(false, "We currently don't support interactive-tx, so this event should never be emitted.");

src/lib.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ pub mod logger;
9494
mod message_handler;
9595
pub mod payment;
9696
mod peer_store;
97+
mod rate_limiter;
9798
mod runtime;
99+
mod static_invoice_store;
98100
mod tx_broadcaster;
99101
mod types;
100102
mod wallet;
@@ -150,13 +152,16 @@ pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId};
150152

151153
use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
152154

155+
use lightning::blinded_path::message::BlindedMessagePath;
153156
use lightning::chain::BestBlock;
154157
use lightning::events::bump_transaction::Wallet as LdkWallet;
155158
use lightning::impl_writeable_tlv_based;
156159
use lightning::ln::channel_state::ChannelShutdownState;
157160
use lightning::ln::channelmanager::PaymentId;
158161
use lightning::ln::msgs::SocketAddress;
159162
use lightning::routing::gossip::NodeAlias;
163+
use lightning::util::ser::Readable;
164+
use lightning::util::ser::Writeable;
160165

161166
use lightning_background_processor::process_events_async_with_kv_store_sync;
162167

@@ -170,6 +175,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
170175
use std::sync::{Arc, Mutex, RwLock};
171176
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
172177

178+
use crate::static_invoice_store::StaticInvoiceStore;
179+
173180
#[cfg(feature = "uniffi")]
174181
uniffi::include_scaffolding!("ldk_node");
175182

@@ -498,6 +505,8 @@ impl Node {
498505
Arc::clone(&self.logger),
499506
));
500507

508+
let static_invoice_store = StaticInvoiceStore::new(Arc::clone(&self.kv_store));
509+
501510
let event_handler = Arc::new(EventHandler::new(
502511
Arc::clone(&self.event_queue),
503512
Arc::clone(&self.wallet),
@@ -509,6 +518,7 @@ impl Node {
509518
self.liquidity_source.clone(),
510519
Arc::clone(&self.payment_store),
511520
Arc::clone(&self.peer_store),
521+
static_invoice_store,
512522
Arc::clone(&self.runtime),
513523
Arc::clone(&self.logger),
514524
Arc::clone(&self.config),
@@ -1476,6 +1486,39 @@ impl Node {
14761486
Error::PersistenceFailed
14771487
})
14781488
}
1489+
1490+
/// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build [`Offer`]s with a
1491+
/// static invoice server, so the server can serve [`StaticInvoice`]s to payers on our behalf when we're offline.
1492+
///
1493+
/// [`Offer`]: lightning::offers::offer::Offer
1494+
/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
1495+
pub fn set_paths_to_static_invoice_server(&self, paths: Vec<u8>) -> Result<(), Error> {
1496+
let decoded_paths = <Vec<BlindedMessagePath> as Readable>::read(&mut &paths[..])
1497+
.or(Err(Error::InvalidBlindedPaths))?;
1498+
1499+
self.channel_manager
1500+
.set_paths_to_static_invoice_server(decoded_paths)
1501+
.or(Err(Error::InvalidBlindedPaths))
1502+
}
1503+
1504+
/// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively
1505+
/// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments.
1506+
///
1507+
/// [`Offer`]: lightning::offers::offer::Offer
1508+
/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
1509+
pub fn blinded_paths_for_async_recipient(
1510+
&self, recipient_id: Vec<u8>,
1511+
) -> Result<Vec<u8>, Error> {
1512+
let paths = self
1513+
.channel_manager
1514+
.blinded_paths_for_async_recipient(recipient_id, None)
1515+
.or(Err(Error::OperationFailed))?;
1516+
1517+
let mut bytes = Vec::new();
1518+
paths.write(&mut bytes).or(Err(Error::OperationFailed))?;
1519+
1520+
Ok(bytes)
1521+
}
14791522
}
14801523

14811524
impl Drop for Node {

src/payment/bolt12.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,4 +450,19 @@ impl Bolt12Payment {
450450

451451
Ok(maybe_wrap(refund))
452452
}
453+
454+
/// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. Will only return an offer if
455+
/// [`Node::set_paths_to_static_invoice_server`] was called and we succeeded in interactively building a
456+
/// [`StaticInvoice`] with the static invoice server.
457+
///
458+
/// Useful for posting offers to receive payments later, such as posting an offer on a website.
459+
///
460+
/// [`Node::set_paths_to_static_invoice_server`]: crate::Node::set_paths_to_static_invoice_server
461+
/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
462+
pub fn get_async_receive_offer(&self) -> Result<Offer, Error> {
463+
self.channel_manager
464+
.get_async_receive_offer()
465+
.map(maybe_wrap)
466+
.or(Err(Error::OfferCreationFailed))
467+
}
453468
}

src/rate_limiter.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use std::collections::HashMap;
2+
use std::time::{Duration, Instant};
3+
4+
/// Implements a leaky-bucket style rate limiter parameterized by the max capacity of the bucket, the refill interval,
5+
/// and the max idle duration. For every passing of the refill interval, one token is added to the bucket, up to the
6+
/// maximum capacity. When the bucket has remained at the maximum capacity for longer than the max idle duration, it is
7+
/// removed to prevent memory leakage.
8+
pub(crate) struct RateLimiter {
9+
users: HashMap<Vec<u8>, Bucket>,
10+
capacity: u32,
11+
refill_interval: Duration,
12+
max_idle: Duration,
13+
}
14+
15+
struct Bucket {
16+
tokens: u32,
17+
last_refill: Instant,
18+
}
19+
20+
impl RateLimiter {
21+
pub(crate) fn new(capacity: u32, refill_interval: Duration, max_idle: Duration) -> Self {
22+
Self { users: HashMap::new(), capacity, refill_interval, max_idle }
23+
}
24+
25+
pub(crate) fn allow(&mut self, user_id: &[u8]) -> bool {
26+
let now = Instant::now();
27+
28+
let entry = self.users.entry(user_id.to_vec());
29+
let is_new_user = matches!(entry, std::collections::hash_map::Entry::Vacant(_));
30+
31+
let bucket = entry.or_insert(Bucket { tokens: self.capacity, last_refill: now });
32+
33+
let elapsed = now.duration_since(bucket.last_refill);
34+
let tokens_to_add = (elapsed.as_secs_f64() / self.refill_interval.as_secs_f64()) as u32;
35+
36+
if tokens_to_add > 0 {
37+
bucket.tokens = (bucket.tokens + tokens_to_add).min(self.capacity);
38+
bucket.last_refill = now;
39+
}
40+
41+
let allow = if bucket.tokens > 0 {
42+
bucket.tokens -= 1;
43+
true
44+
} else {
45+
false
46+
};
47+
48+
// Each time a new user is added, we take the opportunity to clean up old rate limits.
49+
if is_new_user {
50+
self.garbage_collect(self.max_idle);
51+
}
52+
53+
allow
54+
}
55+
56+
fn garbage_collect(&mut self, max_idle: Duration) {
57+
let now = Instant::now();
58+
self.users.retain(|_, bucket| now.duration_since(bucket.last_refill) < max_idle);
59+
}
60+
}

0 commit comments

Comments
 (0)