Skip to content

Commit 51753c4

Browse files
Add Refund wrapper for FFI bindings
Implement Refund struct in ffi/types.rs to provide a wrapper around LDK's Refund for cross-language bindings. Modified payment handling in bolt12.rs to: - Support both native and FFI-compatible types via type aliasing - Implement conditional compilation for transparent FFI support - Update payment functions to handle wrapped types Added testing to verify that properties are preserved when wrapping/unwrapping between native and FFI types.
1 parent b596731 commit 51753c4

File tree

4 files changed

+255
-17
lines changed

4 files changed

+255
-17
lines changed

bindings/ldk_node.udl

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,21 @@ interface Offer {
759759
PublicKey? issuer_signing_pubkey();
760760
};
761761

762+
interface Refund {
763+
[Throws=NodeError, Name=from_str]
764+
constructor([ByRef] string refund_str);
765+
string description();
766+
u64? absolute_expiry_seconds();
767+
boolean is_expired();
768+
string? issuer();
769+
sequence<u8> payer_metadata();
770+
Network? chain();
771+
u64 amount_msats();
772+
u64? quantity();
773+
PublicKey payer_signing_pubkey();
774+
string? payer_note();
775+
};
776+
762777
[Custom]
763778
typedef string Txid;
764779

@@ -777,9 +792,6 @@ typedef string NodeId;
777792
[Custom]
778793
typedef string Address;
779794

780-
[Custom]
781-
typedef string Refund;
782-
783795
[Custom]
784796
typedef string Bolt12Invoice;
785797

src/ffi/conversions.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
mod uniffi_impl {
99
use std::sync::Arc;
1010

11-
use lightning::offers::offer::Offer as LdkOffer;
11+
use lightning::offers::{offer::Offer as LdkOffer, refund::Refund as LdkRefund};
1212
use lightning_invoice::{
1313
Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription,
1414
};
1515

1616
use crate::error::Error;
17-
use crate::ffi::{Bolt11Invoice, Bolt11InvoiceDescription, Offer};
17+
use crate::ffi::{Bolt11Invoice, Bolt11InvoiceDescription, Offer, Refund};
1818

1919
pub trait UniffiType {
2020
type LdkType;
@@ -72,6 +72,20 @@ mod uniffi_impl {
7272
}
7373
}
7474

75+
impl UniffiType for Arc<Refund> {
76+
type LdkType = LdkRefund;
77+
78+
fn from_ldk(ldk_value: Self::LdkType) -> Self {
79+
Arc::new(Refund { inner: ldk_value })
80+
}
81+
}
82+
83+
impl UniffiConversionType for Arc<Refund> {
84+
fn as_ldk(&self) -> Self::LdkType {
85+
self.inner.clone()
86+
}
87+
}
88+
7589
pub fn maybe_convert<T: UniffiConversionType>(value: &T) -> T::LdkType {
7690
value.as_ldk()
7791
}

src/ffi/types.rs

Lines changed: 212 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ pub use lightning::events::{ClosureReason, PaymentFailureReason};
2727
pub use lightning::ln::types::ChannelId;
2828
pub use lightning::offers::invoice::Bolt12Invoice;
2929
pub use lightning::offers::offer::OfferId;
30-
pub use lightning::offers::refund::Refund;
3130
pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees};
3231
pub use lightning::util::string::UntrustedString;
3332

@@ -58,6 +57,7 @@ use bitcoin::hashes::Hash;
5857
use bitcoin::secp256k1::PublicKey;
5958
use lightning::ln::channelmanager::PaymentId;
6059
use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer};
60+
use lightning::offers::refund::Refund as LdkRefund;
6161
use lightning::util::ser::Writeable;
6262
use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef};
6363

@@ -272,15 +272,117 @@ impl AsRef<LdkOffer> for Offer {
272272
}
273273
}
274274

