Skip to content

Commit c198c89

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 62b2523 commit c198c89

File tree

2 files changed

+178
-26
lines changed

2 files changed

+178
-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: 164 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(&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 duration_since_epoch(&self) -> u64 {
421+
self.inner.duration_since_epoch().as_secs()
422+
}
423+
424+
pub fn would_expire(&self, at_time: u64) -> bool {
425+
self.inner.would_expire(Duration::from_secs(at_time))
426+
}
427+
428+
pub fn duration_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,59 @@ 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());
544+
assert_eq!(
545+
ldk_invoice.min_final_cltv_expiry_delta(),
546+
wrapped_invoice.min_final_cltv_expiry_delta()
547+
);
548+
549+
let future_time = Duration::from_secs(wrapped_invoice.duration_since_epoch() + 10000);
550+
assert!(!ldk_invoice.would_expire(future_time));
551+
assert!(!wrapped_invoice.would_expire(future_time.as_secs()));
552+
553+
let invoice_str = wrapped_invoice.to_string();
554+
let parsed_invoice: Bolt11Invoice = invoice_str.parse().unwrap();
555+
assert_eq!(
556+
wrapped_invoice.inner.payment_hash().to_byte_array().to_vec(),
557+
parsed_invoice.inner.payment_hash().to_byte_array().to_vec()
558+
);
559+
}
560+
561+
#[test]
562+
fn test_bolt11_invoice_roundtrip() {
563+
let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa";
564+
let original_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap();
565+
let wrapped_invoice = Bolt11Invoice { inner: original_invoice.clone() };
566+
let unwrapped_invoice: LdkBolt11Invoice = wrapped_invoice.clone().into();
567+
568+
assert_eq!(original_invoice.payment_hash(), unwrapped_invoice.payment_hash());
569+
assert_eq!(original_invoice.payment_secret(), unwrapped_invoice.payment_secret());
570+
assert_eq!(
571+
original_invoice.amount_milli_satoshis(),
572+
unwrapped_invoice.amount_milli_satoshis()
573+
);
574+
assert_eq!(original_invoice.timestamp(), unwrapped_invoice.timestamp());
575+
assert_eq!(original_invoice.expiry_time(), unwrapped_invoice.expiry_time());
576+
assert_eq!(
577+
original_invoice.min_final_cltv_expiry_delta(),
578+
unwrapped_invoice.min_final_cltv_expiry_delta()
579+
);
580+
581+
let original_hints = original_invoice.route_hints();
582+
let unwrapped_hints = unwrapped_invoice.route_hints();
583+
assert_eq!(original_hints.len(), unwrapped_hints.len());
584+
}
444585
}

0 commit comments

Comments
 (0)