Offer parsing from bech32 strings
authorJeffrey Czyz <jkczyz@gmail.com>
Thu, 11 Aug 2022 21:51:06 +0000 (16:51 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Fri, 18 Nov 2022 17:33:06 +0000 (11:33 -0600)
Add common bech32 parsing for BOLT 12 messages. The encoding is similar
to bech32 only without a checksum and with support for continuing
messages across multiple parts.

Messages implementing Bech32Encode are parsed into a TLV stream, which
is converted to the desired message content while performing semantic
checks. Checking after conversion allows for more elaborate checks of
data composed of multiple TLV records and for more meaningful error
messages.

The parsed bytes are also saved to allow creating messages with mirrored
data, even if TLV records are unknown.

lightning/src/offers/mod.rs
lightning/src/offers/offer.rs
lightning/src/offers/parse.rs [new file with mode: 0644]
lightning/src/util/ser_macros.rs

index 2f961a0bb6ef5259deb82e27c784abc8617a52a5..273650285c6e4b9f084d1ed5bb260dafdadd8562 100644 (file)
@@ -13,3 +13,4 @@
 //! Offers are a flexible protocol for Lightning payments.
 
 pub mod offer;
+pub mod parse;
index bf569fbc022ca35e784cdb0d57004f9fab1113d0..d941b69377cce51c262f0721a3f3da53d38fbbed 100644 (file)
 //! extern crate core;
 //! extern crate lightning;
 //!
+//! use core::convert::TryFrom;
 //! use core::num::NonZeroU64;
 //! use core::time::Duration;
 //!
 //! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
-//! use lightning::offers::offer::{OfferBuilder, Quantity};
+//! use lightning::offers::offer::{Offer, OfferBuilder, Quantity};
+//! use lightning::offers::parse::ParseError;
+//! use lightning::util::ser::{Readable, Writeable};
 //!
-//! # use bitcoin::secp256k1;
 //! # use lightning::onion_message::BlindedPath;
 //! # #[cfg(feature = "std")]
 //! # use std::time::SystemTime;
@@ -33,9 +35,9 @@
 //! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() }
 //! #
 //! # #[cfg(feature = "std")]
-//! # fn build() -> Result<(), secp256k1::Error> {
+//! # fn build() -> Result<(), ParseError> {
 //! let secp_ctx = Secp256k1::new();
-//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?);
+//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
 //! let pubkey = PublicKey::from(keys);
 //!
 //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
 //!     .path(create_another_blinded_path())
 //!     .build()
 //!     .unwrap();
+//!
+//! // Encode as a bech32 string for use in a QR code.
+//! let encoded_offer = offer.to_string();
+//!
+//! // Parse from a bech32 string after scanning from a QR code.
+//! let offer = encoded_offer.parse::<Offer>()?;
+//!
+//! // Encode offer as raw bytes.
+//! let mut bytes = Vec::new();
+//! offer.write(&mut bytes).unwrap();
+//!
+//! // Decode raw bytes into an offer.
+//! let offer = Offer::try_from(bytes)?;
 //! # Ok(())
 //! # }
 //! ```
 use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::network::constants::Network;
 use bitcoin::secp256k1::PublicKey;
+use core::convert::TryFrom;
 use core::num::NonZeroU64;
+use core::str::FromStr;
 use core::time::Duration;
 use crate::io;
 use crate::ln::features::OfferFeatures;
 use crate::ln::msgs::MAX_VALUE_MSAT;
+use crate::offers::parse::{Bech32Encode, ParseError, SemanticError};
 use crate::onion_message::BlindedPath;
-use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
+use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
 
 use crate::prelude::*;
@@ -321,6 +339,12 @@ impl Offer {
        }
 }
 
