@@ -27,7 +27,6 @@ pub use lightning::events::{ClosureReason, PaymentFailureReason};
2727pub use lightning:: ln:: types:: ChannelId ;
2828pub use lightning:: offers:: invoice:: Bolt12Invoice ;
2929pub use lightning:: offers:: offer:: OfferId ;
30- pub use lightning:: offers:: refund:: Refund ;
3130pub use lightning:: routing:: gossip:: { NodeAlias , NodeId , RoutingFees } ;
3231pub use lightning:: util:: string:: UntrustedString ;
3332
@@ -58,6 +57,7 @@ use bitcoin::hashes::Hash;
5857use bitcoin:: secp256k1:: PublicKey ;
5958use lightning:: ln:: channelmanager:: PaymentId ;
6059use lightning:: offers:: offer:: { Amount as LdkAmount , Offer as LdkOffer } ;
60+ use lightning:: offers:: refund:: Refund as LdkRefund ;
6161use lightning:: util:: ser:: Writeable ;
6262use lightning_invoice:: { Bolt11Invoice as LdkBolt11Invoice , Bolt11InvoiceDescriptionRef } ;
6363
@@ -201,15 +201,103 @@ impl From<LdkOffer> for Offer {
201201 }
202202}
203203
204- impl UniffiCustomTypeConverter for Refund {
205- type Builtin = String ;
204+ /// A `Refund` is a request to send an [`Bolt12Invoice`] without a preceding [`Offer`].
205+ ///
206+ /// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
207+ /// recoup their funds. A refund may be used more generally as an "offer for money", such as with a
208+ /// bitcoin ATM.
209+ ///
210+ /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
211+ /// [`Offer`]: crate::offers::offer::Offer
212+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
213+ pub struct Refund {
214+ pub inner : LdkRefund ,
215+ }
206216
207- fn into_custom ( val : Self :: Builtin ) -> uniffi:: Result < Self > {
208- Refund :: from_str ( & val) . map_err ( |_| Error :: InvalidRefund . into ( ) )
217+ impl Refund {
218+ pub fn from_str ( refund_str : & str ) -> Result < Self , Error > {
219+ refund_str. parse ( )
209220 }
210221
211- fn from_custom ( obj : Self ) -> Self :: Builtin {
212- obj. to_string ( )
222+ /// A complete description of the purpose of the refund. Intended to be displayed to the user
223+ /// but with the caveat that it has not been verified in any way.
224+ pub fn description ( & self ) -> String {
225+ self . inner . description ( ) . to_string ( )
226+ }
227+
228+ /// Seconds since the Unix epoch when an invoice should no longer be sent.
229+ ///
230+ /// If `None`, the refund does not expire.
231+ pub fn absolute_expiry_seconds ( & self ) -> Option < u64 > {
232+ self . inner . absolute_expiry ( ) . map ( |duration| duration. as_secs ( ) )
233+ }
234+
235+ /// Whether the refund has expired.
236+ pub fn is_expired ( & self ) -> bool {
237+ self . inner . is_expired ( )
238+ }
239+
240+ /// The issuer of the refund, possibly beginning with `user@domain` or `domain`. Intended to be
241+ /// displayed to the user but with the caveat that it has not been verified in any way.
242+ pub fn issuer ( & self ) -> Option < String > {
243+ self . inner . issuer ( ) . map ( |printable| printable. to_string ( ) )
244+ }
245+
246+ /// An unpredictable series of bytes, typically containing information about the derivation of
247+ /// [`payer_signing_pubkey`].
248+ ///
249+ /// [`payer_signing_pubkey`]: Self::payer_signing_pubkey
250+ pub fn payer_metadata ( & self ) -> Vec < u8 > {
251+ self . inner . payer_metadata ( ) . to_vec ( )
252+ }
253+
254+ /// A chain that the refund is valid for.
255+ pub fn chain ( & self ) -> Option < Network > {
256+ Network :: try_from ( self . inner . chain ( ) ) . ok ( )
257+ }
258+
259+ /// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]).
260+ ///
261+ /// [`chain`]: Self::chain
262+ pub fn amount_msats ( & self ) -> u64 {
263+ self . inner . amount_msats ( )
264+ }
265+
266+ // pub fn features(&self)
267+
268+ /// The quantity of an item that refund is for.
269+ pub fn quantity ( & self ) -> Option < u64 > {
270+ self . inner . quantity ( )
271+ }
272+
273+ /// A public node id to send to in the case where there are no [`paths`]. Otherwise, a possibly
274+ /// transient pubkey.
275+ ///
276+ /// [`paths`]: Self::paths
277+ pub fn payer_signing_pubkey ( & self ) -> PublicKey {
278+ self . inner . payer_signing_pubkey ( )
279+ }
280+
281+ /// Payer provided note to include in the invoice.
282+ pub fn payer_note ( & self ) -> Option < String > {
283+ self . inner . payer_note ( ) . map ( |printable| printable. to_string ( ) )
284+ }
285+ }
286+
287+ impl std:: str:: FromStr for Refund {
288+ type Err = Error ;
289+
290+ fn from_str ( refund_str : & str ) -> Result < Self , Self :: Err > {
291+ refund_str
292+ . parse :: < LdkRefund > ( )
293+ . map ( |refund| Refund { inner : refund } )
294+ . map_err ( |_| Error :: InvalidRefund )
295+ }
296+ }
297+
298+ impl From < LdkRefund > for Refund {
299+ fn from ( refund : LdkRefund ) -> Self {
300+ Refund { inner : refund }
213301 }
214302}
215303
@@ -736,9 +824,8 @@ impl UniffiCustomTypeConverter for DateTime {
736824mod tests {
737825 use std:: time:: { SystemTime , UNIX_EPOCH } ;
738826
739- use lightning:: offers:: offer:: OfferBuilder ;
740-
741827 use super :: * ;
828+ use lightning:: offers:: { offer:: OfferBuilder , refund:: RefundBuilder } ;
742829
743830 fn create_test_invoice ( ) -> ( LdkBolt11Invoice , Bolt11Invoice ) {
744831 let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa" ;
@@ -768,6 +855,28 @@ mod tests {
768855 ( ldk_offer, wrapped_offer)
769856 }
770857
858+ fn create_test_refund ( ) -> ( LdkRefund , Refund ) {
859+ let payer_key = bitcoin:: secp256k1:: PublicKey :: from_str (
860+ "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" ,
861+ )
862+ . unwrap ( ) ;
863+
864+ let expiry =
865+ ( SystemTime :: now ( ) + Duration :: from_secs ( 3600 ) ) . duration_since ( UNIX_EPOCH ) . unwrap ( ) ;
866+
867+ let builder = RefundBuilder :: new ( "Test refund" . to_string ( ) . into ( ) , payer_key, 100_000 )
868+ . unwrap ( )
869+ . description ( "Test refund description" . to_string ( ) )
870+ . absolute_expiry ( expiry)
871+ . quantity ( 3 )
872+ . issuer ( "test_issuer" . to_string ( ) ) ;
873+
874+ let ldk_refund = builder. build ( ) . unwrap ( ) ;
875+ let wrapped_refund = Refund :: from ( ldk_refund. clone ( ) ) ;
876+
877+ ( ldk_refund, wrapped_refund)
878+ }
879+
771880 #[ test]
772881 fn test_invoice_description_conversion ( ) {
773882 let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0" . to_string ( ) ;
@@ -930,4 +1039,81 @@ mod tests {
9301039 assert_eq ! ( ldk_offer. is_expired( ) , wrapped_offer. is_expired( ) ) ;
9311040 assert_eq ! ( ldk_offer. id( ) , wrapped_offer. id( ) ) ;
9321041 }
1042+
1043+ #[ test]
1044+ fn test_refund_roundtrip ( ) {
1045+ let ( ldk_refund, _) = create_test_refund ( ) ;
1046+
1047+ let refund_str = ldk_refund. to_string ( ) ;
1048+
1049+ let parsed_refund = Refund :: from_str ( & refund_str) ;
1050+ assert ! ( parsed_refund. is_ok( ) , "Failed to parse refund from string!" ) ;
1051+
1052+ let invalid_result = Refund :: from_str ( "invalid_refund_string" ) ;
1053+ assert ! ( invalid_result. is_err( ) ) ;
1054+ assert ! ( matches!( invalid_result. err( ) . unwrap( ) , Error :: InvalidRefund ) ) ;
1055+ }
1056+
1057+ #[ test]
1058+ fn test_refund_properties ( ) {
1059+ let ( ldk_refund, wrapped_refund) = create_test_refund ( ) ;
1060+
1061+ assert_eq ! ( ldk_refund. description( ) . to_string( ) , wrapped_refund. description( ) ) ;
1062+ assert_eq ! ( ldk_refund. amount_msats( ) , wrapped_refund. amount_msats( ) ) ;
1063+ assert_eq ! ( ldk_refund. is_expired( ) , wrapped_refund. is_expired( ) ) ;
1064+
1065+ match ( ldk_refund. absolute_expiry ( ) , wrapped_refund. absolute_expiry_seconds ( ) ) {
1066+ ( Some ( ldk_expiry) , Some ( wrapped_expiry) ) => {
1067+ assert_eq ! ( ldk_expiry. as_secs( ) , wrapped_expiry) ;
1068+ } ,
1069+ ( None , None ) => {
1070+ // Both fields are missing which is expected behaviour when converting
1071+ } ,
1072+ ( Some ( _) , None ) => {
1073+ panic ! ( "LDK refund had an expiry but wrapped refund did not!" ) ;
1074+ } ,
1075+ ( None , Some ( _) ) => {
1076+ panic ! ( "Wrapped refund had an expiry but LDK refund did not!" ) ;
1077+ } ,
1078+ }
1079+
1080+ match ( ldk_refund. quantity ( ) , wrapped_refund. quantity ( ) ) {
1081+ ( Some ( ldk_expiry) , Some ( wrapped_expiry) ) => {
1082+ assert_eq ! ( ldk_expiry, wrapped_expiry) ;
1083+ } ,
1084+ ( None , None ) => {
1085+ // Both fields are missing which is expected behaviour when converting
1086+ } ,
1087+ ( Some ( _) , None ) => {
1088+ panic ! ( "LDK refund had an quantity but wrapped refund did not!" ) ;
1089+ } ,
1090+ ( None , Some ( _) ) => {
1091+ panic ! ( "Wrapped refund had an quantity but LDK refund did not!" ) ;
1092+ } ,
1093+ }
1094+
1095+ match ( ldk_refund. issuer ( ) , wrapped_refund. issuer ( ) ) {
1096+ ( Some ( ldk_issuer) , Some ( wrapped_issuer) ) => {
1097+ assert_eq ! ( ldk_issuer. to_string( ) , wrapped_issuer) ;
1098+ } ,
1099+ ( None , None ) => {
1100+ // Both fields are missing which is expected behaviour when converting
1101+ } ,
1102+ ( Some ( _) , None ) => {
1103+ panic ! ( "LDK refund had an issuer but wrapped refund did not!" ) ;
1104+ } ,
1105+ ( None , Some ( _) ) => {
1106+ panic ! ( "Wrapped refund had an issuer but LDK refund did not!" ) ;
1107+ } ,
1108+ }
1109+
1110+ assert_eq ! ( ldk_refund. payer_metadata( ) . to_vec( ) , wrapped_refund. payer_metadata( ) ) ;
1111+ assert_eq ! ( ldk_refund. payer_signing_pubkey( ) , wrapped_refund. payer_signing_pubkey( ) ) ;
1112+
1113+ if let Ok ( network) = Network :: try_from ( ldk_refund. chain ( ) ) {
1114+ assert_eq ! ( wrapped_refund. chain( ) , Some ( network) ) ;
1115+ }
1116+
1117+ assert_eq ! ( ldk_refund. payer_note( ) . map( |p| p. to_string( ) ) , wrapped_refund. payer_note( ) ) ;
1118+ }
9331119}
0 commit comments