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:: * ;
@@ -321,6 +339,12 @@ impl Offer {
321
339
}
322
340
}
323
341
342
+ impl AsRef < [ u8 ] > for Offer {
343
+ fn as_ref ( & self ) -> & [ u8 ] {
344
+ & self . bytes
345
+ }
346
+ }
347
+
324
348
impl OfferContents {
325
349
pub fn implied_chain ( & self ) -> ChainHash {
326
350
ChainHash :: using_genesis_block ( Network :: Bitcoin )
@@ -359,12 +383,27 @@ impl OfferContents {
359
383
}
360
384
}
361
385
386
+ impl Writeable for Offer {
387
+ fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
388
+ WithoutLength ( & self . bytes ) . write ( writer)
389
+ }
390
+ }
391
+
362
392
impl Writeable for OfferContents {
363
393
fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
364
394
self . as_tlv_stream ( ) . write ( writer)
365
395
}
366
396
}
367
397
398
+ impl TryFrom < Vec < u8 > > for Offer {
399
+ type Error = ParseError ;
400
+
401
+ fn try_from ( bytes : Vec < u8 > ) -> Result < Self , Self :: Error > {
402
+ let tlv_stream: OfferTlvStream = Readable :: read ( & mut & bytes[ ..] ) ?;
403
+ Offer :: try_from ( ( bytes, tlv_stream) )
404
+ }
405
+ }
406
+
368
407
/// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or
369
408
/// another currency.
370
409
#[ derive( Clone , Debug , PartialEq ) ]
@@ -400,6 +439,15 @@ impl Quantity {
400
439
Quantity :: Bounded ( NonZeroU64 :: new ( 1 ) . unwrap ( ) )
401
440
}
402
441
442
+ fn new ( quantity : Option < u64 > ) -> Self {
443
+ match quantity {
444
+ None => Quantity :: one ( ) ,
445
+ Some ( 0 ) => Quantity :: Unbounded ,
446
+ Some ( 1 ) => unreachable ! ( ) ,
447
+ Some ( n) => Quantity :: Bounded ( NonZeroU64 :: new ( n) . unwrap ( ) ) ,
448
+ }
449
+ }
450
+
403
451
fn to_tlv_record ( & self ) -> Option < u64 > {
404
452
match self {
405
453
Quantity :: Bounded ( n) => {
@@ -425,13 +473,91 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, {
425
473
( 22 , node_id: PublicKey ) ,
426
474
} ) ;
427
475
476
+ impl Bech32Encode for Offer {
477
+ const BECH32_HRP : & ' static str = "lno" ;
478
+ }
479
+
480
+ type ParsedOffer = ( Vec < u8 > , OfferTlvStream ) ;
481
+
482
+ impl FromStr for Offer {
483
+ type Err = ParseError ;
484
+
485
+ fn from_str ( s : & str ) -> Result < Self , <Self as FromStr >:: Err > {
486
+ Self :: from_bech32_str ( s)
487
+ }
488
+ }
489
+
490
+ impl TryFrom < ParsedOffer > for Offer {
491
+ type Error = ParseError ;
492
+
493
+ fn try_from ( offer : ParsedOffer ) -> Result < Self , Self :: Error > {
494
+ let ( bytes, tlv_stream) = offer;
495
+ let contents = OfferContents :: try_from ( tlv_stream) ?;
496
+ Ok ( Offer { bytes, contents } )
497
+ }
498
+ }
499
+
500
+ impl TryFrom < OfferTlvStream > for OfferContents {
501
+ type Error = SemanticError ;
502
+
503
+ fn try_from ( tlv_stream : OfferTlvStream ) -> Result < Self , Self :: Error > {
504
+ let OfferTlvStream {
505
+ chains, metadata, currency, amount, description, features, absolute_expiry, paths,
506
+ issuer, quantity_max, node_id,
507
+ } = tlv_stream;
508
+
509
+ let amount = match ( currency, amount) {
510
+ ( None , None ) => None ,
511
+ ( None , Some ( amount_msats) ) => Some ( Amount :: Bitcoin { amount_msats } ) ,
512
+ ( Some ( _) , None ) => return Err ( SemanticError :: MissingAmount ) ,
513
+ ( Some ( iso4217_code) , Some ( amount) ) => Some ( Amount :: Currency { iso4217_code, amount } ) ,
514
+ } ;
515
+
516
+ let description = match description {
517
+ None => return Err ( SemanticError :: MissingDescription ) ,
518
+ Some ( description) => description,
519
+ } ;
520
+
521
+ let features = features. unwrap_or_else ( OfferFeatures :: empty) ;
522
+
523
+ let absolute_expiry = absolute_expiry
524
+ . map ( |seconds_from_epoch| Duration :: from_secs ( seconds_from_epoch) ) ;
525
+
526
+ let paths = match paths {
527
+ Some ( paths) if paths. is_empty ( ) => return Err ( SemanticError :: MissingPaths ) ,
528
+ paths => paths,
529
+ } ;
530
+
531
+ let supported_quantity = match quantity_max {
532
+ Some ( 1 ) => return Err ( SemanticError :: InvalidQuantity ) ,
533
+ _ => Quantity :: new ( quantity_max) ,
534
+ } ;
535
+
536
+ if node_id. is_none ( ) {
537
+ return Err ( SemanticError :: MissingNodeId ) ;
538
+ }
539
+
540
+ Ok ( OfferContents {
541
+ chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
542
+ supported_quantity, signing_pubkey : node_id,
543
+ } )
544
+ }
545
+ }
546
+
547
+ impl core:: fmt:: Display for Offer {
548
+ fn fmt ( & self , f : & mut core:: fmt:: Formatter ) -> Result < ( ) , core:: fmt:: Error > {
549
+ self . fmt_bech32_str ( f)
550
+ }
551
+ }
552
+
428
553
#[ cfg( test) ]
429
554
mod tests {
430
- use super :: { Amount , OfferBuilder , Quantity } ;
555
+ use super :: { Amount , Offer , OfferBuilder , Quantity } ;
431
556
432
557
use bitcoin:: blockdata:: constants:: ChainHash ;
433
558
use bitcoin:: network:: constants:: Network ;
434
559
use bitcoin:: secp256k1:: { PublicKey , Secp256k1 , SecretKey } ;
560
+ use core:: convert:: TryFrom ;
435
561
use core:: num:: NonZeroU64 ;
436
562
use core:: time:: Duration ;
437
563
use crate :: ln:: features:: OfferFeatures ;
@@ -454,7 +580,7 @@ mod tests {
454
580
let offer = OfferBuilder :: new ( "foo" . into ( ) , pubkey ( 42 ) ) . build ( ) . unwrap ( ) ;
455
581
let tlv_stream = offer. as_tlv_stream ( ) ;
456
582
let mut buffer = Vec :: new ( ) ;
457
- offer. contents . write ( & mut buffer) . unwrap ( ) ;
583
+ offer. write ( & mut buffer) . unwrap ( ) ;
458
584
459
585
assert_eq ! ( offer. bytes, buffer. as_slice( ) ) ;
460
586
assert_eq ! ( offer. chains( ) , vec![ ChainHash :: using_genesis_block( Network :: Bitcoin ) ] ) ;
@@ -481,6 +607,10 @@ mod tests {
481
607
assert_eq ! ( tlv_stream. issuer, None ) ;
482
608
assert_eq ! ( tlv_stream. quantity_max, None ) ;
483
609
assert_eq ! ( tlv_stream. node_id, Some ( & pubkey( 42 ) ) ) ;
610
+
611
+ if let Err ( e) = Offer :: try_from ( buffer) {
612
+ panic ! ( "error parsing offer: {:?}" , e) ;
613
+ }
484
614
}
485
615
486
616
#[ test]
@@ -707,3 +837,88 @@ mod tests {
707
837
assert_eq ! ( tlv_stream. quantity_max, None ) ;
708
838
}
709
839
}
840
+
841
+ #[ cfg( test) ]
842
+ mod bolt12_tests {
843
+ use super :: { Offer , ParseError } ;
844
+ use bitcoin:: bech32;
845
+ use crate :: ln:: msgs:: DecodeError ;
846
+
847
+ // TODO: Remove once test vectors are updated.
848
+ #[ ignore]
849
+ #[ test]
850
+ fn encodes_offer_as_bech32_without_checksum ( ) {
851
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
852
+ let offer = dbg ! ( encoded_offer. parse:: <Offer >( ) . unwrap( ) ) ;
853
+ let reencoded_offer = offer. to_string ( ) ;
854
+ dbg ! ( reencoded_offer. parse:: <Offer >( ) . unwrap( ) ) ;
855
+ assert_eq ! ( reencoded_offer, encoded_offer) ;
856
+ }
857
+
858
+ // TODO: Remove once test vectors are updated.
859
+ #[ ignore]
860
+ #[ test]
861
+ fn parses_bech32_encoded_offers ( ) {
862
+ let offers = [
863
+ // BOLT 12 test vectors
864
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
865
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
866
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
867
+ "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y" ,
868
+ "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\n sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r \n astpwuh73k29qs+\r y" ,
869
+ // Two blinded paths
870
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
871
+ ] ;
872
+ for encoded_offer in & offers {
873
+ if let Err ( e) = encoded_offer. parse :: < Offer > ( ) {
874
+ panic ! ( "Invalid offer ({:?}): {}" , e, encoded_offer) ;
875
+ }
876
+ }
877
+ }
878
+
879
+ #[ test]
880
+ fn fails_parsing_bech32_encoded_offers_with_invalid_continuations ( ) {
881
+ let offers = [
882
+ // BOLT 12 test vectors
883
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+" ,
884
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ " ,
885
+ "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
886
+ "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
887
+ "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
888
+ ] ;
889
+ for encoded_offer in & offers {
890
+ match encoded_offer. parse :: < Offer > ( ) {
891
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
892
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidContinuation ) ,
893
+ }
894
+ }
895
+
896
+ }
897
+
898
+ #[ test]
899
+ fn fails_parsing_bech32_encoded_offer_with_invalid_hrp ( ) {
900
+ let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
901
+ match encoded_offer. parse :: < Offer > ( ) {
902
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
903
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidBech32Hrp ) ,
904
+ }
905
+ }
906
+
907
+ #[ test]
908
+ fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data ( ) {
909
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso" ;
910
+ match encoded_offer. parse :: < Offer > ( ) {
911
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
912
+ Err ( e) => assert_eq ! ( e, ParseError :: Bech32 ( bech32:: Error :: InvalidChar ( 'o' ) ) ) ,
913
+ }
914
+ }
915
+
916
+ #[ test]
917
+ fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data ( ) {
918
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq" ;
919
+ match encoded_offer. parse :: < Offer > ( ) {
920
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
921
+ Err ( e) => assert_eq ! ( e, ParseError :: Decode ( DecodeError :: InvalidValue ) ) ,
922
+ }
923
+ }
924
+ }
0 commit comments