+impl AsRef<[u8]> for Offer {
+       fn as_ref(&self) -> &[u8] {
+               &self.bytes
+       }
+}
+
 impl OfferContents {
        pub fn implied_chain(&self) -> ChainHash {
                ChainHash::using_genesis_block(Network::Bitcoin)
@@ -359,12 +383,27 @@ impl OfferContents {
        }
 }
 
+impl Writeable for Offer {
+       fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+               WithoutLength(&self.bytes).write(writer)
+       }
+}
+
 impl Writeable for OfferContents {
        fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
                self.as_tlv_stream().write(writer)
        }
 }
 
+impl TryFrom<Vec<u8>> for Offer {
+       type Error = ParseError;
+
+       fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+               let tlv_stream: OfferTlvStream = Readable::read(&mut &bytes[..])?;
+               Offer::try_from((bytes, tlv_stream))
+       }
+}
+
 /// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or
 /// another currency.
 #[derive(Clone, Debug, PartialEq)]
@@ -425,13 +464,91 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, {
        (22, node_id: PublicKey),
 });
 
+impl Bech32Encode for Offer {
+       const BECH32_HRP: &'static str = "lno";
+}
+
+type ParsedOffer = (Vec<u8>, OfferTlvStream);
+
+impl FromStr for Offer {
+       type Err = ParseError;
+
+       fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
+               Self::from_bech32_str(s)
+       }
+}
+
+impl TryFrom<ParsedOffer> for Offer {
+       type Error = ParseError;
+
+       fn try_from(offer: ParsedOffer) -> Result<Self, Self::Error> {
+               let (bytes, tlv_stream) = offer;
+               let contents = OfferContents::try_from(tlv_stream)?;
+               Ok(Offer { bytes, contents })
+       }
+}
+
+impl TryFrom<OfferTlvStream> for OfferContents {
+       type Error = SemanticError;
+
+       fn try_from(tlv_stream: OfferTlvStream) -> Result<Self, Self::Error> {
+               let OfferTlvStream {
+                       chains, metadata, currency, amount, description, features, absolute_expiry, paths,
+                       issuer, quantity_max, node_id,
+               } = tlv_stream;
+
+               let amount = match (currency, amount) {
+                       (None, None) => None,
+                       (None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => {
+                               return Err(SemanticError::InvalidAmount);
+                       },
+                       (None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }),
+                       (Some(_), None) => return Err(SemanticError::MissingAmount),
+                       (Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }),
+               };
+
+               let description = match description {
+                       None => return Err(SemanticError::MissingDescription),
+                       Some(description) => description,
+               };
+
+               let features = features.unwrap_or_else(OfferFeatures::empty);
+
+               let absolute_expiry = absolute_expiry
+                       .map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));
+
+               let supported_quantity = match quantity_max {
+                       None => Quantity::one(),
+                       Some(0) => Quantity::Unbounded,
+                       Some(1) => return Err(SemanticError::InvalidQuantity),
+                       Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
+               };
+
+               if node_id.is_none() {
+                       return Err(SemanticError::MissingSigningPubkey);
+               }
+
+               Ok(OfferContents {
+                       chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
+                       supported_quantity, signing_pubkey: node_id,
+               })
+       }
+}
+
+impl core::fmt::Display for Offer {
+       fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+               self.fmt_bech32_str(f)
+       }
+}
+
 #[cfg(test)]
 mod tests {
-       use super::{Amount, OfferBuilder, Quantity};
+       use super::{Amount, Offer, OfferBuilder, Quantity};
 
        use bitcoin::blockdata::constants::ChainHash;
        use bitcoin::network::constants::Network;
        use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
+       use core::convert::TryFrom;
        use core::num::NonZeroU64;
        use core::time::Duration;
        use crate::ln::features::OfferFeatures;
@@ -454,7 +571,7 @@ mod tests {
                let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
                let tlv_stream = offer.as_tlv_stream();
                let mut buffer = Vec::new();
-               offer.contents.write(&mut buffer).unwrap();
+               offer.write(&mut buffer).unwrap();
 
                assert_eq!(offer.bytes, buffer.as_slice());
                assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]);
