|
22 | 22 | //! use core::time::Duration;
|
23 | 23 | //!
|
24 | 24 | //! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
|
25 |
| -//! use lightning::offers::offer::{Amount, OfferBuilder}; |
| 25 | +//! use lightning::offers::offer::{Amount, Offer, OfferBuilder}; |
| 26 | +//! use lightning::offers::parse::ParseError; |
26 | 27 | //!
|
27 |
| -//! # use bitcoin::secp256k1; |
28 | 28 | //! # use lightning::onion_message::BlindedPath;
|
29 | 29 | //! # #[cfg(feature = "std")]
|
30 | 30 | //! # use std::time::SystemTime;
|
|
33 | 33 | //! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() }
|
34 | 34 | //! #
|
35 | 35 | //! # #[cfg(feature = "std")]
|
36 |
| -//! # fn build() -> Result<(), secp256k1::Error> { |
| 36 | +//! # fn build() -> Result<(), ParseError> { |
37 | 37 | //! let secp_ctx = Secp256k1::new();
|
38 |
| -//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); |
| 38 | +//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); |
39 | 39 | //! let pubkey = PublicKey::from(keys);
|
40 | 40 | //!
|
41 | 41 | //! let one_item = NonZeroU64::new(1).unwrap();
|
|
49 | 49 | //! .path(create_another_blinded_path())
|
50 | 50 | //! .build()
|
51 | 51 | //! .unwrap();
|
| 52 | +//! |
| 53 | +//! // Encode as a bech32 string for use in a QR code. |
| 54 | +//! let encoded_offer = offer.to_string(); |
| 55 | +//! |
| 56 | +//! // Parse from a bech32 string after scanning from a QR code. |
| 57 | +//! let offer = encoded_offer.parse::<Offer>()?; |
52 | 58 | //! # Ok(())
|
53 | 59 | //! # }
|
54 | 60 | //! ```
|
55 | 61 |
|
56 | 62 | use bitcoin::blockdata::constants::ChainHash;
|
57 | 63 | use bitcoin::network::constants::Network;
|
58 | 64 | use bitcoin::secp256k1::PublicKey;
|
| 65 | +use core::convert::TryFrom; |
59 | 66 | use core::num::NonZeroU64;
|
60 | 67 | use core::ops::{Bound, RangeBounds};
|
| 68 | +use core::str::FromStr; |
61 | 69 | use core::time::Duration;
|
62 | 70 | use io;
|
63 | 71 | use ln::features::OfferFeatures;
|
64 | 72 | use ln::msgs::MAX_VALUE_MSAT;
|
| 73 | +use offers::parse::{Bech32Encode, ParseError, SemanticError}; |
65 | 74 | use onion_message::BlindedPath;
|
66 | 75 | use util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
|
67 | 76 | use util::string::PrintableString;
|
@@ -354,6 +363,12 @@ impl Offer {
|
354 | 363 | }
|
355 | 364 | }
|
356 | 365 |
|
| 366 | +impl AsRef<[u8]> for Offer { |
| 367 | + fn as_ref(&self) -> &[u8] { |
| 368 | + &self.bytes |
| 369 | + } |
| 370 | +} |
| 371 | + |
357 | 372 | impl OfferContents {
|
358 | 373 | pub fn amount_msats(&self) -> u64 {
|
359 | 374 | self.amount.as_ref().map(Amount::as_msats).unwrap_or(0)
|
@@ -450,6 +465,103 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, {
|
450 | 465 | (24, node_id: PublicKey),
|
451 | 466 | });
|
452 | 467 |
|
| 468 | +impl Bech32Encode for Offer { |
| 469 | + type TlvStream = OfferTlvStream; |
| 470 | + |
| 471 | + const BECH32_HRP: &'static str = "lno"; |
| 472 | +} |
| 473 | + |
| 474 | +impl FromStr for Offer { |
| 475 | + type Err = ParseError; |
| 476 | + |
| 477 | + fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> { |
| 478 | + let (tlv_stream, bytes) = Offer::from_bech32_str(s)?; |
| 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_min, quantity_max, node_id, |
| 491 | + } = tlv_stream; |
| 492 | + |
| 493 | + let supported_chains = [ |
| 494 | + ChainHash::using_genesis_block(Network::Bitcoin), |
| 495 | + ChainHash::using_genesis_block(Network::Testnet), |
| 496 | + ChainHash::using_genesis_block(Network::Signet), |
| 497 | + ChainHash::using_genesis_block(Network::Regtest), |
| 498 | + ]; |
| 499 | + let chains = match chains { |
| 500 | + None => None, |
| 501 | + Some(chains) => match chains.first() { |
| 502 | + None => Some(chains), |
| 503 | + Some(chain) if supported_chains.contains(chain) => Some(chains), |
| 504 | + _ => return Err(SemanticError::UnsupportedChain), |
| 505 | + }, |
| 506 | + }; |
| 507 | + |
| 508 | + let amount = match (currency, amount) { |
| 509 | + (None, None) => None, |
| 510 | + (None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }), |
| 511 | + (Some(_), None) => return Err(SemanticError::MissingAmount), |
| 512 | + (Some(_), Some(_)) => return Err(SemanticError::UnsupportedCurrency), |
| 513 | + }; |
| 514 | + |
| 515 | + let description = match description { |
| 516 | + None => return Err(SemanticError::MissingDescription), |
| 517 | + Some(description) => description, |
| 518 | + }; |
| 519 | + |
| 520 | + let features = features.unwrap_or_else(OfferFeatures::empty); |
| 521 | + |
| 522 | + let absolute_expiry = absolute_expiry |
| 523 | + .map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch)); |
| 524 | + |
| 525 | + let paths = match paths { |
| 526 | + Some(paths) if paths.is_empty() => return Err(SemanticError::MissingPaths), |
| 527 | + paths => paths, |
| 528 | + }; |
| 529 | + |
| 530 | + if let Some(quantity_min) = quantity_min { |
| 531 | + if quantity_min < 1 { |
| 532 | + return Err(SemanticError::InvalidQuantity); |
| 533 | + } |
| 534 | + |
| 535 | + if let Some(quantity_max) = quantity_max { |
| 536 | + if quantity_min > quantity_max { |
| 537 | + return Err(SemanticError::InvalidQuantity); |
| 538 | + } |
| 539 | + } |
| 540 | + } |
| 541 | + |
| 542 | + if let Some(quantity_max) = quantity_max { |
| 543 | + if quantity_max < 1 { |
| 544 | + return Err(SemanticError::InvalidQuantity); |
| 545 | + } |
| 546 | + } |
| 547 | + |
| 548 | + if node_id.is_none() { |
| 549 | + return Err(SemanticError::MissingNodeId); |
| 550 | + } |
| 551 | + |
| 552 | + Ok(OfferContents { |
| 553 | + chains, metadata, amount, description, features, absolute_expiry, issuer, paths, |
| 554 | + quantity_min, quantity_max, signing_pubkey: node_id, |
| 555 | + }) |
| 556 | + } |
| 557 | +} |
| 558 | + |
| 559 | +impl core::fmt::Display for Offer { |
| 560 | + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { |
| 561 | + self.fmt_bech32_str(f) |
| 562 | + } |
| 563 | +} |
| 564 | + |
453 | 565 | #[cfg(test)]
|
454 | 566 | mod tests {
|
455 | 567 | use super::{Amount, OfferBuilder};
|
@@ -830,3 +942,88 @@ mod tests {
|
830 | 942 | );
|
831 | 943 | }
|
832 | 944 | }
|
| 945 | + |
| 946 | +#[cfg(test)] |
| 947 | +mod bolt12_tests { |
| 948 | + use super::{Offer, ParseError}; |
| 949 | + use bitcoin::bech32; |
| 950 | + use ln::msgs::DecodeError; |
| 951 | + |
| 952 | + // TODO: Remove once test vectors are updated. |
| 953 | + #[ignore] |
| 954 | + #[test] |
| 955 | + fn encodes_offer_as_bech32_without_checksum() { |
| 956 | + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy"; |
| 957 | + let offer = dbg!(encoded_offer.parse::<Offer>().unwrap()); |
| 958 | + let reencoded_offer = offer.to_string(); |
| 959 | + dbg!(reencoded_offer.parse::<Offer>().unwrap()); |
| 960 | + assert_eq!(reencoded_offer, encoded_offer); |
| 961 | + } |
| 962 | + |
| 963 | + // TODO: Remove once test vectors are updated. |
| 964 | + #[ignore] |
| 965 | + #[test] |
| 966 | + fn parses_bech32_encoded_offers() { |
| 967 | + let offers = [ |
| 968 | + // BOLT 12 test vectors |
| 969 | + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", |
| 970 | + "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", |
| 971 | + "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", |
| 972 | + "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y", |
| 973 | + "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\nsqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r\nastpwuh73k29qs+\r y", |
| 974 | + // Two blinded paths |
| 975 | + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", |
| 976 | + ]; |
| 977 | + for encoded_offer in &offers { |
| 978 | + if let Err(e) = encoded_offer.parse::<Offer>() { |
| 979 | + panic!("Invalid offer ({:?}): {}", e, encoded_offer); |
| 980 | + } |
| 981 | + } |
| 982 | + } |
| 983 | + |
| 984 | + #[test] |
| 985 | + fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() { |
| 986 | + let offers = [ |
| 987 | + // BOLT 12 test vectors |
| 988 | + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+", |
| 989 | + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ ", |
| 990 | + "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", |
| 991 | + "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", |
| 992 | + "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", |
| 993 | + ]; |
| 994 | + for encoded_offer in &offers { |
| 995 | + match encoded_offer.parse::<Offer>() { |
| 996 | + Ok(_) => panic!("Valid offer: {}", encoded_offer), |
| 997 | + Err(e) => assert_eq!(e, ParseError::InvalidContinuation), |
| 998 | + } |
| 999 | + } |
| 1000 | + |
| 1001 | + } |
| 1002 | + |
| 1003 | + #[test] |
| 1004 | + fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() { |
| 1005 | + let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy"; |
| 1006 | + match encoded_offer.parse::<Offer>() { |
| 1007 | + Ok(_) => panic!("Valid offer: {}", encoded_offer), |
| 1008 | + Err(e) => assert_eq!(e, ParseError::InvalidBech32Hrp), |
| 1009 | + } |
| 1010 | + } |
| 1011 | + |
| 1012 | + #[test] |
| 1013 | + fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() { |
| 1014 | + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso"; |
| 1015 | + match encoded_offer.parse::<Offer>() { |
| 1016 | + Ok(_) => panic!("Valid offer: {}", encoded_offer), |
| 1017 | + Err(e) => assert_eq!(e, ParseError::Bech32(bech32::Error::InvalidChar('o'))), |
| 1018 | + } |
| 1019 | + } |
| 1020 | + |
| 1021 | + #[test] |
| 1022 | + fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() { |
| 1023 | + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq"; |
| 1024 | + match encoded_offer.parse::<Offer>() { |
| 1025 | + Ok(_) => panic!("Valid offer: {}", encoded_offer), |
| 1026 | + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), |
| 1027 | + } |
| 1028 | + } |
| 1029 | +} |
0 commit comments