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 ) ]
@@ -425,13 +464,91 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, {
425
464
( 22 , node_id: PublicKey ) ,
426
465
} ) ;
427
466
467
+ impl Bech32Encode for Offer {
468
+ const BECH32_HRP : & ' static str = "lno" ;
469
+ }
470
+
471
+ type ParsedOffer = ( Vec < u8 > , OfferTlvStream ) ;
472
+
473
+ impl FromStr for Offer {
474
+ type Err = ParseError ;
475
+
476
+ fn from_str ( s : & str ) -> Result < Self , <Self as FromStr >:: Err > {
477
+ Self :: from_bech32_str ( s)
478
+ }
479
+ }
480
+
481
+ impl TryFrom < ParsedOffer > for Offer {
482
+ type Error = ParseError ;
483
+
484
+ fn try_from ( offer : ParsedOffer ) -> Result < Self , Self :: Error > {
485
+ let ( bytes, tlv_stream) = offer;
486
+ let contents = OfferContents :: try_from ( tlv_stream) ?;
487
+ Ok ( Offer { bytes, contents } )
488
+ }
489
+ }
490
+
491
+ impl TryFrom < OfferTlvStream > for OfferContents {
492
+ type Error = SemanticError ;
493
+
494
+ fn try_from ( tlv_stream : OfferTlvStream ) -> Result < Self , Self :: Error > {
495
+ let OfferTlvStream {
496
+ chains, metadata, currency, amount, description, features, absolute_expiry, paths,
497
+ issuer, quantity_max, node_id,
498
+ } = tlv_stream;
499
+
500
+ let amount = match ( currency, amount) {
501
+ ( None , None ) => None ,
502
+ ( None , Some ( amount_msats) ) if amount_msats > MAX_VALUE_MSAT => {
503
+ return Err ( SemanticError :: InvalidAmount ) ;
504
+ } ,
505
+ ( None , Some ( amount_msats) ) => Some ( Amount :: Bitcoin { amount_msats } ) ,
506
+ ( Some ( _) , None ) => return Err ( SemanticError :: MissingAmount ) ,
507
+ ( Some ( iso4217_code) , Some ( amount) ) => Some ( Amount :: Currency { iso4217_code, amount } ) ,
508
+ } ;
509
+
510
+ let description = match description {
511
+ None => return Err ( SemanticError :: MissingDescription ) ,
512
+ Some ( description) => description,
513
+ } ;
514
+
515
+ let features = features. unwrap_or_else ( OfferFeatures :: empty) ;
516
+
517
+ let absolute_expiry = absolute_expiry
518
+ . map ( |seconds_from_epoch| Duration :: from_secs ( seconds_from_epoch) ) ;
519
+
520
+ let supported_quantity = match quantity_max {
521
+ None => Quantity :: one ( ) ,
522
+ Some ( 0 ) => Quantity :: Unbounded ,
523
+ Some ( 1 ) => return Err ( SemanticError :: InvalidQuantity ) ,
524
+ Some ( n) => Quantity :: Bounded ( NonZeroU64 :: new ( n) . unwrap ( ) ) ,
525
+ } ;
526
+
527
+ if node_id. is_none ( ) {
528
+ return Err ( SemanticError :: MissingSigningPubkey ) ;
529
+ }
530
+
531
+ Ok ( OfferContents {
532
+ chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
533
+ supported_quantity, signing_pubkey : node_id,
534
+ } )
535
+ }
536
+ }
537
+
538
+ impl core:: fmt:: Display for Offer {
539
+ fn fmt ( & self , f : & mut core:: fmt:: Formatter ) -> Result < ( ) , core:: fmt:: Error > {
540
+ self . fmt_bech32_str ( f)
541
+ }
542
+ }
543
+
428
544
#[ cfg( test) ]
429
545
mod tests {
430
- use super :: { Amount , OfferBuilder , Quantity } ;
546
+ use super :: { Amount , Offer , OfferBuilder , Quantity } ;
431
547
432
548
use bitcoin:: blockdata:: constants:: ChainHash ;
433
549
use bitcoin:: network:: constants:: Network ;
434
550
use bitcoin:: secp256k1:: { PublicKey , Secp256k1 , SecretKey } ;
551
+ use core:: convert:: TryFrom ;
435
552
use core:: num:: NonZeroU64 ;
436
553
use core:: time:: Duration ;
437
554
use crate :: ln:: features:: OfferFeatures ;
@@ -454,7 +571,7 @@ mod tests {
454
571
let offer = OfferBuilder :: new ( "foo" . into ( ) , pubkey ( 42 ) ) . build ( ) . unwrap ( ) ;
455
572
let tlv_stream = offer. as_tlv_stream ( ) ;
456
573
let mut buffer = Vec :: new ( ) ;
457
- offer. contents . write ( & mut buffer) . unwrap ( ) ;
574
+ offer. write ( & mut buffer) . unwrap ( ) ;
458
575
459
576
assert_eq ! ( offer. bytes, buffer. as_slice( ) ) ;
460
577
assert_eq ! ( offer. chains( ) , vec![ ChainHash :: using_genesis_block( Network :: Bitcoin ) ] ) ;
@@ -481,6 +598,10 @@ mod tests {
481
598
assert_eq ! ( tlv_stream. issuer, None ) ;
482
599
assert_eq ! ( tlv_stream. quantity_max, None ) ;
483
600
assert_eq ! ( tlv_stream. node_id, Some ( & pubkey( 42 ) ) ) ;
601
+
602
+ if let Err ( e) = Offer :: try_from ( buffer) {
603
+ panic ! ( "error parsing offer: {:?}" , e) ;
604
+ }
484
605
}
485
606
486
607
#[ test]
@@ -707,3 +828,88 @@ mod tests {
707
828
assert_eq ! ( tlv_stream. quantity_max, None ) ;
708
829
}
709
830
}
831
+
832
+ #[ cfg( test) ]
833
+ mod bech32_tests {
834
+ use super :: { Offer , ParseError } ;
835
+ use bitcoin:: bech32;
836
+ use crate :: ln:: msgs:: DecodeError ;
837
+
838
+ // TODO: Remove once test vectors are updated.
839
+ #[ ignore]
840
+ #[ test]
841
+ fn encodes_offer_as_bech32_without_checksum ( ) {
842
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
843
+ let offer = dbg ! ( encoded_offer. parse:: <Offer >( ) . unwrap( ) ) ;
844
+ let reencoded_offer = offer. to_string ( ) ;
845
+ dbg ! ( reencoded_offer. parse:: <Offer >( ) . unwrap( ) ) ;
846
+ assert_eq ! ( reencoded_offer, encoded_offer) ;
847
+ }
848
+
849
+ // TODO: Remove once test vectors are updated.
850
+ #[ ignore]
851
+ #[ test]
852
+ fn parses_bech32_encoded_offers ( ) {
853
+ let offers = [
854
+ // BOLT 12 test vectors
855
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
856
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
857
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
858
+ "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y" ,
859
+ "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\n sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r \n astpwuh73k29qs+\r y" ,
860
+ // Two blinded paths
861
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
862
+ ] ;
863
+ for encoded_offer in & offers {
864
+ if let Err ( e) = encoded_offer. parse :: < Offer > ( ) {
865
+ panic ! ( "Invalid offer ({:?}): {}" , e, encoded_offer) ;
866
+ }
867
+ }
868
+ }
869
+
870
+ #[ test]
871
+ fn fails_parsing_bech32_encoded_offers_with_invalid_continuations ( ) {
872
+ let offers = [
873
+ // BOLT 12 test vectors
874
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+" ,
875
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ " ,
876
+ "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
877
+ "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
878
+ "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
879
+ ] ;
880
+ for encoded_offer in & offers {
881
+ match encoded_offer. parse :: < Offer > ( ) {
882
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
883
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidContinuation ) ,
884
+ }
885
+ }
886
+
887
+ }
888
+
889
+ #[ test]
890
+ fn fails_parsing_bech32_encoded_offer_with_invalid_hrp ( ) {
891
+ let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
892
+ match encoded_offer. parse :: < Offer > ( ) {
893
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
894
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidBech32Hrp ) ,
895
+ }
896
+ }
897
+
898
+ #[ test]
899
+ fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data ( ) {
900
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso" ;
901
+ match encoded_offer. parse :: < Offer > ( ) {
902
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
903
+ Err ( e) => assert_eq ! ( e, ParseError :: Bech32 ( bech32:: Error :: InvalidChar ( 'o' ) ) ) ,
904
+ }
905
+ }
906
+
907
+ #[ test]
908
+ fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data ( ) {
909
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq" ;
910
+ match encoded_offer. parse :: < Offer > ( ) {
911
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
912
+ Err ( e) => assert_eq ! ( e, ParseError :: Decode ( DecodeError :: InvalidValue ) ) ,
913
+ }
914
+ }
915
+ }
0 commit comments