@@ -26,7 +26,7 @@ pub use lightning::chain::channelmonitor::BalanceSource;
2626pub use lightning:: events:: { ClosureReason , PaymentFailureReason } ;
2727pub use lightning:: ln:: types:: ChannelId ;
2828pub use lightning:: offers:: invoice:: Bolt12Invoice ;
29- pub use lightning:: offers:: offer:: { Offer , OfferId } ;
29+ pub use lightning:: offers:: offer:: OfferId ;
3030pub use lightning:: offers:: refund:: Refund ;
3131pub use lightning:: routing:: gossip:: { NodeAlias , NodeId , RoutingFees } ;
3232pub use lightning:: util:: string:: UntrustedString ;
@@ -57,6 +57,7 @@ use bitcoin::hashes::sha256::Hash as Sha256;
5757use bitcoin:: hashes:: Hash ;
5858use bitcoin:: secp256k1:: PublicKey ;
5959use lightning:: ln:: channelmanager:: PaymentId ;
60+ use lightning:: offers:: offer:: { Amount as LdkAmount , Offer as LdkOffer } ;
6061use lightning:: util:: ser:: Writeable ;
6162use lightning_invoice:: { Bolt11Invoice as LdkBolt11Invoice , Bolt11InvoiceDescriptionRef } ;
6263
@@ -114,15 +115,166 @@ impl UniffiCustomTypeConverter for Address {
114115 }
115116}
116117
117- impl UniffiCustomTypeConverter for Offer {
118- type Builtin = String ;
118+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
119+ pub enum OfferAmount {
120+ Bitcoin { amount_msats : u64 } ,
121+ Currency { iso4217_code : String , amount : u64 } ,
122+ }
119123
120- fn into_custom ( val : Self :: Builtin ) -> uniffi:: Result < Self > {
121- Offer :: from_str ( & val) . map_err ( |_| Error :: InvalidOffer . into ( ) )
124+ impl From < LdkAmount > for OfferAmount {
125+ fn from ( ldk_amount : LdkAmount ) -> Self {
126+ match ldk_amount {
127+ LdkAmount :: Bitcoin { amount_msats } => OfferAmount :: Bitcoin { amount_msats } ,
128+ LdkAmount :: Currency { iso4217_code, amount } => OfferAmount :: Currency {
129+ iso4217_code : iso4217_code. iter ( ) . map ( |& b| b as char ) . collect ( ) ,
130+ amount,
131+ } ,
132+ }
122133 }
134+ }
123135
124- fn from_custom ( obj : Self ) -> Self :: Builtin {
125- obj. to_string ( )
136+ /// An `Offer` is a potentially long-lived proposal for payment of a good or service.
137+ ///
138+ /// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a
139+ /// customer may request an [`Bolt12Invoice`] for a specific quantity and using an amount sufficient
140+ /// to cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`].
141+ ///
142+ /// Offers may be denominated in currency other than bitcoin but are ultimately paid using the
143+ /// latter.
144+ ///
145+ /// Through the use of [`BlindedMessagePath`]s, offers provide recipient privacy.
146+ ///
147+ /// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest
148+ /// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice
149+ /// [`Offer`]: lightning::offers::Offer:amount
150+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
151+ pub struct Offer {
152+ pub ( crate ) inner : LdkOffer ,
153+ }
154+
155+ impl Offer {
156+ pub fn from_str ( offer_str : & str ) -> Result < Self , Error > {
157+ offer_str. parse ( )
158+ }
159+
160+ /// Returns the id of the offer.
161+ pub fn id ( & self ) -> OfferId {
162+ OfferId ( self . inner . id ( ) . 0 )
163+ }
164+
165+ /// Whether the offer has expired.
166+ pub fn is_expired ( & self ) -> bool {
167+ self . inner . is_expired ( )
168+ }
169+
170+ /// A complete description of the purpose of the payment.
171+ ///
172+ /// Intended to be displayed to the user but with the caveat that it has not been verified in any way.
173+ pub fn description ( & self ) -> Option < String > {
174+ self . inner . description ( ) . map ( |printable| printable. to_string ( ) )
175+ }
176+
177+ /// The issuer of the offer, possibly beginning with `user@domain` or `domain`.
178+ ///
179+ /// Intended to be displayed to the user but with the caveat that it has not been verified in any way.
180+ pub fn issuer ( & self ) -> Option < String > {
181+ self . inner . issuer ( ) . map ( |printable| printable. to_string ( ) )
182+ }
183+
184+ /// The minimum amount required for a successful payment of a single item.
185+ pub fn amount ( & self ) -> Option < OfferAmount > {
186+ self . inner . amount ( ) . map ( |amount| amount. into ( ) )
187+ }
188+
189+ /// Returns whether the given quantity is valid for the offer.
190+ pub fn is_valid_quantity ( & self , quantity : u64 ) -> bool {
191+ self . inner . is_valid_quantity ( quantity)
192+ }
193+
194+ /// Returns whether a quantity is expected in an [`InvoiceRequest`] for the offer.
195+ ///
196+ /// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest
197+ pub fn expects_quantity ( & self ) -> bool {
198+ self . inner . expects_quantity ( )
199+ }
200+
201+ /// Returns whether the given chain is supported by the offer.
202+ pub fn supports_chain ( & self , chain : Network ) -> bool {
203+ self . inner . supports_chain ( chain. chain_hash ( ) )
204+ }
205+
206+ /// The chains that may be used when paying a requested invoice (e.g., bitcoin mainnet).
207+ ///
208+ /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats)
209+ /// for the selected chain.
210+ pub fn chains ( & self ) -> Vec < Network > {
211+ self . inner . chains ( ) . into_iter ( ) . filter_map ( Network :: from_chain_hash) . collect ( )
212+ }
213+
214+ /// Opaque bytes set by the originator.
215+ ///
216+ /// Useful for authentication and validating fields since it is reflected in `invoice_request`
217+ /// messages along with all the other fields from the `offer`.
218+ pub fn metadata ( & self ) -> Option < Vec < u8 > > {
219+ self . inner . metadata ( ) . cloned ( )
220+ }
221+
222+ /// Seconds since the Unix epoch when an invoice should no longer be requested.
223+ ///
224+ /// If `None`, the offer does not expire.
225+ pub fn absolute_expiry_seconds ( & self ) -> Option < u64 > {
226+ self . inner . absolute_expiry ( ) . map ( |duration| duration. as_secs ( ) )
227+ }
228+
229+ /// The public key corresponding to the key used by the recipient to sign invoices.
230+ /// - If [`Offer::paths`] is empty, MUST be `Some` and contain the recipient's node id for
231+ /// sending an [`InvoiceRequest`].
232+ /// - If [`Offer::paths`] is not empty, MAY be `Some` and contain a transient id.
233+ /// - If `None`, the signing pubkey will be the final blinded node id from the
234+ /// [`BlindedMessagePath`] in [`Offer::paths`] used to send the [`InvoiceRequest`].
235+ ///
236+ /// See also [`Bolt12Invoice::signing_pubkey`].
237+ ///
238+ /// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest
239+ /// [`Bolt12Invoice::signing_pubkey`]: lightning::offers::invoice::Bolt12Invoice::signing_pubkey
240+ pub fn issuer_signing_pubkey ( & self ) -> Option < PublicKey > {
241+ self . inner . issuer_signing_pubkey ( )
242+ }
243+ }
244+
245+ impl std:: str:: FromStr for Offer {
246+ type Err = Error ;
247+
248+ fn from_str ( offer_str : & str ) -> Result < Self , Self :: Err > {
249+ offer_str
250+ . parse :: < LdkOffer > ( )
251+ . map ( |offer| Offer { inner : offer } )
252+ . map_err ( |_| Error :: InvalidOffer )
253+ }
254+ }
255+
256+ impl From < LdkOffer > for Offer {
257+ fn from ( offer : LdkOffer ) -> Self {
258+ Offer { inner : offer }
259+ }
260+ }
261+
262+ impl Deref for Offer {
263+ type Target = LdkOffer ;
264+ fn deref ( & self ) -> & Self :: Target {
265+ & self . inner
266+ }
267+ }
268+
269+ impl AsRef < LdkOffer > for Offer {
270+ fn as_ref ( & self ) -> & LdkOffer {
271+ self . deref ( )
272+ }
273+ }
274+
275+ impl std:: fmt:: Display for Offer {
276+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
277+ write ! ( f, "{}" , self . inner)
126278 }
127279}
128280
@@ -661,6 +813,13 @@ impl UniffiCustomTypeConverter for DateTime {
661813
662814#[ cfg( test) ]
663815mod tests {
816+ use std:: {
817+ num:: NonZeroU64 ,
818+ time:: { SystemTime , UNIX_EPOCH } ,
819+ } ;
820+
821+ use lightning:: offers:: offer:: { OfferBuilder , Quantity } ;
822+
664823 use super :: * ;
665824
666825 fn create_test_invoice ( ) -> ( LdkBolt11Invoice , Bolt11Invoice ) {
@@ -670,6 +829,36 @@ mod tests {
670829 ( ldk_invoice, wrapped_invoice)
671830 }
672831
832+ fn create_test_offer ( ) -> ( LdkOffer , Offer ) {
833+ let pubkey = bitcoin:: secp256k1:: PublicKey :: from_str (
834+ "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" ,
835+ )
836+ . unwrap ( ) ;
837+
838+ let expiry =
839+ ( SystemTime :: now ( ) + Duration :: from_secs ( 3600 ) ) . duration_since ( UNIX_EPOCH ) . unwrap ( ) ;
840+
841+ let quantity = NonZeroU64 :: new ( 10_000 ) . unwrap ( ) ;
842+
843+ let builder = OfferBuilder :: new ( pubkey)
844+ . description ( "Test offer description" . to_string ( ) )
845+ . amount_msats ( 100_000 )
846+ . issuer ( "Offer issuer" . to_string ( ) )
847+ . absolute_expiry ( expiry)
848+ . chain ( Network :: Bitcoin )
849+ . supported_quantity ( Quantity :: Bounded ( quantity) )
850+ . metadata ( vec ! [
851+ 0xde , 0xad , 0xbe , 0xef , 0xca , 0xfe , 0xba , 0xbe , 0x12 , 0x34 , 0x56 , 0x78 , 0x90 , 0xab ,
852+ 0xcd , 0xef ,
853+ ] )
854+ . unwrap ( ) ;
855+
856+ let ldk_offer = builder. build ( ) . unwrap ( ) ;
857+ let wrapped_offer = Offer :: from ( ldk_offer. clone ( ) ) ;
858+
859+ ( ldk_offer, wrapped_offer)
860+ }
861+
673862 #[ test]
674863 fn test_invoice_description_conversion ( ) {
675864 let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0" . to_string ( ) ;
@@ -779,4 +968,111 @@ mod tests {
779968 parsed_invoice. payment_hash( ) . to_byte_array( ) . to_vec( )
780969 ) ;
781970 }
971+
972+ #[ test]
973+ fn test_offer ( ) {
974+ let ( ldk_offer, wrapped_offer) = create_test_offer ( ) ;
975+ match ( ldk_offer. description ( ) , wrapped_offer. description ( ) ) {
976+ ( Some ( ldk_desc) , Some ( wrapped_desc) ) => {
977+ assert_eq ! ( ldk_desc. to_string( ) , wrapped_desc) ;
978+ } ,
979+ ( None , None ) => {
980+ // Both fields are missing which is expected behaviour when converting
981+ } ,
982+ ( Some ( _) , None ) => {
983+ panic ! ( "LDK offer had a description but wrapped offer did not!" ) ;
984+ } ,
985+ ( None , Some ( _) ) => {
986+ panic ! ( "Wrapped offer had a description but LDK offer did not!" ) ;
987+ } ,
988+ }
989+
990+ match ( ldk_offer. amount ( ) , wrapped_offer. amount ( ) ) {
991+ ( Some ( ldk_amount) , Some ( wrapped_amount) ) => {
992+ let ldk_amount: OfferAmount = ldk_amount. into ( ) ;
993+ assert_eq ! ( ldk_amount, wrapped_amount) ;
994+ } ,
995+ ( None , None ) => {
996+ // Both fields are missing which is expected behaviour when converting
997+ } ,
998+ ( Some ( _) , None ) => {
999+ panic ! ( "LDK offer had an amount but wrapped offer did not!" ) ;
1000+ } ,
1001+ ( None , Some ( _) ) => {
1002+ panic ! ( "Wrapped offer had an amount but LDK offer did not!" ) ;
1003+ } ,
1004+ }
1005+
1006+ match ( ldk_offer. issuer ( ) , wrapped_offer. issuer ( ) ) {
1007+ ( Some ( ldk_issuer) , Some ( wrapped_issuer) ) => {
1008+ assert_eq ! ( ldk_issuer. to_string( ) , wrapped_issuer) ;
1009+ } ,
1010+ ( None , None ) => {
1011+ // Both fields are missing which is expected behaviour when converting
1012+ } ,
1013+ ( Some ( _) , None ) => {
1014+ panic ! ( "LDK offer had an issuer but wrapped offer did not!" ) ;
1015+ } ,
1016+ ( None , Some ( _) ) => {
1017+ panic ! ( "Wrapped offer had an issuer but LDK offer did not!" ) ;
1018+ } ,
1019+ }
1020+
1021+ assert_eq ! ( ldk_offer. is_expired( ) , wrapped_offer. is_expired( ) ) ;
1022+ assert_eq ! ( ldk_offer. id( ) , wrapped_offer. id( ) ) ;
1023+ assert_eq ! ( ldk_offer. is_valid_quantity( 10_000 ) , wrapped_offer. is_valid_quantity( 10_000 ) ) ;
1024+ assert_eq ! ( ldk_offer. expects_quantity( ) , wrapped_offer. expects_quantity( ) ) ;
1025+ assert_eq ! (
1026+ ldk_offer. supports_chain( Network :: Bitcoin . chain_hash( ) ) ,
1027+ wrapped_offer. supports_chain( Network :: Bitcoin )
1028+ ) ;
1029+ assert_eq ! (
1030+ ldk_offer. chains( ) ,
1031+ wrapped_offer. chains( ) . iter( ) . map( |c| c. chain_hash( ) ) . collect:: <Vec <_>>( )
1032+ ) ;
1033+ match ( ldk_offer. metadata ( ) , wrapped_offer. metadata ( ) ) {
1034+ ( Some ( ldk_metadata) , Some ( wrapped_metadata) ) => {
1035+ assert_eq ! ( ldk_metadata. clone( ) , wrapped_metadata) ;
1036+ } ,
1037+ ( None , None ) => {
1038+ // Both fields are missing which is expected behaviour when converting
1039+ } ,
1040+ ( Some ( _) , None ) => {
1041+ panic ! ( "LDK offer had metadata but wrapped offer did not!" ) ;
1042+ } ,
1043+ ( None , Some ( _) ) => {
1044+ panic ! ( "Wrapped offer had metadata but LDK offer did not!" ) ;
1045+ } ,
1046+ }
1047+
1048+ match ( ldk_offer. absolute_expiry ( ) , wrapped_offer. absolute_expiry_seconds ( ) ) {
1049+ ( Some ( ldk_expiry) , Some ( wrapped_expiry) ) => {
1050+ assert_eq ! ( ldk_expiry. as_secs( ) , wrapped_expiry) ;
1051+ } ,
1052+ ( None , None ) => {
1053+ // Both fields are missing which is expected behaviour when converting
1054+ } ,
1055+ ( Some ( _) , None ) => {
1056+ panic ! ( "LDK offer had an absolute expiry but wrapped offer did not!" ) ;
1057+ } ,
1058+ ( None , Some ( _) ) => {
1059+ panic ! ( "Wrapped offer had an absolute expiry but LDK offer did not!" ) ;
1060+ } ,
1061+ }
1062+
1063+ match ( ldk_offer. issuer_signing_pubkey ( ) , wrapped_offer. issuer_signing_pubkey ( ) ) {
1064+ ( Some ( ldk_expiry_signing_pubkey) , Some ( wrapped_issuer_signing_pubkey) ) => {
1065+ assert_eq ! ( ldk_expiry_signing_pubkey, wrapped_issuer_signing_pubkey) ;
1066+ } ,
1067+ ( None , None ) => {
1068+ // Both fields are missing which is expected behaviour when converting
1069+ } ,
1070+ ( Some ( _) , None ) => {
1071+ panic ! ( "LDK offer had an issuer signing pubkey but wrapped offer did not!" ) ;
1072+ } ,
1073+ ( None , Some ( _) ) => {
1074+ panic ! ( "Wrapped offer had an issuer signing pubkey but LDK offer did not!" ) ;
1075+ } ,
1076+ }
1077+ }
7821078}
0 commit comments