275-
impl UniffiCustomTypeConverter for Refund {
276-
type Builtin = String;
275+
/// A `Refund` is a request to send an [`Bolt12Invoice`] without a preceding [`Offer`].
276+
///
277+
/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
278+
/// recoup their funds. A refund may be used more generally as an "offer for money", such as with a
279+
/// bitcoin ATM.
280+
///
281+
/// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice
282+
/// [`Offer`]: lightning::offers::offer::Offer
283+
#[derive(Debug, Clone, PartialEq, Eq)]
284+
pub struct Refund {
285+
pub inner: LdkRefund,
286+
}
277287

278-
fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
279-
Refund::from_str(&val).map_err(|_| Error::InvalidRefund.into())
288+
impl Refund {
289+
pub fn from_str(refund_str: &str) -> Result<Self, Error> {
290+
refund_str.parse()
280291
}
281292

282-
fn from_custom(obj: Self) -> Self::Builtin {
283-
obj.to_string()
293+
/// A complete description of the purpose of the refund.
294+
///
295+
/// Intended to be displayed to the user but with the caveat that it has not been verified in any way.
296+
pub fn description(&self) -> String {
297+
self.inner.description().to_string()
298+
}
299+
300+
/// Seconds since the Unix epoch when an invoice should no longer be sent.
301+
///
302+
/// If `None`, the refund does not expire.
303+
pub fn absolute_expiry_seconds(&self) -> Option<u64> {
304+
self.inner.absolute_expiry().map(|duration| duration.as_secs())
305+
}
306+
307+
/// Whether the refund has expired.
308+
pub fn is_expired(&self) -> bool {
309+
self.inner.is_expired()
310+
}
311+
312+
/// The issuer of the refund, possibly beginning with `user@domain` or `domain`.
313+
///
314+
/// Intended to be displayed to the user but with the caveat that it has not been verified in any way.
315+
pub fn issuer(&self) -> Option<String> {
316+
self.inner.issuer().map(|printable| printable.to_string())
317+
}
318+
319+
/// An unpredictable series of bytes, typically containing information about the derivation of
320+
/// [`payer_signing_pubkey`].
321+
///
322+
/// [`payer_signing_pubkey`]: Self::payer_signing_pubkey
323+
pub fn payer_metadata(&self) -> Vec<u8> {
324+
self.inner.payer_metadata().to_vec()
325+
}
326+
327+
/// A chain that the refund is valid for.
328+
pub fn chain(&self) -> Option<Network> {
329+
Network::try_from(self.inner.chain()).ok()
330+
}
331+
332+
/// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]).
333+
///
334+
/// [`chain`]: Self::chain
335+
pub fn amount_msats(&self) -> u64 {
336+
self.inner.amount_msats()
337+
}
338+
339+
/// The quantity of an item that refund is for.
340+
pub fn quantity(&self) -> Option<u64> {
341+
self.inner.quantity()
342+
}
343+
344+
/// A public node id to send to in the case where there are no [`paths`].
345+
///
346+
/// Otherwise, a possibly transient pubkey.
347+
///
348+
/// [`paths`]: lightning::offers::refund::Refund::paths
349+
pub fn payer_signing_pubkey(&self) -> PublicKey {
350+
self.inner.payer_signing_pubkey()
351+
}
352+
353+
/// Payer provided note to include in the invoice.
354+
pub fn payer_note(&self) -> Option<String> {
355+
self.inner.payer_note().map(|printable| printable.to_string())
356+
}
357+
}
358+
359+
impl std::str::FromStr for Refund {
360+
type Err = Error;
361+
362+
fn from_str(refund_str: &str) -> Result<Self, Self::Err> {
363+
refund_str
364+
.parse::<LdkRefund>()
365+
.map(|refund| Refund { inner: refund })
366+
.map_err(|_| Error::InvalidRefund)
367+
}
368+
}
369+
370+
impl From<LdkRefund> for Refund {
371+
fn from(refund: LdkRefund) -> Self {
372+
Refund { inner: refund }
373+
}
374+
}
375+
376+
impl Deref for Refund {
377+
type Target = LdkRefund;
378+
fn deref(&self) -> &Self::Target {
379+
&self.inner
380+
}
381+
}
382+
383+
impl AsRef<LdkRefund> for Refund {
384+
fn as_ref(&self) -> &LdkRefund {
385+
self.deref()
284386
}
285387
}
286388

@@ -830,9 +932,11 @@ mod tests {
830932
time::{SystemTime, UNIX_EPOCH},
831933
};
832934

833-
use lightning::offers::offer::{OfferBuilder, Quantity};
834-
835935
use super::*;
936+
use lightning::offers::{
937+
offer::{OfferBuilder, Quantity},
938+
refund::RefundBuilder,
939+
};
836940

837941
fn create_test_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) {
838942
let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa";
@@ -871,6 +975,28 @@ mod tests {
871975
(ldk_offer, wrapped_offer)
872976
}
873977

978+
fn create_test_refund() -> (LdkRefund, Refund) {
979+
let payer_key = bitcoin::secp256k1::PublicKey::from_str(
980+
"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619",
981+
)
982+
.unwrap();
983+
984+
let expiry =
985+
(SystemTime::now() + Duration::from_secs(3600)).duration_since(UNIX_EPOCH).unwrap();
986+
987+
let builder = RefundBuilder::new("Test refund".to_string().into(), payer_key, 100_000)
988+
.unwrap()
989+
.description("Test refund description".to_string())
990+
.absolute_expiry(expiry)
991+
.quantity(3)
992+
.issuer("test_issuer".to_string());
993+
994+
let ldk_refund = builder.build().unwrap();
995+
let wrapped_refund = Refund::from(ldk_refund.clone());
996+
997+
(ldk_refund, wrapped_refund)
998+
}
999+
8741000
#[test]
8751001
fn test_invoice_description_conversion() {
8761002
let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string();
@@ -1087,4 +1213,81 @@ mod tests {
10871213
},
10881214
}
10891215
}
1216+
1217+
#[test]
1218+
fn test_refund_roundtrip() {
1219+
let (ldk_refund, _) = create_test_refund();
1220+
1221+
let refund_str = ldk_refund.to_string();
1222+
1223+
let parsed_refund = Refund::from_str(&refund_str);
1224+
assert!(parsed_refund.is_ok(), "Failed to parse refund from string!");
1225+
1226+
let invalid_result = Refund::from_str("invalid_refund_string");
1227+
assert!(invalid_result.is_err());
1228+
assert!(matches!(invalid_result.err().unwrap(), Error::InvalidRefund));
1229+
}
1230+
1231+
#[test]
1232+
fn test_refund_properties() {
1233+
let (ldk_refund, wrapped_refund) = create_test_refund();
1234+
1235+
assert_eq!(ldk_refund.description().to_string(), wrapped_refund.description());
1236+
assert_eq!(ldk_refund.amount_msats(), wrapped_refund.amount_msats());
1237+
assert_eq!(ldk_refund.is_expired(), wrapped_refund.is_expired());
1238+
1239+
match (ldk_refund.absolute_expiry(), wrapped_refund.absolute_expiry_seconds()) {
1240+
(Some(ldk_expiry), Some(wrapped_expiry)) => {
1241+
assert_eq!(ldk_expiry.as_secs(), wrapped_expiry);
1242+
},
1243+
(None, None) => {
1244+
// Both fields are missing which is expected behaviour when converting
1245+
},
1246+
(Some(_), None) => {
1247+
panic!("LDK refund had an expiry but wrapped refund did not!");
1248+
},
1249+
(None, Some(_)) => {
1250+
panic!("Wrapped refund had an expiry but LDK refund did not!");
1251+
},
1252+
}
1253+
1254+
match (ldk_refund.quantity(), wrapped_refund.quantity()) {
1255+
(Some(ldk_expiry), Some(wrapped_expiry)) => {
1256+
assert_eq!(ldk_expiry, wrapped_expiry);
1257+
},
1258+
(None, None) => {
1259+
// Both fields are missing which is expected behaviour when converting
1260+
},
1261+
(Some(_), None) => {
1262+
panic!("LDK refund had an quantity but wrapped refund did not!");
1263+
},
1264+
(None, Some(_)) => {
1265+
panic!("Wrapped refund had an quantity but LDK refund did not!");
1266+
},
1267+
}
1268+
1269+
match (ldk_refund.issuer(), wrapped_refund.issuer()) {
1270+
(Some(ldk_issuer), Some(wrapped_issuer)) => {
1271+
assert_eq!(ldk_issuer.to_string(), wrapped_issuer);
1272+
},
1273+
(None, None) => {
1274+
// Both fields are missing which is expected behaviour when converting
1275+
},
1276+
(Some(_), None) => {
1277+
panic!("LDK refund had an issuer but wrapped refund did not!");
1278+
},
1279+
(None, Some(_)) => {
1280+
panic!("Wrapped refund had an issuer but LDK refund did not!");
1281+
},
1282+
}
1283+
1284+
assert_eq!(ldk_refund.payer_metadata().to_vec(), wrapped_refund.payer_metadata());
1285+
assert_eq!(ldk_refund.payer_signing_pubkey(), wrapped_refund.payer_signing_pubkey());
1286+
1287+
if let Ok(network) = Network::try_from(ldk_refund.chain()) {
1288+
assert_eq!(wrapped_refund.chain(), Some(network));
1289+
}
1290+
1291+
assert_eq!(ldk_refund.payer_note().map(|p| p.to_string()), wrapped_refund.payer_note());
1292+
}
10901293
}

