Skip to content

Commit cfc8fbd

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 cfc8fbd

File tree

2 files changed

+313
-26
lines changed

2 files changed

+313
-26
lines changed

bindings/ldk_node.udl

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

701+
enum Currency {
702+
"Bitcoin",
703+
"BitcoinTestnet",
704+
"Regtest",
705+
"Simnet",
706+
"Signet",
707+
};
708+
709+
dictionary RouteHintHop {
710+
PublicKey src_node_id;
711+
u64 short_channel_id;
712+
u16 cltv_expiry_delta;
713+
u64? htlc_minimum_msat;
714+
u64? htlc_maximum_msat;
715+
RoutingFees fees;
716+
};
717+
718+
interface Bolt11Invoice {
719+
[Throws=NodeError, Name=from_str]
720+
constructor([ByRef] string invoice_str);
721+
sequence<u8> signable_hash();
722+
u64 expiry_time_seconds();
723+
u64 min_final_cltv_expiry_delta();
724+
u64? amount_milli_satoshis();
725+
boolean is_expired();
726+
u64 seconds_since_epoch();
727+
boolean would_expire(u64 at_time_seconds);
728+
u64 seconds_until_expiry();
729+
PaymentHash payment_hash();
730+
u64 timestamp_seconds();
731+
Currency currency();
732+
PaymentSecret payment_secret();
733+
Bolt11InvoiceDescription description();
734+
sequence<Address> fallback_addresses();
735+
sequence<sequence<RouteHintHop>> route_hints();
736+
sequence<u8>? features();
737+
PublicKey recover_payee_pub_key();
738+
Network network();
739+
};
740+
701741
[Custom]
702742
typedef string Txid;
703743

@@ -716,9 +756,6 @@ typedef string NodeId;
716756
[Custom]
717757
typedef string Address;
718758

719-
[Custom]
720-
typedef string Bolt11Invoice;
721-
722759
[Custom]
723760
typedef string Offer;
724761

src/uniffi_types.rs

Lines changed: 273 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, Bolt11InvoiceDescriptionRef};
6462

6563
use std::convert::TryInto;
6664
use std::str::FromStr;
65+
use std::sync::Arc;
66+
use std::time::{Duration, UNIX_EPOCH};
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,211 @@ impl From<lightning_invoice::Bolt11InvoiceDescription> for Bolt11InvoiceDescript
405387
}
406388
}
407389

