@@ -27,7 +27,6 @@ pub use lightning::events::{ClosureReason, PaymentFailureReason};
27
27
pub use lightning:: ln:: types:: ChannelId ;
28
28
pub use lightning:: offers:: invoice:: Bolt12Invoice ;
29
29
pub use lightning:: offers:: offer:: OfferId ;
30
- pub use lightning:: offers:: refund:: Refund ;
31
30
pub use lightning:: routing:: gossip:: { NodeAlias , NodeId , RoutingFees } ;
32
31
pub use lightning:: util:: string:: UntrustedString ;
33
32
@@ -58,6 +57,7 @@ use bitcoin::hashes::Hash;
58
57
use bitcoin:: secp256k1:: PublicKey ;
59
58
use lightning:: ln:: channelmanager:: PaymentId ;
60
59
use lightning:: offers:: offer:: { Amount as LdkAmount , Offer as LdkOffer } ;
60
+ use lightning:: offers:: refund:: Refund as LdkRefund ;
61
61
use lightning:: util:: ser:: Writeable ;
62
62
use lightning_invoice:: { Bolt11Invoice as LdkBolt11Invoice , Bolt11InvoiceDescriptionRef } ;
63
63
@@ -278,15 +278,123 @@ impl std::fmt::Display for Offer {
278
278
}
279
279
}
280
280
281
- impl UniffiCustomTypeConverter for Refund {
282
- type Builtin = String ;
281
+ /// A `Refund` is a request to send an [`Bolt12Invoice`] without a preceding [`Offer`].
282
+ ///
283
+ /// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
284
+ /// recoup their funds. A refund may be used more generally as an "offer for money", such as with a
285
+ /// bitcoin ATM.
286
+ ///
287
+ /// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice
288
+ /// [`Offer`]: lightning::offers::offer::Offer
289
+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
290
+ pub struct Refund {
291
+ pub ( crate ) inner : LdkRefund ,
292
+ }
283
293
284
- fn into_custom ( val : Self :: Builtin ) -> uniffi:: Result < Self > {
285
- Refund :: from_str ( & val) . map_err ( |_| Error :: InvalidRefund . into ( ) )
294
+ impl Refund {
295
+ pub fn from_str ( refund_str : & str ) -> Result < Self , Error > {
296
+ refund_str. parse ( )
286
297
}
287
298
288
- fn from_custom ( obj : Self ) -> Self :: Builtin {
289
- obj. to_string ( )
299
+ /// A complete description of the purpose of the refund.
300
+ ///
301
+ /// Intended to be displayed to the user but with the caveat that it has not been verified in any way.
302
+ pub fn description ( & self ) -> String {
303
+ self . inner . description ( ) . to_string ( )
304
+ }
305
+
306
+ /// Seconds since the Unix epoch when an invoice should no longer be sent.
307
+ ///
308
+ /// If `None`, the refund does not expire.
309
+ pub fn absolute_expiry_seconds ( & self ) -> Option < u64 > {
310
+ self . inner . absolute_expiry ( ) . map ( |duration| duration. as_secs ( ) )
311
+ }
312
+
313
+ /// Whether the refund has expired.
314
+ pub fn is_expired ( & self ) -> bool {
315
+ self . inner . is_expired ( )
316
+ }
317
+
318
+ /// The issuer of the refund, possibly beginning with `user@domain` or `domain`.
319
+ ///
320
+ /// Intended to be displayed to the user but with the caveat that it has not been verified in any way.
321
+ pub fn issuer ( & self ) -> Option < String > {
322
+ self . inner . issuer ( ) . map ( |printable| printable. to_string ( ) )
323
+ }
324
+
325
+ /// An unpredictable series of bytes, typically containing information about the derivation of
326
+ /// [`payer_signing_pubkey`].
327
+ ///
328
+ /// [`payer_signing_pubkey`]: Self::payer_signing_pubkey
329
+ pub fn payer_metadata ( & self ) -> Vec < u8 > {
330
+ self . inner . payer_metadata ( ) . to_vec ( )
331
+ }
332
+
333
+ /// A chain that the refund is valid for.
334
+ pub fn chain ( & self ) -> Option < Network > {
335
+ Network :: try_from ( self . inner . chain ( ) ) . ok ( )
336
+ }
337
+
338
+ /// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]).
339
+ ///
340
+ /// [`chain`]: Self::chain
341
+ pub fn amount_msats ( & self ) -> u64 {
342
+ self . inner . amount_msats ( )
343
+ }
344
+
345
+ /// The quantity of an item that refund is for.
346
+ pub fn quantity ( & self ) -> Option < u64 > {
347
+ self . inner . quantity ( )
348
+ }
349
+
350
+ /// A public node id to send to in the case where there are no [`paths`].
351
+ ///
352
+ /// Otherwise, a possibly transient pubkey.
353
+ ///
354
+ /// [`paths`]: lightning::offers::refund::Refund::paths
355
+ pub fn payer_signing_pubkey ( & self ) -> PublicKey {
356
+ self . inner . payer_signing_pubkey ( )
357
+ }
358
+
359
+ /// Payer provided note to include in the invoice.
360
+ pub fn payer_note ( & self ) -> Option < String > {
361
+ self . inner . payer_note ( ) . map ( |printable| printable. to_string ( ) )
362
+ }
363
+ }
364
+
365
+ impl std:: str:: FromStr for Refund {
366
+ type Err = Error ;
367
+
368
+ fn from_str ( refund_str : & str ) -> Result < Self , Self :: Err > {
369
+ refund_str
370
+ . parse :: < LdkRefund > ( )
371
+ . map ( |refund| Refund { inner : refund } )
372
+ . map_err ( |_| Error :: InvalidRefund )
373
+ }
374
+ }
375
+
376
+ impl From < LdkRefund > for Refund {
377
+ fn from ( refund : LdkRefund ) -> Self {
378
+ Refund { inner : refund }
379
+ }
380
+ }
381
+
382
+ impl Deref for Refund {
383
+ type Target = LdkRefund ;
384
+ fn deref ( & self ) -> & Self :: Target {
385
+ & self . inner
386
+ }
387
+ }
388
+
389
+ impl AsRef < LdkRefund > for Refund {
390
+ fn as_ref ( & self ) -> & LdkRefund {
391
+ self . deref ( )
392
+ }
393
+ }
394
+
395
+ impl std:: fmt:: Display for Refund {
396
+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
397
+ write ! ( f, "{}" , self . inner)
290
398
}
291
399
}
292
400
@@ -818,9 +926,11 @@ mod tests {
818
926
time:: { SystemTime , UNIX_EPOCH } ,
819
927
} ;
820
928
821
- use lightning:: offers:: offer:: { OfferBuilder , Quantity } ;
822
-
823
929
use super :: * ;
930
+ use lightning:: offers:: {
931
+ offer:: { OfferBuilder , Quantity } ,
932
+ refund:: RefundBuilder ,
933
+ } ;
824
934
825
935
fn create_test_invoice ( ) -> ( LdkBolt11Invoice , Bolt11Invoice ) {
826
936
let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa" ;
@@ -859,6 +969,28 @@ mod tests {
859
969
( ldk_offer, wrapped_offer)
860
970
}
861
971
972
+ fn create_test_refund ( ) -> ( LdkRefund , Refund ) {
973
+ let payer_key = bitcoin:: secp256k1:: PublicKey :: from_str (
974
+ "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" ,
975
+ )
976
+ . unwrap ( ) ;
977
+
978
+ let expiry =
979
+ ( SystemTime :: now ( ) + Duration :: from_secs ( 3600 ) ) . duration_since ( UNIX_EPOCH ) . unwrap ( ) ;
980
+
981
+ let builder = RefundBuilder :: new ( "Test refund" . to_string ( ) . into ( ) , payer_key, 100_000 )
982
+ . unwrap ( )
983
+ . description ( "Test refund description" . to_string ( ) )
984
+ . absolute_expiry ( expiry)
985
+ . quantity ( 3 )
986
+ . issuer ( "test_issuer" . to_string ( ) ) ;
987
+
988
+ let ldk_refund = builder. build ( ) . unwrap ( ) ;
989
+ let wrapped_refund = Refund :: from ( ldk_refund. clone ( ) ) ;
990
+
991
+ ( ldk_refund, wrapped_refund)
992
+ }
993
+
862
994
#[ test]
863
995
fn test_invoice_description_conversion ( ) {
864
996
let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0" . to_string ( ) ;
@@ -1075,4 +1207,81 @@ mod tests {
1075
1207
} ,
1076
1208
}
1077
1209
}
1210
+
1211
+ #[ test]
1212
+ fn test_refund_roundtrip ( ) {
1213
+ let ( ldk_refund, _) = create_test_refund ( ) ;
1214
+
1215
+ let refund_str = ldk_refund. to_string ( ) ;
1216
+
1217
+ let parsed_refund = Refund :: from_str ( & refund_str) ;
1218
+ assert ! ( parsed_refund. is_ok( ) , "Failed to parse refund from string!" ) ;
1219
+
1220
+ let invalid_result = Refund :: from_str ( "invalid_refund_string" ) ;
1221
+ assert ! ( invalid_result. is_err( ) ) ;
1222
+ assert ! ( matches!( invalid_result. err( ) . unwrap( ) , Error :: InvalidRefund ) ) ;
1223
+ }
1224
+
1225
+ #[ test]
1226
+ fn test_refund_properties ( ) {
1227
+ let ( ldk_refund, wrapped_refund) = create_test_refund ( ) ;
1228
+
1229
+ assert_eq ! ( ldk_refund. description( ) . to_string( ) , wrapped_refund. description( ) ) ;
1230
+ assert_eq ! ( ldk_refund. amount_msats( ) , wrapped_refund. amount_msats( ) ) ;
1231
+ assert_eq ! ( ldk_refund. is_expired( ) , wrapped_refund. is_expired( ) ) ;
1232
+
1233
+ match ( ldk_refund. absolute_expiry ( ) , wrapped_refund. absolute_expiry_seconds ( ) ) {
1234
+ ( Some ( ldk_expiry) , Some ( wrapped_expiry) ) => {
1235
+ assert_eq ! ( ldk_expiry. as_secs( ) , wrapped_expiry) ;
1236
+ } ,
1237
+ ( None , None ) => {
1238
+ // Both fields are missing which is expected behaviour when converting
1239
+ } ,
1240
+ ( Some ( _) , None ) => {
1241
+ panic ! ( "LDK refund had an expiry but wrapped refund did not!" ) ;
1242
+ } ,
1243
+ ( None , Some ( _) ) => {
1244
+ panic ! ( "Wrapped refund had an expiry but LDK refund did not!" ) ;
1245
+ } ,
1246
+ }
1247
+
1248
+ match ( ldk_refund. quantity ( ) , wrapped_refund. quantity ( ) ) {
1249
+ ( Some ( ldk_expiry) , Some ( wrapped_expiry) ) => {
1250
+ assert_eq ! ( ldk_expiry, wrapped_expiry) ;
1251
+ } ,
1252
+ ( None , None ) => {
1253
+ // Both fields are missing which is expected behaviour when converting
1254
+ } ,
1255
+ ( Some ( _) , None ) => {
1256
+ panic ! ( "LDK refund had an quantity but wrapped refund did not!" ) ;
1257
+ } ,
1258
+ ( None , Some ( _) ) => {
1259
+ panic ! ( "Wrapped refund had an quantity but LDK refund did not!" ) ;
1260
+ } ,
1261
+ }
1262
+
1263
+ match ( ldk_refund. issuer ( ) , wrapped_refund. issuer ( ) ) {
1264
+ ( Some ( ldk_issuer) , Some ( wrapped_issuer) ) => {
1265
+ assert_eq ! ( ldk_issuer. to_string( ) , wrapped_issuer) ;
1266
+ } ,
1267
+ ( None , None ) => {
1268
+ // Both fields are missing which is expected behaviour when converting
1269
+ } ,
1270
+ ( Some ( _) , None ) => {
1271
+ panic ! ( "LDK refund had an issuer but wrapped refund did not!" ) ;
1272
+ } ,
1273
+ ( None , Some ( _) ) => {
1274
+ panic ! ( "Wrapped refund had an issuer but LDK refund did not!" ) ;
1275
+ } ,
1276
+ }
1277
+
1278
+ assert_eq ! ( ldk_refund. payer_metadata( ) . to_vec( ) , wrapped_refund. payer_metadata( ) ) ;
1279
+ assert_eq ! ( ldk_refund. payer_signing_pubkey( ) , wrapped_refund. payer_signing_pubkey( ) ) ;
1280
+
1281
+ if let Ok ( network) = Network :: try_from ( ldk_refund. chain ( ) ) {
1282
+ assert_eq ! ( wrapped_refund. chain( ) , Some ( network) ) ;
1283
+ }
1284
+
1285
+ assert_eq ! ( ldk_refund. payer_note( ) . map( |p| p. to_string( ) ) , wrapped_refund. payer_note( ) ) ;
1286
+ }
1078
1287
}
0 commit comments