Skip to content

Commit c5b6756

Browse files
feat: Add Bolt11Invoice interface and wrapper implementation for FFI bindings
- Convert Bolt11Invoice from string type to full interface in UDL - Define required Bolt11Invoice methods in the UDL interface (expiry_time_seconds, min_final_cltv_expiry_delta, amount_milli_satoshis, is_expired, etc.) - Create Bolt11Invoice struct in uniffi_types.rs to wrap LdkBolt11Invoice - Implement methods required by the UDL interface - Add From/Into implementations for conversion between wrapper and LDK types - Add tests for the wrapper struct
1 parent 1525255 commit c5b6756

File tree

2 files changed

+186
-26
lines changed

2 files changed

+186
-26
lines changed

bindings/ldk_node.udl

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,20 @@ dictionary NodeAnnouncementInfo {
698698
sequence<SocketAddress> addresses;
699699
};
700700

701+
interface Bolt11Invoice {
702+
[Throws=NodeError, Name=from_str]
703+
constructor([ByRef] string invoice_str);
704+
sequence<u8> signable_hash();
705+
u64 expiry_time_seconds();
706+
u64 min_final_cltv_expiry_delta();
707+
u64? amount_milli_satoshis();
708+
boolean is_expired();
709+
u64 seconds_since_epoch();
710+
boolean would_expire(u64 at_time_seconds);
711+
u64 seconds_until_expiry();
712+
PaymentHash payment_hash();
713+
};
714+
701715
[Custom]
702716
typedef string Txid;
703717

@@ -716,9 +730,6 @@ typedef string NodeId;
716730
[Custom]
717731
typedef string Address;
718732

719-
[Custom]
720-
typedef string Bolt11Invoice;
721-
722733
[Custom]
723734
typedef string Offer;
724735

src/uniffi_types.rs

Lines changed: 172 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,10 @@ pub use lightning::util::string::UntrustedString;
3333

3434
pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
3535

36-
pub use lightning_invoice::{Bolt11Invoice, Description};
36+
pub use lightning_invoice::{Description, SignedRawBolt11Invoice};
3737

3838
pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo;
39-
pub use lightning_liquidity::lsps1::msgs::{
40-
Bolt11PaymentInfo, OrderId, OrderParameters, PaymentState,
41-
};
39+
pub use lightning_liquidity::lsps1::msgs::{OrderId, OrderParameters, PaymentState};
4240

4341
pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid};
4442

@@ -60,10 +58,12 @@ use bitcoin::hashes::Hash;
6058
use bitcoin::secp256k1::PublicKey;
6159
use lightning::ln::channelmanager::PaymentId;
6260
use lightning::util::ser::Writeable;
63-
use lightning_invoice::SignedRawBolt11Invoice;
61+
use lightning_invoice::Bolt11Invoice as LdkBolt11Invoice;
6462

6563
use std::convert::TryInto;
6664
use std::str::FromStr;
65+
use std::sync::Arc;
66+
use std::time::Duration;
6767

6868
impl UniffiCustomTypeConverter for PublicKey {
6969
type Builtin = String;
@@ -113,24 +113,6 @@ impl UniffiCustomTypeConverter for Address {
113113
}
114114
}
115115

116-
impl UniffiCustomTypeConverter for Bolt11Invoice {
117-
type Builtin = String;
118-
119-
fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
120-
if let Ok(signed) = val.parse::<SignedRawBolt11Invoice>() {
121-
if let Ok(invoice) = Bolt11Invoice::from_signed(signed) {
122-
return Ok(invoice);
123-
}
124-
}
125-
126-
Err(Error::InvalidInvoice.into())
127-
}
128-
129-
fn from_custom(obj: Self) -> Self::Builtin {
130-
obj.to_string()
131-
}
132-
}
133-
134116
impl UniffiCustomTypeConverter for Offer {
135117
type Builtin = String;
136118

@@ -405,6 +387,110 @@ impl From<lightning_invoice::Bolt11InvoiceDescription> for Bolt11InvoiceDescript
405387
}
406388
}
407389

