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