//! .issuer("Foo Bar".to_string())
//! .path(create_blinded_path())
//! .path(create_another_blinded_path())
-//! .build()
-//! .unwrap();
+//! .build()?;
//!
//! // Encode as a bech32 string for use in a QR code.
//! let encoded_offer = offer.to_string();
use crate::io;
use crate::ln::features::OfferFeatures;
use crate::ln::msgs::MAX_VALUE_MSAT;
-use crate::offers::parse::{Bech32Encode, ParseError, SemanticError};
+use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
use crate::onion_message::BlindedPath;
-use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer};
+use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
use crate::util::string::PrintableString;
use crate::prelude::*;
self
}
- /// Sets the quantity of items for [`Offer::supported_quantity`].
+ /// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
+ /// [`Quantity::one`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
}
/// Builds an [`Offer`] from the builder's settings.
- pub fn build(mut self) -> Result<Offer, ()> {
+ pub fn build(mut self) -> Result<Offer, SemanticError> {
match self.offer.amount {
Some(Amount::Bitcoin { amount_msats }) => {
if amount_msats > MAX_VALUE_MSAT {
- return Err(());
+ return Err(SemanticError::InvalidAmount);
}
},
- Some(Amount::Currency { .. }) => unreachable!(),
+ Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
None => {},
}
}
}
-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)]
}
impl Quantity {
- fn one() -> Self {
+ /// The default quantity of one.
+ pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}
}
}
-tlv_stream!(OfferTlvStream, OfferTlvStreamRef, {
+tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
(2, chains: (Vec<ChainHash>, WithoutLength)),
(4, metadata: (Vec<u8>, WithoutLength)),
(6, currency: CurrencyCode),
const BECH32_HRP: &'static str = "lno";
}
-type ParsedOffer = (Vec<u8>, OfferTlvStream);
-
impl FromStr for Offer {
type Err = ParseError;
}
}
-impl TryFrom<ParsedOffer> for Offer {
+impl TryFrom<Vec<u8>> for Offer {
type Error = ParseError;
- fn try_from(offer: ParsedOffer) -> Result<Self, Self::Error> {
- let (bytes, tlv_stream) = offer;
+ fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+ let offer = ParsedMessage::<OfferTlvStream>::try_from(bytes)?;
+ let ParsedMessage { bytes, tlv_stream } = offer;
let contents = OfferContents::try_from(tlv_stream)?;
Ok(Offer { bytes, contents })
}
use core::num::NonZeroU64;
use core::time::Duration;
use crate::ln::features::OfferFeatures;
- use crate::ln::msgs::MAX_VALUE_MSAT;
+ use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
+ use crate::offers::parse::{ParseError, SemanticError};
use crate::onion_message::{BlindedHop, BlindedPath};
- use crate::util::ser::Writeable;
+ use crate::util::ser::{BigSize, Writeable};
use crate::util::string::PrintableString;
fn pubkey(byte: u8) -> PublicKey {
assert_eq!(builder.offer.amount, Some(currency_amount.clone()));
assert_eq!(tlv_stream.amount, Some(10));
assert_eq!(tlv_stream.currency, Some(b"USD"));
+ match builder.build() {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, SemanticError::UnsupportedCurrency),
+ }
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.amount(currency_amount.clone())
let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 };
match OfferBuilder::new("foo".into(), pubkey(42)).amount(invalid_amount).build() {
Ok(_) => panic!("expected error"),
- Err(e) => assert_eq!(e, ()),
+ Err(e) => assert_eq!(e, SemanticError::InvalidAmount),
}
}
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(tlv_stream.quantity_max, None);
}
+
+ #[test]
+ fn parses_offer_with_chains() {
+ let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ .chain(Network::Bitcoin)
+ .chain(Network::Testnet)
+ .build()
+ .unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+ }
+
+ #[test]
+ fn parses_offer_with_amount() {
+ let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ .amount(Amount::Bitcoin { amount_msats: 1000 })
+ .build()
+ .unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let mut tlv_stream = offer.as_tlv_stream();
+ tlv_stream.amount = Some(1000);
+ tlv_stream.currency = Some(b"USD");
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ if let Err(e) = Offer::try_from(encoded_offer) {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let mut tlv_stream = offer.as_tlv_stream();
+ tlv_stream.amount = None;
+ tlv_stream.currency = Some(b"USD");
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ match Offer::try_from(encoded_offer) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount)),
+ }
+
+ let mut tlv_stream = offer.as_tlv_stream();
+ tlv_stream.amount = Some(MAX_VALUE_MSAT + 1);
+ tlv_stream.currency = None;
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ match Offer::try_from(encoded_offer) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidAmount)),
+ }
+ }
+
+ #[test]
+ fn parses_offer_with_description() {
+ let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let mut tlv_stream = offer.as_tlv_stream();
+ tlv_stream.description = None;
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ match Offer::try_from(encoded_offer) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingDescription));
+ },
+ }
+ }
+
+ #[test]
+ fn parses_offer_with_paths() {
+ let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ .path(BlindedPath {
+ introduction_node_id: pubkey(40),
+ blinding_point: pubkey(41),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
+ BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] },
+ ],
+ })
+ .path(BlindedPath {
+ introduction_node_id: pubkey(40),
+ blinding_point: pubkey(41),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] },
+ BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] },
+ ],
+ })
+ .build()
+ .unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let mut builder = OfferBuilder::new("foo".into(), pubkey(42));
+ builder.offer.paths = Some(vec![]);
+
+ let offer = builder.build().unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+ }
+
+ #[test]
+ fn parses_offer_with_quantity() {
+ let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ .supported_quantity(Quantity::one())
+ .build()
+ .unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ .supported_quantity(Quantity::Unbounded)
+ .build()
+ .unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ .supported_quantity(Quantity::Bounded(NonZeroU64::new(10).unwrap()))
+ .build()
+ .unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let mut tlv_stream = offer.as_tlv_stream();
+ tlv_stream.quantity_max = Some(1);
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ match Offer::try_from(encoded_offer) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
+ },
+ }
+ }
+
+ #[test]
+ fn parses_offer_with_node_id() {
+ let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let mut builder = OfferBuilder::new("foo".into(), pubkey(42));
+ builder.offer.signing_pubkey = None;
+
+ let offer = builder.build().unwrap();
+ match offer.to_string().parse::<Offer>() {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey));
+ },
+ }
+ }
+
+ #[test]
+ fn fails_parsing_offer_with_extra_tlv_records() {
+ let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
+
+ let mut encoded_offer = Vec::new();
+ offer.write(&mut encoded_offer).unwrap();
+ BigSize(80).write(&mut encoded_offer).unwrap();
+ BigSize(32).write(&mut encoded_offer).unwrap();
+ [42u8; 32].write(&mut encoded_offer).unwrap();
+
+ match Offer::try_from(encoded_offer) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
+ }
+ }
}
#[cfg(test)]