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