390+
#[derive(Debug, Clone, PartialEq, Eq)]
391+
pub struct Bolt11Invoice {
392+
pub inner: LdkBolt11Invoice,
393+
}
394+
395+
impl Bolt11Invoice {
396+
pub fn from_str(invoice_str: &str) -> Result<Self, Error> {
397+
invoice_str.parse()
398+
}
399+
400+
pub fn signable_hash(&self) -> Vec<u8> {
401+
self.inner.signable_hash().to_vec()
402+
}
403+
404+
pub fn expiry_time_seconds(&self) -> u64 {
405+
self.inner.expiry_time().as_secs()
406+
}
407+
408+
pub fn min_final_cltv_expiry_delta(&self) -> u64 {
409+
self.inner.min_final_cltv_expiry_delta()
410+
}
411+
412+
pub fn amount_milli_satoshis(&self) -> Option<u64> {
413+
self.inner.amount_milli_satoshis()
414+
}
415+
416+
pub fn is_expired(&self) -> bool {
417+
self.inner.is_expired()
418+
}
419+
420+
pub fn seconds_since_epoch(&self) -> u64 {
421+
self.inner.duration_since_epoch().as_secs()
422+
}
423+
424+
pub fn would_expire(&self, at_time_seconds: u64) -> bool {
425+
self.inner.would_expire(Duration::from_secs(at_time_seconds))
426+
}
427+
428+
pub fn seconds_until_expiry(&self) -> u64 {
429+
self.inner.duration_until_expiry().as_secs()
430+
}
431+
432+
pub fn payment_hash(&self) -> PaymentHash {
433+
PaymentHash(self.inner.payment_hash().to_byte_array())
434+
}
435+
436+
pub fn into_inner(self) -> lightning_invoice::Bolt11Invoice {
437+
self.inner
438+
}
439+
}
440+
441+
impl std::str::FromStr for Bolt11Invoice {
442+
type Err = Error;
443+
444+
fn from_str(invoice_str: &str) -> Result<Self, Self::Err> {
445+
match invoice_str.parse::<SignedRawBolt11Invoice>() {
446+
Ok(signed) => match LdkBolt11Invoice::from_signed(signed) {
447+
Ok(invoice) => Ok(Bolt11Invoice { inner: invoice }),
448+
Err(_) => Err(Error::InvalidInvoice),
449+
},
450+
Err(_) => Err(Error::InvalidInvoice),
451+
}
452+
}
453+
}
454+
455+
impl std::fmt::Display for Bolt11Invoice {
456+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457+
write!(f, "{}", self.inner)
458+
}
459+
}
460+
461+
impl From<LdkBolt11Invoice> for Bolt11Invoice {
462+
fn from(invoice: LdkBolt11Invoice) -> Self {
463+
Bolt11Invoice { inner: invoice }
464+
}
465+
}
466+
467+
impl From<Bolt11Invoice> for LdkBolt11Invoice {
468+
fn from(wrapper: Bolt11Invoice) -> Self {
469+
wrapper.into_inner()
470+
}
471+
}
472+
473+
#[derive(Clone, Debug, PartialEq, Eq)]
474+
pub struct Bolt11PaymentInfo {
475+
pub state: PaymentState,
476+
pub expires_at: chrono::DateTime<chrono::Utc>,
477+
pub fee_total_sat: u64,
478+
pub order_total_sat: u64,
479+
pub invoice: Arc<Bolt11Invoice>,
480+
}
481+
482+
impl From<lightning_liquidity::lsps1::msgs::Bolt11PaymentInfo> for Bolt11PaymentInfo {
483+
fn from(info: lightning_liquidity::lsps1::msgs::Bolt11PaymentInfo) -> Self {
484+
Self {
485+
state: info.state,
486+
expires_at: info.expires_at,
487+
fee_total_sat: info.fee_total_sat,
488+
order_total_sat: info.order_total_sat,
489+
invoice: Arc::new(info.invoice.into()),
490+
}
491+
}
492+
}
493+
408494
impl UniffiCustomTypeConverter for OrderId {
409495
type Builtin = String;
410496

@@ -441,4 +527,67 @@ mod tests {
441527
let reconverted_description: Bolt11InvoiceDescription = converted_description.into();
442528
assert_eq!(description, reconverted_description);
443529
}
530+
531+
#[test]
532+
fn test_bolt11_invoice() {
533+
let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa";
534+
let ldk_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap();
535+
536+
let wrapped_invoice = Bolt11Invoice::from(ldk_invoice.clone());
537+
538+
assert_eq!(
539+
ldk_invoice.payment_hash().to_byte_array().to_vec(),
540+
wrapped_invoice.payment_hash().0.to_vec()
541+
);
542+
assert_eq!(ldk_invoice.amount_milli_satoshis(), wrapped_invoice.amount_milli_satoshis());
543+
assert_eq!(ldk_invoice.expiry_time().as_secs(), wrapped_invoice.expiry_time_seconds());
544+
assert_eq!(
545+
ldk_invoice.min_final_cltv_expiry_delta(),
546+
wrapped_invoice.min_final_cltv_expiry_delta()
547+
);
548+
assert_eq!(
549+
ldk_invoice.duration_since_epoch().as_secs(),
550+
wrapped_invoice.seconds_since_epoch()
551+
);
552+
assert_eq!(
553+
ldk_invoice.duration_until_expiry().as_secs(),
554+
wrapped_invoice.seconds_until_expiry()
555+
);
556+
557+
let future_time = Duration::from_secs(wrapped_invoice.duration_since_epoch() + 10000);
558+
assert!(!ldk_invoice.would_expire(future_time));
559+
assert!(!wrapped_invoice.would_expire(future_time.as_secs()));
560+
561+
let invoice_str = wrapped_invoice.to_string();
562+
let parsed_invoice: Bolt11Invoice = invoice_str.parse().unwrap();
563+
assert_eq!(
564+
wrapped_invoice.inner.payment_hash().to_byte_array().to_vec(),
565+
parsed_invoice.inner.payment_hash().to_byte_array().to_vec()
566+
);
567+
}
568+
569+
#[test]
570+
fn test_bolt11_invoice_roundtrip() {
571+
let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa";
572+
let original_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap();
573+
let wrapped_invoice = Bolt11Invoice { inner: original_invoice.clone() };
574+
let unwrapped_invoice: LdkBolt11Invoice = wrapped_invoice.clone().into();
575+
576+
assert_eq!(original_invoice.payment_hash(), unwrapped_invoice.payment_hash());
577+
assert_eq!(original_invoice.payment_secret(), unwrapped_invoice.payment_secret());
578+
assert_eq!(
579+
original_invoice.amount_milli_satoshis(),
580+
unwrapped_invoice.amount_milli_satoshis()
581+
);
582+
assert_eq!(original_invoice.timestamp(), unwrapped_invoice.timestamp());
583+
assert_eq!(original_invoice.expiry_time(), unwrapped_invoice.expiry_time());
584+
assert_eq!(
585+
original_invoice.min_final_cltv_expiry_delta(),
586+
unwrapped_invoice.min_final_cltv_expiry_delta()
587+
);
588+
589+
let original_hints = original_invoice.route_hints();
590+
let unwrapped_hints = unwrapped_invoice.route_hints();
591+
assert_eq!(original_hints.len(), unwrapped_hints.len());
592+
}
444593
}

0 commit comments

Comments
 (0)