src/payment/bolt12.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use lightning::ln::channelmanager::{PaymentId, Retry};
2020
use lightning::offers::invoice::Bolt12Invoice;
2121
use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity};
2222
use lightning::offers::parse::Bolt12SemanticError;
23-
use lightning::offers::refund::Refund;
2423
use lightning::util::string::UntrustedString;
2524

2625
use rand::RngCore;
@@ -34,6 +33,11 @@ type Offer = LdkOffer;
3433
#[cfg(feature = "uniffi")]
3534
type Offer = Arc<crate::ffi::Offer>;
3635

36+
#[cfg(not(feature = "uniffi"))]
37+
type Refund = lightning::offers::refund::Refund;
38+
#[cfg(feature = "uniffi")]
39+
type Refund = Arc<crate::ffi::Refund>;
40+
3741
/// A payment handler allowing to create and pay [BOLT 12] offers and refunds.
3842
///
3943
/// Should be retrieved by calling [`Node::bolt12_payment`].
@@ -334,8 +338,11 @@ impl Bolt12Payment {
334338
///
335339
/// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to
336340
/// retrieve the refund).
341+
///
342+
/// [`Refund`]: lightning::offers::refund::Refund
337343
pub fn request_refund_payment(&self, refund: &Refund) -> Result<Bolt12Invoice, Error> {
338-
let invoice = self.channel_manager.request_refund_payment(refund).map_err(|e| {
344+
let refund = maybe_convert(refund);
345+
let invoice = self.channel_manager.request_refund_payment(&refund).map_err(|e| {
339346
log_error!(self.logger, "Failed to request refund payment: {:?}", e);
340347
Error::InvoiceRequestCreationFailed
341348
})?;
@@ -366,6 +373,8 @@ impl Bolt12Payment {
366373
}
367374

368375
/// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given.
376+
///
377+
/// [`Refund`]: lightning::offers::refund::Refund
369378
pub fn initiate_refund(
370379
&self, amount_msat: u64, expiry_secs: u32, quantity: Option<u64>,
371380
payer_note: Option<String>,
@@ -427,6 +436,6 @@ impl Bolt12Payment {
427436

428437
self.payment_store.insert(payment)?;
429438

430-
Ok(refund)
439+
Ok(maybe_wrap(refund))
431440
}
432441
}

0 commit comments

Comments
 (0)