390+
impl<'a> From<Bolt11InvoiceDescriptionRef<'a>> for Bolt11InvoiceDescription {
391+
fn from(value: Bolt11InvoiceDescriptionRef<'a>) -> Self {
392+
match value {
393+
lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(description) => {
394+
Bolt11InvoiceDescription::Direct { description: description.to_string() }
395+
},
396+
lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(hash) => {
397+
Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) }
398+
},
399+
}
400+
}
401+
}
402+
403+
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
404+
pub enum Currency {
405+
Bitcoin,
406+
BitcoinTestnet,
407+
Regtest,
408+
Simnet,
409+
Signet,
410+
}
411+
412+
impl From<lightning_invoice::Currency> for Currency {
413+
fn from(currency: lightning_invoice::Currency) -> Self {
414+
match currency {
415+
lightning_invoice::Currency::Bitcoin => Currency::Bitcoin,
416+
lightning_invoice::Currency::BitcoinTestnet => Currency::BitcoinTestnet,
417+
lightning_invoice::Currency::Regtest => Currency::Regtest,
418+
lightning_invoice::Currency::Simnet => Currency::Simnet,
419+
lightning_invoice::Currency::Signet => Currency::Signet,
420+
}
421+
}
422+
}
423+
424+
#[derive(Debug, Clone, PartialEq, Eq)]
425+
pub struct RouteHintHop {
426+
pub src_node_id: PublicKey,
427+
pub short_channel_id: u64,
428+
pub cltv_expiry_delta: u16,
429+
pub htlc_minimum_msat: Option<u64>,
430+
pub htlc_maximum_msat: Option<u64>,
431+
pub fees: RoutingFees,
432+
}
433+
434+
impl From<lightning::routing::router::RouteHintHop> for RouteHintHop {
435+
fn from(hop: lightning::routing::router::RouteHintHop) -> Self {
436+
Self {
437+
src_node_id: hop.src_node_id,
438+
short_channel_id: hop.short_channel_id,
439+
cltv_expiry_delta: hop.cltv_expiry_delta,
440+
htlc_minimum_msat: hop.htlc_minimum_msat,
441+
htlc_maximum_msat: hop.htlc_maximum_msat,
442+
fees: hop.fees,
443+
}
444+
}
445+
}
446+
447+
#[derive(Debug, Clone, PartialEq, Eq)]
448+
pub struct Bolt11Invoice {
449+
pub inner: LdkBolt11Invoice,
450+
}
451+
452+
impl Bolt11Invoice {
453+
pub fn from_str(invoice_str: &str) -> Result<Self, Error> {
454+
invoice_str.parse()
455+
}
456+
457+
pub fn signable_hash(&self) -> Vec<u8> {
458+
self.inner.signable_hash().to_vec()
459+
}
460+
461+
pub fn expiry_time_seconds(&self) -> u64 {
462+
self.inner.expiry_time().as_secs()
463+
}
464+
465+
pub fn min_final_cltv_expiry_delta(&self) -> u64 {
466+
self.inner.min_final_cltv_expiry_delta()
467+
}
468+
469+
pub fn amount_milli_satoshis(&self) -> Option<u64> {
470+
self.inner.amount_milli_satoshis()
471+
}
472+
473+
pub fn is_expired(&self) -> bool {
474+
self.inner.is_expired()
475+
}
476+
477+
pub fn seconds_since_epoch(&self) -> u64 {
478+
self.inner.duration_since_epoch().as_secs()
479+
}
480+
481+
pub fn would_expire(&self, at_time_seconds: u64) -> bool {
482+
self.inner.would_expire(Duration::from_secs(at_time_seconds))
483+
}
484+
485+
pub fn seconds_until_expiry(&self) -> u64 {
486+
self.inner.duration_until_expiry().as_secs()
487+
}
488+
489+
pub fn payment_hash(&self) -> PaymentHash {
490+
PaymentHash(self.inner.payment_hash().to_byte_array())
491+
}
492+
493+
pub fn timestamp_seconds(&self) -> u64 {
494+
self.inner.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs()
495+
}
496+
497+
pub fn currency(&self) -> Currency {
498+
self.inner.currency().into()
499+
}
500+
501+
pub fn payment_secret(&self) -> PaymentSecret {
502+
PaymentSecret(self.inner.payment_secret().0)
503+
}
504+
505+
pub fn description(&self) -> Bolt11InvoiceDescription {
506+
self.inner.description().into()
507+
}
508+
509+
pub fn fallback_addresses(&self) -> Vec<Address> {
510+
self.inner.fallback_addresses()
511+
}
512+
513+
pub fn route_hints(&self) -> Vec<Vec<RouteHintHop>> {
514+
self.inner
515+
.route_hints()
516+
.iter()
517+
.map(|route| {
518+
route.0.iter()
519+
.map(|hop| RouteHintHop::from(hop.clone()))
520+
.collect()
521+
})
522+
.collect()
523+
}
524+
525+
pub fn features(&self) -> Option<Vec<u8>> {
526+
self.inner.features().map(|f| f.encode())
527+
}
528+
529+
pub fn recover_payee_pub_key(&self) -> PublicKey {
530+
self.inner.recover_payee_pub_key()
531+
}
532+
533+
pub fn network(&self) -> Network {
534+
self.inner.network()
535+
}
536+
537+
pub fn into_inner(self) -> lightning_invoice::Bolt11Invoice {
538+
self.inner
539+
}
540+
}
541+
542+
impl std::str::FromStr for Bolt11Invoice {
543+
type Err = Error;
544+
545+
fn from_str(invoice_str: &str) -> Result<Self, Self::Err> {
546+
match invoice_str.parse::<SignedRawBolt11Invoice>() {
547+
Ok(signed) => match LdkBolt11Invoice::from_signed(signed) {
548+
Ok(invoice) => Ok(Bolt11Invoice { inner: invoice }),
549+
Err(_) => Err(Error::InvalidInvoice),
550+
},
551+
Err(_) => Err(Error::InvalidInvoice),
552+
}
553+
}
554+
}
555+
556+
impl std::fmt::Display for Bolt11Invoice {
557+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558+
write!(f, "{}", self.inner)
559+
}
560+
}
561+
562+
impl From<LdkBolt11Invoice> for Bolt11Invoice {
563+
fn from(invoice: LdkBolt11Invoice) -> Self {
564+
Bolt11Invoice { inner: invoice }
565+
}
566+
}
567+
568+
impl From<Bolt11Invoice> for LdkBolt11Invoice {
569+
fn from(wrapper: Bolt11Invoice) -> Self {
570+
wrapper.into_inner()
571+
}
572+
}
573+
574+
#[derive(Clone, Debug, PartialEq, Eq)]
575+
pub struct Bolt11PaymentInfo {
576+
pub state: PaymentState,
577+
pub expires_at: chrono::DateTime<chrono::Utc>,
578+
pub fee_total_sat: u64,
579+
pub order_total_sat: u64,
580+
pub invoice: Arc<Bolt11Invoice>,
581+
}
582+
583+
impl From<lightning_liquidity::lsps1::msgs::Bolt11PaymentInfo> for Bolt11PaymentInfo {
584+
fn from(info: lightning_liquidity::lsps1::msgs::Bolt11PaymentInfo) -> Self {
585+
Self {
586+
state: info.state,
587+
expires_at: info.expires_at,
588+
fee_total_sat: info.fee_total_sat,
589+
order_total_sat: info.order_total_sat,
590+
invoice: Arc::new(info.invoice.into()),
591+
}
592+
}
593+
}
594+
408595
impl UniffiCustomTypeConverter for OrderId {
409596
type Builtin = String;
410597

@@ -441,4 +628,67 @@ mod tests {
441628
let reconverted_description: Bolt11InvoiceDescription = converted_description.into();
442629
assert_eq!(description, reconverted_description);
443630
}
631+
632+
#[test]
633+
fn test_bolt11_invoice() {
634+
let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa";
635+
let ldk_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap();
636+
637+
let wrapped_invoice = Bolt11Invoice::from(ldk_invoice.clone());
638+
639+
assert_eq!(
640+
ldk_invoice.payment_hash().to_byte_array().to_vec(),
641+
wrapped_invoice.payment_hash().0.to_vec()
642+
);
643+
assert_eq!(ldk_invoice.amount_milli_satoshis(), wrapped_invoice.amount_milli_satoshis());
644+
assert_eq!(ldk_invoice.expiry_time().as_secs(), wrapped_invoice.expiry_time_seconds());
645+
assert_eq!(
646+
ldk_invoice.min_final_cltv_expiry_delta(),
647+
wrapped_invoice.min_final_cltv_expiry_delta()
648+
);
649+
assert_eq!(
650+
ldk_invoice.duration_since_epoch().as_secs(),
651+
wrapped_invoice.seconds_since_epoch()
652+
);
653+
assert_eq!(
654+
ldk_invoice.duration_until_expiry().as_secs(),
655+
wrapped_invoice.seconds_until_expiry()
656+
);
657+
658+
let future_time = Duration::from_secs(wrapped_invoice.seconds_since_epoch() + 10000);
659+
assert!(!ldk_invoice.would_expire(future_time));
660+
assert!(!wrapped_invoice.would_expire(future_time.as_secs()));
661+
662+
let invoice_str = wrapped_invoice.to_string();
663+
let parsed_invoice: Bolt11Invoice = invoice_str.parse().unwrap();
664+
assert_eq!(
665+
wrapped_invoice.inner.payment_hash().to_byte_array().to_vec(),
666+
parsed_invoice.inner.payment_hash().to_byte_array().to_vec()
667+
);
668+
}
669+
670+
#[test]
671+
fn test_bolt11_invoice_roundtrip() {
672+
let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa";
673+
let original_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap();
674+
let wrapped_invoice = Bolt11Invoice { inner: original_invoice.clone() };
675+
let unwrapped_invoice: LdkBolt11Invoice = wrapped_invoice.clone().into();
676+
677+
assert_eq!(original_invoice.payment_hash(), unwrapped_invoice.payment_hash());
678+
assert_eq!(original_invoice.payment_secret(), unwrapped_invoice.payment_secret());
679+
assert_eq!(
680+
original_invoice.amount_milli_satoshis(),
681+
unwrapped_invoice.amount_milli_satoshis()
682+
);
683+
assert_eq!(original_invoice.timestamp(), unwrapped_invoice.timestamp());
684+
assert_eq!(original_invoice.expiry_time(), unwrapped_invoice.expiry_time());
685+
assert_eq!(
686+
original_invoice.min_final_cltv_expiry_delta(),
687+
unwrapped_invoice.min_final_cltv_expiry_delta()
688+
);
689+
690+
let original_hints = original_invoice.route_hints();
691+
let unwrapped_hints = unwrapped_invoice.route_hints();
692+
assert_eq!(original_hints.len(), unwrapped_hints.len());
693+
}
444694
}

0 commit comments

Comments
 (0)