18
18
//! extern crate core;
19
19
//! extern crate lightning;
20
20
//!
21
+ //! use core::convert::TryFrom;
21
22
//! use core::num::NonZeroU64;
22
23
//! use core::time::Duration;
23
24
//!
24
25
//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
25
- //! use lightning::offers::offer::{OfferBuilder, Quantity};
26
+ //! use lightning::offers::offer::{Offer, OfferBuilder, Quantity};
27
+ //! use lightning::offers::parse::ParseError;
28
+ //! use lightning::util::ser::{Readable, Writeable};
26
29
//!
27
- //! # use bitcoin::secp256k1;
28
30
//! # use lightning::onion_message::BlindedPath;
29
31
//! # #[cfg(feature = "std")]
30
32
//! # use std::time::SystemTime;
33
35
//! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() }
34
36
//! #
35
37
//! # #[cfg(feature = "std")]
36
- //! # fn build() -> Result<(), secp256k1::Error > {
38
+ //! # fn build() -> Result<(), ParseError > {
37
39
//! let secp_ctx = Secp256k1::new();
38
- //! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])? );
40
+ //! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap() );
39
41
//! let pubkey = PublicKey::from(keys);
40
42
//!
41
43
//! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
48
50
//! .path(create_another_blinded_path())
49
51
//! .build()
50
52
//! .unwrap();
53
+ //!
54
+ //! // Encode as a bech32 string for use in a QR code.
55
+ //! let encoded_offer = offer.to_string();
56
+ //!
57
+ //! // Parse from a bech32 string after scanning from a QR code.
58
+ //! let offer = encoded_offer.parse::<Offer>()?;
59
+ //!
60
+ //! // Encode offer as raw bytes.
61
+ //! let mut bytes = Vec::new();
62
+ //! offer.write(&mut bytes).unwrap();
63
+ //!
64
+ //! // Decode raw bytes into an offer.
65
+ //! let offer = Offer::try_from(bytes)?;
51
66
//! # Ok(())
52
67
//! # }
53
68
//! ```
54
69
55
70
use bitcoin:: blockdata:: constants:: ChainHash ;
56
71
use bitcoin:: network:: constants:: Network ;
57
72
use bitcoin:: secp256k1:: PublicKey ;
73
+ use core:: convert:: TryFrom ;
58
74
use core:: num:: NonZeroU64 ;
75
+ use core:: str:: FromStr ;
59
76
use core:: time:: Duration ;
60
77
use crate :: io;
61
78
use crate :: ln:: features:: OfferFeatures ;
62
79
use crate :: ln:: msgs:: MAX_VALUE_MSAT ;
80
+ use crate :: offers:: parse:: { Bech32Encode , ParseError , SemanticError } ;
63
81
use crate :: onion_message:: BlindedPath ;
64
- use crate :: util:: ser:: { HighZeroBytesDroppedBigSize , WithoutLength , Writeable , Writer } ;
82
+ use crate :: util:: ser:: { HighZeroBytesDroppedBigSize , Readable , WithoutLength , Writeable , Writer } ;
65
83
use crate :: util:: string:: PrintableString ;
66
84
67
85
use crate :: prelude:: * ;
@@ -315,6 +333,12 @@ impl Offer {
315
333
}
316
334
}
317
335
336
+ impl AsRef < [ u8 ] > for Offer {
337
+ fn as_ref ( & self ) -> & [ u8 ] {
338
+ & self . bytes
339
+ }
340
+ }
341
+
318
342
impl OfferContents {
319
343
pub fn supported_quantity ( & self ) -> Quantity {
320
344
self . supported_quantity
@@ -349,12 +373,27 @@ impl OfferContents {
349
373
}
350
374
}
351
375
376
+ impl Writeable for Offer {
377
+ fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
378
+ WithoutLength ( & self . bytes ) . write ( writer)
379
+ }
380
+ }
381
+
352
382
impl Writeable for OfferContents {
353
383
fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
354
384
self . as_tlv_stream ( ) . write ( writer)
355
385
}
356
386
}
357
387
388
+ impl TryFrom < Vec < u8 > > for Offer {
389
+ type Error = ParseError ;
390
+
391
+ fn try_from ( bytes : Vec < u8 > ) -> Result < Self , Self :: Error > {
392
+ let tlv_stream: OfferTlvStream = Readable :: read ( & mut & bytes[ ..] ) ?;
393
+ Offer :: try_from ( ( bytes, tlv_stream) )
394
+ }
395
+ }
396
+
358
397
/// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or
359
398
/// another currency.
360
399
#[ derive( Clone , Debug , PartialEq ) ]
@@ -390,6 +429,15 @@ impl Quantity {
390
429
Quantity :: Bounded ( NonZeroU64 :: new ( 1 ) . unwrap ( ) )
391
430
}
392
431
432
+ fn new ( quantity : Option < u64 > ) -> Self {
433
+ match quantity {
434
+ None => Quantity :: one ( ) ,
435
+ Some ( 0 ) => Quantity :: Unbounded ,
436
+ Some ( 1 ) => unreachable ! ( ) ,
437
+ Some ( n) => Quantity :: Bounded ( NonZeroU64 :: new ( n) . unwrap ( ) ) ,
438
+ }
439
+ }
440
+
393
441
fn to_tlv_record ( & self ) -> Option < u64 > {
394
442
match self {
395
443
Quantity :: Bounded ( n) => {
@@ -415,13 +463,91 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, {
415
463
( 22 , node_id: PublicKey ) ,
416
464
} ) ;
417
465
466
+ impl Bech32Encode for Offer {
467
+ const BECH32_HRP : & ' static str = "lno" ;
468
+ }
469
+
470
+ type ParsedOffer = ( Vec < u8 > , OfferTlvStream ) ;
471
+
472
+ impl FromStr for Offer {
473
+ type Err = ParseError ;
474
+
475
+ fn from_str ( s : & str ) -> Result < Self , <Self as FromStr >:: Err > {
476
+ Self :: from_bech32_str ( s)
477
+ }
478
+ }
479
+
480
+ impl TryFrom < ParsedOffer > for Offer {
481
+ type Error = ParseError ;
482
+
483
+ fn try_from ( offer : ParsedOffer ) -> Result < Self , Self :: Error > {
484
+ let ( bytes, tlv_stream) = offer;
485
+ let contents = OfferContents :: try_from ( tlv_stream) ?;
486
+ Ok ( Offer { bytes, contents } )
487
+ }
488
+ }
489
+
490
+ impl TryFrom < OfferTlvStream > for OfferContents {
491
+ type Error = SemanticError ;
492
+
493
+ fn try_from ( tlv_stream : OfferTlvStream ) -> Result < Self , Self :: Error > {
494
+ let OfferTlvStream {
495
+ chains, metadata, currency, amount, description, features, absolute_expiry, paths,
496
+ issuer, quantity_max, node_id,
497
+ } = tlv_stream;
498
+
499
+ let amount = match ( currency, amount) {
500
+ ( None , None ) => None ,
501
+ ( None , Some ( amount_msats) ) => Some ( Amount :: Bitcoin { amount_msats } ) ,
502
+ ( Some ( _) , None ) => return Err ( SemanticError :: MissingAmount ) ,
503
+ ( Some ( iso4217_code) , Some ( amount) ) => Some ( Amount :: Currency { iso4217_code, amount } ) ,
504
+ } ;
505
+
506
+ let description = match description {
507
+ None => return Err ( SemanticError :: MissingDescription ) ,
508
+ Some ( description) => description,
509
+ } ;
510
+
511
+ let features = features. unwrap_or_else ( OfferFeatures :: empty) ;
512
+
513
+ let absolute_expiry = absolute_expiry
514
+ . map ( |seconds_from_epoch| Duration :: from_secs ( seconds_from_epoch) ) ;
515
+
516
+ let paths = match paths {
517
+ Some ( paths) if paths. is_empty ( ) => return Err ( SemanticError :: MissingPaths ) ,
518
+ paths => paths,
519
+ } ;
520
+
521
+ let supported_quantity = match quantity_max {
522
+ Some ( 1 ) => return Err ( SemanticError :: InvalidQuantity ) ,
523
+ _ => Quantity :: new ( quantity_max) ,
524
+ } ;
525
+
526
+ if node_id. is_none ( ) {
527
+ return Err ( SemanticError :: MissingNodeId ) ;
528
+ }
529
+
530
+ Ok ( OfferContents {
531
+ chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
532
+ supported_quantity, signing_pubkey : node_id,
533
+ } )
534
+ }
535
+ }
536
+
537
+ impl core:: fmt:: Display for Offer {
538
+ fn fmt ( & self , f : & mut core:: fmt:: Formatter ) -> Result < ( ) , core:: fmt:: Error > {
539
+ self . fmt_bech32_str ( f)
540
+ }
541
+ }
542
+
418
543
#[ cfg( test) ]
419
544
mod tests {
420
- use super :: { Amount , OfferBuilder , Quantity } ;
545
+ use super :: { Amount , Offer , OfferBuilder , Quantity } ;
421
546
422
547
use bitcoin:: blockdata:: constants:: ChainHash ;
423
548
use bitcoin:: network:: constants:: Network ;
424
549
use bitcoin:: secp256k1:: { PublicKey , Secp256k1 , SecretKey } ;
550
+ use core:: convert:: TryFrom ;
425
551
use core:: num:: NonZeroU64 ;
426
552
use core:: time:: Duration ;
427
553
use crate :: ln:: features:: OfferFeatures ;
@@ -444,7 +570,7 @@ mod tests {
444
570
let offer = OfferBuilder :: new ( "foo" . into ( ) , pubkey ( 42 ) ) . build ( ) . unwrap ( ) ;
445
571
let tlv_stream = offer. as_tlv_stream ( ) ;
446
572
let mut buffer = Vec :: new ( ) ;
447
- offer. contents . write ( & mut buffer) . unwrap ( ) ;
573
+ offer. write ( & mut buffer) . unwrap ( ) ;
448
574
449
575
assert_eq ! ( offer. bytes, buffer. as_slice( ) ) ;
450
576
assert_eq ! ( offer. chains( ) , vec![ ChainHash :: using_genesis_block( Network :: Bitcoin ) ] ) ;
@@ -471,6 +597,10 @@ mod tests {
471
597
assert_eq ! ( tlv_stream. issuer, None ) ;
472
598
assert_eq ! ( tlv_stream. quantity_max, None ) ;
473
599
assert_eq ! ( tlv_stream. node_id, Some ( & pubkey( 42 ) ) ) ;
600
+
601
+ if let Err ( e) = Offer :: try_from ( buffer) {
602
+ panic ! ( "error parsing offer: {:?}" , e) ;
603
+ }
474
604
}
475
605
476
606
#[ test]
@@ -693,3 +823,88 @@ mod tests {
693
823
assert_eq ! ( tlv_stream. quantity_max, None ) ;
694
824
}
695
825
}
826
+
827
+ #[ cfg( test) ]
828
+ mod bolt12_tests {
829
+ use super :: { Offer , ParseError } ;
830
+ use bitcoin:: bech32;
831
+ use crate :: ln:: msgs:: DecodeError ;
832
+
833
+ // TODO: Remove once test vectors are updated.
834
+ #[ ignore]
835
+ #[ test]
836
+ fn encodes_offer_as_bech32_without_checksum ( ) {
837
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
838
+ let offer = dbg ! ( encoded_offer. parse:: <Offer >( ) . unwrap( ) ) ;
839
+ let reencoded_offer = offer. to_string ( ) ;
840
+ dbg ! ( reencoded_offer. parse:: <Offer >( ) . unwrap( ) ) ;
841
+ assert_eq ! ( reencoded_offer, encoded_offer) ;
842
+ }
843
+
844
+ // TODO: Remove once test vectors are updated.
845
+ #[ ignore]
846
+ #[ test]
847
+ fn parses_bech32_encoded_offers ( ) {
848
+ let offers = [
849
+ // BOLT 12 test vectors
850
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
851
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
852
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
853
+ "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y" ,
854
+ "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\n sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r \n astpwuh73k29qs+\r y" ,
855
+ // Two blinded paths
856
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
857
+ ] ;
858
+ for encoded_offer in & offers {
859
+ if let Err ( e) = encoded_offer. parse :: < Offer > ( ) {
860
+ panic ! ( "Invalid offer ({:?}): {}" , e, encoded_offer) ;
861
+ }
862
+ }
863
+ }
864
+
865
+ #[ test]
866
+ fn fails_parsing_bech32_encoded_offers_with_invalid_continuations ( ) {
867
+ let offers = [
868
+ // BOLT 12 test vectors
869
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+" ,
870
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ " ,
871
+ "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
872
+ "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
873
+ "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
874
+ ] ;
875
+ for encoded_offer in & offers {
876
+ match encoded_offer. parse :: < Offer > ( ) {
877
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
878
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidContinuation ) ,
879
+ }
880
+ }
881
+
882
+ }
883
+
884
+ #[ test]
885
+ fn fails_parsing_bech32_encoded_offer_with_invalid_hrp ( ) {
886
+ let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
887
+ match encoded_offer. parse :: < Offer > ( ) {
888
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
889
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidBech32Hrp ) ,
890
+ }
891
+ }
892
+
893
+ #[ test]
894
+ fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data ( ) {
895
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso" ;
896
+ match encoded_offer. parse :: < Offer > ( ) {
897
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
898
+ Err ( e) => assert_eq ! ( e, ParseError :: Bech32 ( bech32:: Error :: InvalidChar ( 'o' ) ) ) ,
899
+ }
900
+ }
901
+
902
+ #[ test]
903
+ fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data ( ) {
904
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq" ;
905
+ match encoded_offer. parse :: < Offer > ( ) {
906
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
907
+ Err ( e) => assert_eq ! ( e, ParseError :: Decode ( DecodeError :: InvalidValue ) ) ,
908
+ }
909
+ }
910
+ }
0 commit comments