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::{Amount, OfferBuilder, Quantity};
26
+ //! use lightning::offers::offer::{Amount, 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:: * ;
@@ -169,11 +187,9 @@ impl OfferBuilder {
169
187
170
188
/// Builds an [`Offer`] from the builder's settings.
171
189
pub fn build ( self ) -> Result < Offer , ( ) > {
190
+ // TODO: Also check for Amount::Currency
172
191
if let Some ( Amount :: Currency { .. } ) = self . offer . amount {
173
- return Err ( ( ) ) ;
174
- }
175
-
176
- if self . offer . amount_msats ( ) > MAX_VALUE_MSAT {
192
+ } else if self . offer . amount_msats ( ) > MAX_VALUE_MSAT {
177
193
return Err ( ( ) ) ;
178
194
}
179
195
@@ -302,6 +318,12 @@ impl Offer {
302
318
}
303
319
}
304
320
321
+ impl AsRef < [ u8 ] > for Offer {
322
+ fn as_ref ( & self ) -> & [ u8 ] {
323
+ & self . bytes
324
+ }
325
+ }
326
+
305
327
impl OfferContents {
306
328
pub fn amount_msats ( & self ) -> u64 {
307
329
self . amount . as_ref ( ) . map ( Amount :: as_msats) . unwrap_or ( 0 )
@@ -340,12 +362,27 @@ impl OfferContents {
340
362
}
341
363
}
342
364
365
+ impl Writeable for Offer {
366
+ fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
367
+ WithoutLength ( & self . bytes ) . write ( writer)
368
+ }
369
+ }
370
+
343
371
impl Writeable for OfferContents {
344
372
fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
345
373
self . as_tlv_stream ( ) . write ( writer)
346
374
}
347
375
}
348
376
377
+ impl TryFrom < Vec < u8 > > for Offer {
378
+ type Error = ParseError ;
379
+
380
+ fn try_from ( bytes : Vec < u8 > ) -> Result < Self , Self :: Error > {
381
+ let tlv_stream: OfferTlvStream = Readable :: read ( & mut & bytes[ ..] ) ?;
382
+ Offer :: try_from ( ( bytes, tlv_stream) )
383
+ }
384
+ }
385
+
349
386
/// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or
350
387
/// another currency.
351
388
#[ derive( Clone , Debug , PartialEq ) ]
@@ -389,6 +426,14 @@ pub enum Quantity {
389
426
}
390
427
391
428
impl Quantity {
429
+ fn new ( quantity : Option < u64 > ) -> Self {
430
+ match quantity {
431
+ Some ( 1 ) | None => Quantity :: One ,
432
+ Some ( 0 ) => Quantity :: Many ,
433
+ Some ( n) => Quantity :: Max ( NonZeroU64 :: new ( n) . unwrap ( ) ) ,
434
+ }
435
+ }
436
+
392
437
fn to_tlv_record ( & self ) -> Option < u64 > {
393
438
match self {
394
439
Quantity :: One => None ,
@@ -412,13 +457,88 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, {
412
457
( 22 , node_id: PublicKey ) ,
413
458
} ) ;
414
459
460
+ impl Bech32Encode for Offer {
461
+ const BECH32_HRP : & ' static str = "lno" ;
462
+ }
463
+
464
+ type ParsedOffer = ( Vec < u8 > , OfferTlvStream ) ;
465
+
466
+ impl FromStr for Offer {
467
+ type Err = ParseError ;
468
+
469
+ fn from_str ( s : & str ) -> Result < Self , <Self as FromStr >:: Err > {
470
+ Self :: from_bech32_str ( s)
471
+ }
472
+ }
473
+
474
+ impl TryFrom < ParsedOffer > for Offer {
475
+ type Error = ParseError ;
476
+
477
+ fn try_from ( offer : ParsedOffer ) -> Result < Self , Self :: Error > {
478
+ let ( bytes, tlv_stream) = offer;
479
+ let contents = OfferContents :: try_from ( tlv_stream) ?;
480
+ Ok ( Offer { bytes, contents } )
481
+ }
482
+ }
483
+
484
+ impl TryFrom < OfferTlvStream > for OfferContents {
485
+ type Error = SemanticError ;
486
+
487
+ fn try_from ( tlv_stream : OfferTlvStream ) -> Result < Self , Self :: Error > {
488
+ let OfferTlvStream {
489
+ chains, metadata, currency, amount, description, features, absolute_expiry, paths,
490
+ issuer, quantity_max, node_id,
491
+ } = tlv_stream;
492
+
493
+ let amount = match ( currency, amount) {
494
+ ( None , None ) => None ,
495
+ ( None , Some ( amount_msats) ) => Some ( Amount :: Bitcoin { amount_msats } ) ,
496
+ ( Some ( _) , None ) => return Err ( SemanticError :: MissingAmount ) ,
497
+ ( Some ( iso4217_code) , Some ( amount) ) => Some ( Amount :: Currency { iso4217_code, amount } ) ,
498
+ } ;
499
+
500
+ let description = match description {
501
+ None => return Err ( SemanticError :: MissingDescription ) ,
502
+ Some ( description) => description,
503
+ } ;
504
+
505
+ let features = features. unwrap_or_else ( OfferFeatures :: empty) ;
506
+
507
+ let absolute_expiry = absolute_expiry
508
+ . map ( |seconds_from_epoch| Duration :: from_secs ( seconds_from_epoch) ) ;
509
+
510
+ let paths = match paths {
511
+ Some ( paths) if paths. is_empty ( ) => return Err ( SemanticError :: MissingPaths ) ,
512
+ paths => paths,
513
+ } ;
514
+
515
+ let supported_quantity = Quantity :: new ( quantity_max) ;
516
+
517
+ if node_id. is_none ( ) {
518
+ return Err ( SemanticError :: MissingNodeId ) ;
519
+ }
520
+
521
+ Ok ( OfferContents {
522
+ chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
523
+ supported_quantity, signing_pubkey : node_id,
524
+ } )
525
+ }
526
+ }
527
+
528
+ impl core:: fmt:: Display for Offer {
529
+ fn fmt ( & self , f : & mut core:: fmt:: Formatter ) -> Result < ( ) , core:: fmt:: Error > {
530
+ self . fmt_bech32_str ( f)
531
+ }
532
+ }
533
+
415
534
#[ cfg( test) ]
416
535
mod tests {
417
- use super :: { Amount , OfferBuilder , Quantity } ;
536
+ use super :: { Amount , Offer , OfferBuilder , Quantity } ;
418
537
419
538
use bitcoin:: blockdata:: constants:: ChainHash ;
420
539
use bitcoin:: network:: constants:: Network ;
421
540
use bitcoin:: secp256k1:: { PublicKey , Secp256k1 , SecretKey } ;
541
+ use core:: convert:: TryFrom ;
422
542
use core:: num:: NonZeroU64 ;
423
543
use core:: time:: Duration ;
424
544
use crate :: ln:: features:: OfferFeatures ;
@@ -441,7 +561,7 @@ mod tests {
441
561
let offer = OfferBuilder :: new ( "foo" . into ( ) , pubkey ( 42 ) ) . build ( ) . unwrap ( ) ;
442
562
let tlv_stream = offer. as_tlv_stream ( ) ;
443
563
let mut buffer = Vec :: new ( ) ;
444
- offer. contents . write ( & mut buffer) . unwrap ( ) ;
564
+ offer. write ( & mut buffer) . unwrap ( ) ;
445
565
446
566
assert_eq ! ( offer. bytes, buffer. as_slice( ) ) ;
447
567
assert_eq ! ( offer. chains( ) , vec![ ChainHash :: using_genesis_block( Network :: Bitcoin ) ] ) ;
@@ -468,6 +588,10 @@ mod tests {
468
588
assert_eq ! ( tlv_stream. issuer, None ) ;
469
589
assert_eq ! ( tlv_stream. quantity_max, None ) ;
470
590
assert_eq ! ( tlv_stream. node_id, Some ( & pubkey( 42 ) ) ) ;
591
+
592
+ if let Err ( e) = Offer :: try_from ( buffer) {
593
+ panic ! ( "error parsing offer: {:?}" , e) ;
594
+ }
471
595
}
472
596
473
597
#[ test]
@@ -534,16 +658,14 @@ mod tests {
534
658
assert_eq ! ( tlv_stream. amount, Some ( 1000 ) ) ;
535
659
assert_eq ! ( tlv_stream. currency, None ) ;
536
660
537
- let builder = OfferBuilder :: new ( "foo" . into ( ) , pubkey ( 42 ) )
538
- . amount ( currency_amount. clone ( ) ) ;
539
- let tlv_stream = builder. offer . as_tlv_stream ( ) ;
540
- assert_eq ! ( builder. offer. amount. as_ref( ) , Some ( & currency_amount) ) ;
661
+ let offer = OfferBuilder :: new ( "foo" . into ( ) , pubkey ( 42 ) )
662
+ . amount ( currency_amount. clone ( ) )
663
+ . build ( )
664
+ . unwrap ( ) ;
665
+ let tlv_stream = offer. as_tlv_stream ( ) ;
666
+ assert_eq ! ( offer. amount( ) , Some ( & currency_amount) ) ;
541
667
assert_eq ! ( tlv_stream. amount, Some ( 10 ) ) ;
542
668
assert_eq ! ( tlv_stream. currency, Some ( b"USD" ) ) ;
543
- match builder. build ( ) {
544
- Ok ( _) => panic ! ( "expected error" ) ,
545
- Err ( e) => assert_eq ! ( e, ( ) ) ,
546
- }
547
669
548
670
let offer = OfferBuilder :: new ( "foo" . into ( ) , pubkey ( 42 ) )
549
671
. amount ( currency_amount. clone ( ) )
@@ -694,3 +816,88 @@ mod tests {
694
816
assert_eq ! ( tlv_stream. quantity_max, None ) ;
695
817
}
696
818
}
819
+
820
+ #[ cfg( test) ]
821
+ mod bolt12_tests {
822
+ use super :: { Offer , ParseError } ;
823
+ use bitcoin:: bech32;
824
+ use crate :: ln:: msgs:: DecodeError ;
825
+
826
+ // TODO: Remove once test vectors are updated.
827
+ #[ ignore]
828
+ #[ test]
829
+ fn encodes_offer_as_bech32_without_checksum ( ) {
830
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
831
+ let offer = dbg ! ( encoded_offer. parse:: <Offer >( ) . unwrap( ) ) ;
832
+ let reencoded_offer = offer. to_string ( ) ;
833
+ dbg ! ( reencoded_offer. parse:: <Offer >( ) . unwrap( ) ) ;
834
+ assert_eq ! ( reencoded_offer, encoded_offer) ;
835
+ }
836
+
837
+ // TODO: Remove once test vectors are updated.
838
+ #[ ignore]
839
+ #[ test]
840
+ fn parses_bech32_encoded_offers ( ) {
841
+ let offers = [
842
+ // BOLT 12 test vectors
843
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
844
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
845
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
846
+ "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y" ,
847
+ "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\n sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r \n astpwuh73k29qs+\r y" ,
848
+ // Two blinded paths
849
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
850
+ ] ;
851
+ for encoded_offer in & offers {
852
+ if let Err ( e) = encoded_offer. parse :: < Offer > ( ) {
853
+ panic ! ( "Invalid offer ({:?}): {}" , e, encoded_offer) ;
854
+ }
855
+ }
856
+ }
857
+
858
+ #[ test]
859
+ fn fails_parsing_bech32_encoded_offers_with_invalid_continuations ( ) {
860
+ let offers = [
861
+ // BOLT 12 test vectors
862
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+" ,
863
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ " ,
864
+ "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
865
+ "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
866
+ "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
867
+ ] ;
868
+ for encoded_offer in & offers {
869
+ match encoded_offer. parse :: < Offer > ( ) {
870
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
871
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidContinuation ) ,
872
+ }
873
+ }
874
+
875
+ }
876
+
877
+ #[ test]
878
+ fn fails_parsing_bech32_encoded_offer_with_invalid_hrp ( ) {
879
+ let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
880
+ match encoded_offer. parse :: < Offer > ( ) {
881
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
882
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidBech32Hrp ) ,
883
+ }
884
+ }
885
+
886
+ #[ test]
887
+ fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data ( ) {
888
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso" ;
889
+ match encoded_offer. parse :: < Offer > ( ) {
890
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
891
+ Err ( e) => assert_eq ! ( e, ParseError :: Bech32 ( bech32:: Error :: InvalidChar ( 'o' ) ) ) ,
892
+ }
893
+ }
894
+
895
+ #[ test]
896
+ fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data ( ) {
897
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq" ;
898
+ match encoded_offer. parse :: < Offer > ( ) {
899
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
900
+ Err ( e) => assert_eq ! ( e, ParseError :: Decode ( DecodeError :: InvalidValue ) ) ,
901
+ }
902
+ }
903
+ }
0 commit comments