@@ -481,6 +598,10 @@ mod tests {
                assert_eq!(tlv_stream.issuer, None);
                assert_eq!(tlv_stream.quantity_max, None);
                assert_eq!(tlv_stream.node_id, Some(&pubkey(42)));
+
+               if let Err(e) = Offer::try_from(buffer) {
+                       panic!("error parsing offer: {:?}", e);
+               }
        }
 
        #[test]
@@ -707,3 +828,88 @@ mod tests {
                assert_eq!(tlv_stream.quantity_max, None);
        }
 }
+
+#[cfg(test)]
+mod bech32_tests {
+       use super::{Offer, ParseError};
+       use bitcoin::bech32;
+       use crate::ln::msgs::DecodeError;
+
+       // TODO: Remove once test vectors are updated.
+       #[ignore]
+       #[test]
+       fn encodes_offer_as_bech32_without_checksum() {
+               let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
+               let offer = dbg!(encoded_offer.parse::<Offer>().unwrap());
+               let reencoded_offer = offer.to_string();
+               dbg!(reencoded_offer.parse::<Offer>().unwrap());
+               assert_eq!(reencoded_offer, encoded_offer);
+       }
+
+       // TODO: Remove once test vectors are updated.
+       #[ignore]
+       #[test]
+       fn parses_bech32_encoded_offers() {
+               let offers = [
+                       // BOLT 12 test vectors
+                       "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
+                       "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
+                       "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
+                       "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y",
+                       "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+  0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\nsqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r\nastpwuh73k29qs+\r  y",
+                       // Two blinded paths
+                       "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
+               ];
+               for encoded_offer in &offers {
+                       if let Err(e) = encoded_offer.parse::<Offer>() {
+                               panic!("Invalid offer ({:?}): {}", e, encoded_offer);
+                       }
+               }
+       }
+
+       #[test]
+       fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() {
+               let offers = [
+                       // BOLT 12 test vectors
+                       "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+",
+                       "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ ",
+                       "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
+                       "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
+                       "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
+               ];
+               for encoded_offer in &offers {
+                       match encoded_offer.parse::<Offer>() {
+                               Ok(_) => panic!("Valid offer: {}", encoded_offer),
+                               Err(e) => assert_eq!(e, ParseError::InvalidContinuation),
+                       }
+               }
+
+       }
+
+       #[test]
+       fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() {
+               let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
+               match encoded_offer.parse::<Offer>() {
+                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
+                       Err(e) => assert_eq!(e, ParseError::InvalidBech32Hrp),
+               }
+       }
+
+       #[test]
+       fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() {
+               let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso";
+               match encoded_offer.parse::<Offer>() {
+                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
+                       Err(e) => assert_eq!(e, ParseError::Bech32(bech32::Error::InvalidChar('o'))),
+               }
+       }
+
+       #[test]
+       fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() {
+               let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq";
+               match encoded_offer.parse::<Offer>() {
+                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
+                       Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
+               }
+       }
+}
diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs
new file mode 100644 (file)
index 0000000..c9d568a
--- /dev/null
@@ -0,0 +1,125 @@
+// This file is Copyright its original authors, visible in version control
+// history.
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Parsing and formatting for bech32 message encoding.
+
+use bitcoin::bech32;
+use bitcoin::bech32::{FromBase32, ToBase32};
+use core::convert::TryFrom;
+use core::fmt;
+use crate::ln::msgs::DecodeError;
+
+use crate::prelude::*;
+
+/// Indicates a message can be encoded using bech32.
+pub(crate) trait Bech32Encode: AsRef<[u8]> + TryFrom<Vec<u8>, Error=ParseError> {
+       /// Human readable part of the message's bech32 encoding.
+       const BECH32_HRP: &'static str;
+
+       /// Parses a bech32-encoded message into a TLV stream.
+       fn from_bech32_str(s: &str) -> Result<Self, ParseError> {
+               // Offer encoding may be split by '+' followed by optional whitespace.
+               let encoded = match s.split('+').skip(1).next() {
+                       Some(_) => {
+                               for chunk in s.split('+') {
+                                       let chunk = chunk.trim_start();
+                                       if chunk.is_empty() || chunk.contains(char::is_whitespace) {
+                                               return Err(ParseError::InvalidContinuation);
+                                       }
+                               }
+
+                               let s = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect::<String>();
+                               Bech32String::Owned(s)
+                       },
+                       None => Bech32String::Borrowed(s),
+               };
+
+               let (hrp, data) = bech32::decode_without_checksum(encoded.as_ref())?;
+
+               if hrp != Self::BECH32_HRP {
+                       return Err(ParseError::InvalidBech32Hrp);
+               }
+
+               let data = Vec::<u8>::from_base32(&data)?;
+               Self::try_from(data)
+       }
+
+       /// Formats the message using bech32-encoding.
+       fn fmt_bech32_str(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+               bech32::encode_without_checksum_to_fmt(f, Self::BECH32_HRP, self.as_ref().to_base32())
+                       .expect("HRP is invalid").unwrap();
+
+               Ok(())
+       }
+}
+
+// Used to avoid copying a bech32 string not containing the continuation character (+).
+enum Bech32String<'a> {
+       Borrowed(&'a str),
+       Owned(String),
+}
+
+impl<'a> AsRef<str> for Bech32String<'a> {
+       fn as_ref(&self) -> &str {
+               match self {
+                       Bech32String::Borrowed(s) => s,
+                       Bech32String::Owned(s) => s,
+               }
+       }
+}
+
+/// Error when parsing a bech32 encoded message using [`str::parse`].
+#[derive(Debug, PartialEq)]
+pub enum ParseError {
+       /// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages
+       /// across multiple parts (i.e., '+' followed by whitespace).
+       InvalidContinuation,
+       /// The bech32 encoding's human-readable part does not match what was expected for the message
+       /// being parsed.
+       InvalidBech32Hrp,
+       /// The string could not be bech32 decoded.
+       Bech32(bech32::Error),
+       /// The bech32 decoded string could not be decoded as the expected message type.
+       Decode(DecodeError),
+       /// The parsed message has invalid semantics.
+       InvalidSemantics(SemanticError),
+}
+
+/// Error when interpreting a TLV stream as a specific type.
+#[derive(Debug, PartialEq)]
+pub enum SemanticError {
+       /// An amount was expected but was missing.
+       MissingAmount,
+       /// The amount exceeded the total bitcoin supply.
+       InvalidAmount,
+       /// A required description was not provided.
+       MissingDescription,
+       /// A signing pubkey was not provided.
+       MissingSigningPubkey,
+       /// An unsupported quantity was provided.
+       InvalidQuantity,
+}
+
+impl From<bech32::Error> for ParseError {
+       fn from(error: bech32::Error) -> Self {
+               Self::Bech32(error)
+       }
+}
+
+impl From<DecodeError> for ParseError {
+       fn from(error: DecodeError) -> Self {
+               Self::Decode(error)
+       }
+}
+
+impl From<SemanticError> for ParseError {
+       fn from(error: SemanticError) -> Self {
+               Self::InvalidSemantics(error)
+       }
+}
index 3ec0848680ffe62a2532e03d47d1fc5b62d7f4b8..129fb8f407b088de53aa7a94439d68fb478ed0c4 100644 (file)
@@ -476,7 +476,7 @@ macro_rules! tlv_stream {
                $(($type:expr, $field:ident : $fieldty:tt)),* $(,)*
        }) => {
                #[derive(Debug)]
-               struct $name {
+               pub(crate) struct $name {
                        $(
                                $field: Option<tlv_record_type!($fieldty)>,
                        )*