Skip to content

Commit 6d01797

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, 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 762bc1d commit 6d01797

File tree

2 files changed

+179
-26
lines changed

2 files changed

+179
-26
lines changed

bindings/ldk_node.udl

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,20 @@ dictionary NodeAnnouncementInfo {
693693
sequence<SocketAddress> addresses;
694694
};
695695

696+
interface Bolt11Invoice {
697+
[Throws=NodeError, Name=from_str]
698+
constructor([ByRef] string invoice_str);
699+
sequence<u8> signable_hash();
700+
u64 expiry_time();
701+
u64 min_final_cltv_expiry_delta();
702+
u64? amount_milli_satoshis();
703+
boolean is_expired();
704+
u64 duration_since_epoch();
705+
boolean would_expire(u64 at_time);
706+
u64 duration_until_expiry();
707+
PaymentHash payment_hash();
708+
};
709+
696710
[Custom]
697711
typedef string Txid;
698712

@@ -711,9 +725,6 @@ typedef string NodeId;
711725
[Custom]
712726
typedef string Address;
713727

714-
[Custom]
715-
typedef string Bolt11Invoice;
716-
717728
[Custom]
718729
typedef string Offer;
719730

src/uniffi_types.rs

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

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

0 commit comments

Comments
 (0)