@@ -26,7 +26,7 @@ pub use lightning::chain::channelmonitor::BalanceSource;
26
26
pub use lightning:: events:: { ClosureReason , PaymentFailureReason } ;
27
27
pub use lightning:: ln:: types:: ChannelId ;
28
28
pub use lightning:: offers:: invoice:: Bolt12Invoice ;
29
- pub use lightning:: offers:: offer:: { Offer , OfferId } ;
29
+ pub use lightning:: offers:: offer:: OfferId ;
30
30
pub use lightning:: offers:: refund:: Refund ;
31
31
pub use lightning:: routing:: gossip:: { NodeAlias , NodeId , RoutingFees } ;
32
32
pub use lightning:: util:: string:: UntrustedString ;
@@ -57,6 +57,7 @@ use bitcoin::hashes::sha256::Hash as Sha256;
57
57
use bitcoin:: hashes:: Hash ;
58
58
use bitcoin:: secp256k1:: PublicKey ;
59
59
use lightning:: ln:: channelmanager:: PaymentId ;
60
+ use lightning:: offers:: offer:: { Amount as LdkAmount , Offer as LdkOffer } ;
60
61
use lightning:: util:: ser:: Writeable ;
61
62
use lightning_invoice:: { Bolt11Invoice as LdkBolt11Invoice , Bolt11InvoiceDescriptionRef } ;
62
63
@@ -114,15 +115,166 @@ impl UniffiCustomTypeConverter for Address {
114
115
}
115
116
}
116
117
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
+ }
119
123
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
+ }
122
133
}
134
+ }
123
135
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)
126
278
}
127
279
}
128
280
@@ -661,6 +813,13 @@ impl UniffiCustomTypeConverter for DateTime {
661
813
662
814
#[ cfg( test) ]
663
815
mod tests {
816
+ use std:: {
817
+ num:: NonZeroU64 ,
818
+ time:: { SystemTime , UNIX_EPOCH } ,
819
+ } ;
820
+
821
+ use lightning:: offers:: offer:: { OfferBuilder , Quantity } ;
822
+
664
823
use super :: * ;
665
824
666
825
fn create_test_invoice ( ) -> ( LdkBolt11Invoice , Bolt11Invoice ) {
@@ -670,6 +829,36 @@ mod tests {
670
829
( ldk_invoice, wrapped_invoice)
671
830
}
672
831
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
+
673
862
#[ test]
674
863
fn test_invoice_description_conversion ( ) {
675
864
let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0" . to_string ( ) ;
@@ -779,4 +968,111 @@ mod tests {
779
968
parsed_invoice. payment_hash( ) . to_byte_array( ) . to_vec( )
780
969
) ;
781
970
}
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
+ }
782
1078
}
0 commit comments