//! let pubkey = PublicKey::from(keys);
//!
//! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
-//! let offer = OfferBuilder::new("coffee, large".to_string(), pubkey)
+//! let offer = OfferBuilder::new(pubkey)
+//! .description("coffee, large".to_string())
//! .amount_msats(20_000)
//! .supported_quantity(Quantity::Unbounded)
//! .absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap())
use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self};
-use core::convert::TryFrom;
use core::hash::{Hash, Hasher};
use core::num::NonZeroU64;
use core::ops::Deref;
use crate::ln::channelmanager::PaymentId;
use crate::ln::features::OfferFeatures;
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
-use crate::ln::msgs::MAX_VALUE_MSAT;
-use crate::offers::merkle::TlvStream;
+use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
+use crate::offers::merkle::{TaggedHash, TlvStream};
use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
use crate::offers::signer::{Metadata, MetadataMaterial, self};
-use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
+use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer};
use crate::util::string::PrintableString;
#[cfg(not(c_bindings))]
crate::offers::invoice_request::{InvoiceRequestWithDerivedPayerIdBuilder, InvoiceRequestWithExplicitPayerIdBuilder},
};
+#[allow(unused_imports)]
use crate::prelude::*;
#[cfg(feature = "std")]
pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~";
+/// An identifier for an [`Offer`] built using [`DerivedMetadata`].
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct OfferId(pub [u8; 32]);
+
+impl OfferId {
+ const ID_TAG: &'static str = "LDK Offer ID";
+
+ fn from_valid_offer_tlv_stream(bytes: &[u8]) -> Self {
+ let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(Self::ID_TAG, bytes);
+ Self(tagged_hash.to_bytes())
+ }
+
+ fn from_valid_invreq_tlv_stream(bytes: &[u8]) -> Self {
+ let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES);
+ let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream);
+ Self(tagged_hash.to_bytes())
+ }
+}
+
+impl Writeable for OfferId {
+ fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
+ self.0.write(w)
+ }
+}
+
+impl Readable for OfferId {
+ fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
+ Ok(OfferId(Readable::read(r)?))
+ }
+}
+
/// Builds an [`Offer`] for the "offer to be paid" flow.
///
/// See [module-level documentation] for usage.
///
/// See [module-level documentation] for usage.
///
-/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
-///
/// [module-level documentation]: self
#[cfg(c_bindings)]
+#[derive(Clone)]
pub struct OfferWithExplicitMetadataBuilder<'a> {
offer: OfferContents,
metadata_strategy: core::marker::PhantomData<ExplicitMetadata>,
///
/// See [module-level documentation] for usage.
///
-/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
-///
/// [module-level documentation]: self
#[cfg(c_bindings)]
+#[derive(Clone)]
pub struct OfferWithDerivedMetadataBuilder<'a> {
offer: OfferContents,
metadata_strategy: core::marker::PhantomData<DerivedMetadata>,
macro_rules! offer_explicit_metadata_builder_methods { (
$self: ident, $self_type: ty, $return_type: ty, $return_value: expr
) => {
- /// Creates a new builder for an offer setting the [`Offer::description`] and using the
- /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered
- /// while the offer is valid.
+ /// Creates a new builder for an offer using the [`Offer::signing_pubkey`] for signing invoices.
+ /// The associated secret key must be remembered while the offer is valid.
///
/// Use a different pubkey per offer to avoid correlating offers.
///
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder
- pub fn new(description: String, signing_pubkey: PublicKey) -> Self {
+ pub fn new(signing_pubkey: PublicKey) -> Self {
Self {
offer: OfferContents {
- chains: None, metadata: None, amount: None, description,
+ chains: None, metadata: None, amount: None, description: None,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
- supported_quantity: Quantity::One, signing_pubkey,
+ supported_quantity: Quantity::One, signing_pubkey: Some(signing_pubkey),
},
metadata_strategy: core::marker::PhantomData,
secp_ctx: None,
/// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
pub fn deriving_signing_pubkey<ES: Deref>(
- description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
+ node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
secp_ctx: &'a Secp256k1<$secp_context>
) -> Self where ES::Target: EntropySource {
let nonce = Nonce::from_entropy_source(entropy_source);
let metadata = Metadata::DerivedSigningPubkey(derivation_material);
Self {
offer: OfferContents {
- chains: None, metadata: Some(metadata), amount: None, description,
+ chains: None, metadata: Some(metadata), amount: None, description: None,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
- supported_quantity: Quantity::One, signing_pubkey: node_id,
+ supported_quantity: Quantity::One, signing_pubkey: Some(node_id),
},
metadata_strategy: core::marker::PhantomData,
secp_ctx: Some(secp_ctx),
$return_value
}
+ /// Sets the [`Offer::description`].
+ ///
+ /// Successive calls to this method will override the previous setting.
+ pub fn description($($self_mut)* $self: $self_type, description: String) -> $return_type {
+ $self.offer.description = Some(description);
+ $return_value
+ }
+
/// Sets the [`Offer::issuer`].
///
/// Successive calls to this method will override the previous setting.
None => {},
}
+ if $self.offer.amount.is_some() && $self.offer.description.is_none() {
+ $self.offer.description = Some(String::new());
+ }
+
if let Some(chains) = &$self.offer.chains {
if chains.len() == 1 && chains[0] == $self.offer.implied_chain() {
$self.offer.chains = None;
let (derived_metadata, keys) = metadata.derive_from(tlv_stream, $self.secp_ctx);
metadata = derived_metadata;
if let Some(keys) = keys {
- $self.offer.signing_pubkey = keys.public_key();
+ $self.offer.signing_pubkey = Some(keys.public_key());
}
}
let mut bytes = Vec::new();
$self.offer.write(&mut bytes).unwrap();
+ let id = OfferId::from_valid_offer_tlv_stream(&bytes);
+
Offer {
bytes,
#[cfg(not(c_bindings))]
contents: $self.offer,
#[cfg(c_bindings)]
- contents: $self.offer.clone()
+ contents: $self.offer.clone(),
+ id,
}
}
} }
$return_value
}
+ #[cfg_attr(c_bindings, allow(dead_code))]
+ pub(crate) fn clear_signing_pubkey($($self_mut)* $self: $self_type) -> $return_type {
+ $self.offer.signing_pubkey = None;
+ $return_value
+ }
+
#[cfg_attr(c_bindings, allow(dead_code))]
pub(super) fn build_unchecked($self: $self_type) -> Offer {
$self.build_without_checks()
}
}
+#[cfg(c_bindings)]
+impl<'a> From<OfferWithDerivedMetadataBuilder<'a>>
+for OfferBuilder<'a, DerivedMetadata, secp256k1::All> {
+ fn from(builder: OfferWithDerivedMetadataBuilder<'a>) -> Self {
+ let OfferWithDerivedMetadataBuilder { offer, metadata_strategy, secp_ctx } = builder;
+
+ Self { offer, metadata_strategy, secp_ctx }
+ }
+}
+
/// An `Offer` is a potentially long-lived proposal for payment of a good or service.
///
/// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a
// fields.
pub(super) bytes: Vec<u8>,
pub(super) contents: OfferContents,
+ id: OfferId,
}
/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or a
chains: Option<Vec<ChainHash>>,
metadata: Option<Metadata>,
amount: Option<Amount>,
- description: String,
+ description: Option<String>,
features: OfferFeatures,
absolute_expiry: Option<Duration>,
issuer: Option<String>,
paths: Option<Vec<BlindedPath>>,
supported_quantity: Quantity,
- signing_pubkey: PublicKey,
+ signing_pubkey: Option<PublicKey>,
}
macro_rules! offer_accessors { ($self: ident, $contents: expr) => {
}
/// The minimum amount required for a successful payment of a single item.
- pub fn amount(&$self) -> Option<&$crate::offers::offer::Amount> {
+ pub fn amount(&$self) -> Option<$crate::offers::offer::Amount> {
$contents.amount()
}
/// A complete description of the purpose of the payment. Intended to be displayed to the user
/// but with the caveat that it has not been verified in any way.
- pub fn description(&$self) -> $crate::util::string::PrintableString {
+ pub fn description(&$self) -> Option<$crate::util::string::PrintableString> {
$contents.description()
}
}
/// The public key used by the recipient to sign invoices.
- pub fn signing_pubkey(&$self) -> bitcoin::secp256k1::PublicKey {
+ pub fn signing_pubkey(&$self) -> Option<bitcoin::secp256k1::PublicKey> {
$contents.signing_pubkey()
}
} }
impl Offer {
offer_accessors!(self, self.contents);
+ /// Returns the id of the offer.
+ pub fn id(&self) -> OfferId {
+ self.id
+ }
+
pub(super) fn implied_chain(&self) -> ChainHash {
self.contents.implied_chain()
}
self.metadata.as_ref().and_then(|metadata| metadata.as_bytes())
}
- pub fn amount(&self) -> Option<&Amount> {
- self.amount.as_ref()
+ pub fn amount(&self) -> Option<Amount> {
+ self.amount
}
- pub fn description(&self) -> PrintableString {
- PrintableString(&self.description)
+ pub fn description(&self) -> Option<PrintableString> {
+ self.description.as_ref().map(|description| PrintableString(description))
}
pub fn features(&self) -> &OfferFeatures {
}
}
- pub(super) fn signing_pubkey(&self) -> PublicKey {
+ pub(super) fn signing_pubkey(&self) -> Option<PublicKey> {
self.signing_pubkey
}
/// Verifies that the offer metadata was produced from the offer in the TLV stream.
pub(super) fn verify<T: secp256k1::Signing>(
&self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1<T>
- ) -> Result<Option<KeyPair>, ()> {
+ ) -> Result<(OfferId, Option<KeyPair>), ()> {
match self.metadata() {
Some(metadata) => {
let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| {
_ => true,
}
});
- signer::verify_recipient_metadata(
- metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx
- )
+ let signing_pubkey = match self.signing_pubkey() {
+ Some(signing_pubkey) => signing_pubkey,
+ None => return Err(()),
+ };
+ let keys = signer::verify_recipient_metadata(
+ metadata, key, IV_BYTES, signing_pubkey, tlv_stream, secp_ctx
+ )?;
+
+ let offer_id = OfferId::from_valid_invreq_tlv_stream(bytes);
+
+ Ok((offer_id, keys))
},
None => Err(()),
}
metadata: self.metadata(),
currency,
amount,
- description: Some(&self.description),
+ description: self.description.as_ref(),
features,
absolute_expiry: self.absolute_expiry.map(|duration| duration.as_secs()),
paths: self.paths.as_ref(),
issuer: self.issuer.as_ref(),
quantity_max: self.supported_quantity.to_tlv_record(),
- node_id: Some(&self.signing_pubkey),
+ node_id: self.signing_pubkey.as_ref(),
}
}
}
/// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or
/// another currency.
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Amount {
/// An amount of bitcoin.
Bitcoin {
let offer = ParsedMessage::<OfferTlvStream>::try_from(bytes)?;
let ParsedMessage { bytes, tlv_stream } = offer;
let contents = OfferContents::try_from(tlv_stream)?;
- Ok(Offer { bytes, contents })
+ let id = OfferId::from_valid_offer_tlv_stream(&bytes);
+
+ Ok(Offer { bytes, contents, id })
}
}
(Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }),
};
- let description = match description {
- None => return Err(Bolt12SemanticError::MissingDescription),
- Some(description) => description,
- };
+ if amount.is_some() && description.is_none() {
+ return Err(Bolt12SemanticError::MissingDescription);
+ }
let features = features.unwrap_or_else(OfferFeatures::empty);
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};
- let signing_pubkey = match node_id {
- None => return Err(Bolt12SemanticError::MissingSigningPubkey),
- Some(node_id) => node_id,
+ let (signing_pubkey, paths) = match (node_id, paths) {
+ (None, None) => return Err(Bolt12SemanticError::MissingSigningPubkey),
+ (_, Some(paths)) if paths.is_empty() => return Err(Bolt12SemanticError::MissingPaths),
+ (node_id, paths) => (node_id, paths),
};
Ok(OfferContents {
use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
use bitcoin::secp256k1::Secp256k1;
- use core::convert::TryFrom;
use core::num::NonZeroU64;
use core::time::Duration;
- use crate::blinded_path::{BlindedHop, BlindedPath};
+ use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode};
use crate::sign::KeyMaterial;
use crate::ln::features::OfferFeatures;
use crate::ln::inbound_payment::ExpandedKey;
#[test]
fn builds_offer_with_defaults() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
+ let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
let mut buffer = Vec::new();
offer.write(&mut buffer).unwrap();
assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin)));
assert_eq!(offer.metadata(), None);
assert_eq!(offer.amount(), None);
- assert_eq!(offer.description(), PrintableString("foo"));
+ assert_eq!(offer.description(), None);
assert_eq!(offer.offer_features(), &OfferFeatures::empty());
assert_eq!(offer.absolute_expiry(), None);
#[cfg(feature = "std")]
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::One);
- assert_eq!(offer.signing_pubkey(), pubkey(42));
+ assert_eq!(offer.signing_pubkey(), Some(pubkey(42)));
assert_eq!(
offer.as_tlv_stream(),
metadata: None,
currency: None,
amount: None,
- description: Some(&String::from("foo")),
+ description: None,
features: None,
absolute_expiry: None,
paths: None,
let mainnet = ChainHash::using_genesis_block(Network::Bitcoin);
let testnet = ChainHash::using_genesis_block(Network::Testnet);
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.chain(Network::Bitcoin)
.build()
.unwrap();
assert_eq!(offer.chains(), vec![mainnet]);
assert_eq!(offer.as_tlv_stream().chains, None);
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.chain(Network::Testnet)
.build()
.unwrap();
assert_eq!(offer.chains(), vec![testnet]);
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet]));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.chain(Network::Testnet)
.chain(Network::Testnet)
.build()
assert_eq!(offer.chains(), vec![testnet]);
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet]));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.chain(Network::Bitcoin)
.chain(Network::Testnet)
.build()
#[test]
fn builds_offer_with_metadata() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.metadata(vec![42; 32]).unwrap()
.build()
.unwrap();
assert_eq!(offer.metadata(), Some(&vec![42; 32]));
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32]));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.metadata(vec![42; 32]).unwrap()
.metadata(vec![43; 32]).unwrap()
.build()
#[test]
fn builds_offer_with_metadata_derived() {
- let desc = "foo".to_string();
let node_id = recipient_pubkey();
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
let entropy = FixedEntropy {};
#[cfg(c_bindings)]
use super::OfferWithDerivedMetadataBuilder as OfferBuilder;
let offer = OfferBuilder
- ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
+ ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
.amount_msats(1000)
.build().unwrap();
- assert_eq!(offer.signing_pubkey(), node_id);
+ assert_eq!(offer.signing_pubkey(), Some(node_id));
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap();
- assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok());
+ match invoice_request.verify(&expanded_key, &secp_ctx) {
+ Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()),
+ Err(_) => panic!("unexpected error"),
+ }
// Fails verification with altered offer field
let mut tlv_stream = offer.as_tlv_stream();
#[test]
fn builds_offer_with_derived_signing_pubkey() {
- let desc = "foo".to_string();
let node_id = recipient_pubkey();
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
let entropy = FixedEntropy {};
let secp_ctx = Secp256k1::new();
let blinded_path = BlindedPath {
- introduction_node_id: pubkey(40),
+ introduction_node: IntroductionNode::NodeId(pubkey(40)),
blinding_point: pubkey(41),
blinded_hops: vec![
BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] },
#[cfg(c_bindings)]
use super::OfferWithDerivedMetadataBuilder as OfferBuilder;
let offer = OfferBuilder
- ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
+ ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
.amount_msats(1000)
.path(blinded_path)
.build().unwrap();
- assert_ne!(offer.signing_pubkey(), node_id);
+ assert_ne!(offer.signing_pubkey(), Some(node_id));
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap();
- assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok());
+ match invoice_request.verify(&expanded_key, &secp_ctx) {
+ Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()),
+ Err(_) => panic!("unexpected error"),
+ }
// Fails verification with altered offer field
let mut tlv_stream = offer.as_tlv_stream();
let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 };
let currency_amount = Amount::Currency { iso4217_code: *b"USD", amount: 10 };
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.amount_msats(1000)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
- assert_eq!(offer.amount(), Some(&bitcoin_amount));
+ assert_eq!(offer.amount(), Some(bitcoin_amount));
assert_eq!(tlv_stream.amount, Some(1000));
assert_eq!(tlv_stream.currency, None);
#[cfg(not(c_bindings))]
- let builder = OfferBuilder::new("foo".into(), pubkey(42))
+ let builder = OfferBuilder::new(pubkey(42))
.amount(currency_amount.clone());
#[cfg(c_bindings)]
- let mut builder = OfferBuilder::new("foo".into(), pubkey(42));
+ let mut builder = OfferBuilder::new(pubkey(42));
#[cfg(c_bindings)]
builder.amount(currency_amount.clone());
let tlv_stream = builder.offer.as_tlv_stream();
Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedCurrency),
}
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.amount(currency_amount.clone())
.amount(bitcoin_amount.clone())
.build()
assert_eq!(tlv_stream.currency, None);
let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 };
- match OfferBuilder::new("foo".into(), pubkey(42)).amount(invalid_amount).build() {
+ match OfferBuilder::new(pubkey(42)).amount(invalid_amount).build() {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount),
}
}
+ #[test]
+ fn builds_offer_with_description() {
+ let offer = OfferBuilder::new(pubkey(42))
+ .description("foo".into())
+ .build()
+ .unwrap();
+ assert_eq!(offer.description(), Some(PrintableString("foo")));
+ assert_eq!(offer.as_tlv_stream().description, Some(&String::from("foo")));
+
+ let offer = OfferBuilder::new(pubkey(42))
+ .description("foo".into())
+ .description("bar".into())
+ .build()
+ .unwrap();
+ assert_eq!(offer.description(), Some(PrintableString("bar")));
+ assert_eq!(offer.as_tlv_stream().description, Some(&String::from("bar")));
+
+ let offer = OfferBuilder::new(pubkey(42))
+ .amount_msats(1000)
+ .build()
+ .unwrap();
+ assert_eq!(offer.description(), Some(PrintableString("")));
+ assert_eq!(offer.as_tlv_stream().description, Some(&String::from("")));
+ }
+
#[test]
fn builds_offer_with_features() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.features_unchecked(OfferFeatures::unknown())
.build()
.unwrap();
assert_eq!(offer.offer_features(), &OfferFeatures::unknown());
assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown()));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.features_unchecked(OfferFeatures::unknown())
.features_unchecked(OfferFeatures::empty())
.build()
let past_expiry = Duration::from_secs(0);
let now = future_expiry - Duration::from_secs(1_000);
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.absolute_expiry(future_expiry)
.build()
.unwrap();
assert_eq!(offer.absolute_expiry(), Some(future_expiry));
assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(future_expiry.as_secs()));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.absolute_expiry(future_expiry)
.absolute_expiry(past_expiry)
.build()
fn builds_offer_with_paths() {
let paths = vec![
BlindedPath {
- introduction_node_id: pubkey(40),
+ introduction_node: IntroductionNode::NodeId(pubkey(40)),
blinding_point: pubkey(41),
blinded_hops: vec![
BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
],
},
BlindedPath {
- introduction_node_id: pubkey(40),
+ introduction_node: IntroductionNode::NodeId(pubkey(40)),
blinding_point: pubkey(41),
blinded_hops: vec![
BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] },
},
];
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.path(paths[0].clone())
.path(paths[1].clone())
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.paths(), paths.as_slice());
- assert_eq!(offer.signing_pubkey(), pubkey(42));
+ assert_eq!(offer.signing_pubkey(), Some(pubkey(42)));
assert_ne!(pubkey(42), pubkey(44));
assert_eq!(tlv_stream.paths, Some(&paths));
assert_eq!(tlv_stream.node_id, Some(&pubkey(42)));
#[test]
fn builds_offer_with_issuer() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
- .issuer("bar".into())
+ let offer = OfferBuilder::new(pubkey(42))
+ .issuer("foo".into())
.build()
.unwrap();
- assert_eq!(offer.issuer(), Some(PrintableString("bar")));
- assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar")));
+ assert_eq!(offer.issuer(), Some(PrintableString("foo")));
+ assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("foo")));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
+ .issuer("foo".into())
.issuer("bar".into())
- .issuer("baz".into())
.build()
.unwrap();
- assert_eq!(offer.issuer(), Some(PrintableString("baz")));
- assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("baz")));
+ assert_eq!(offer.issuer(), Some(PrintableString("bar")));
+ assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar")));
}
#[test]
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::One)
.build()
.unwrap();
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::Unbounded)
.build()
.unwrap();
assert_eq!(offer.supported_quantity(), Quantity::Unbounded);
assert_eq!(tlv_stream.quantity_max, Some(0));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.build()
.unwrap();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::One)
.build()
#[test]
fn fails_requesting_invoice_with_unknown_required_features() {
- match OfferBuilder::new("foo".into(), pubkey(42))
+ match OfferBuilder::new(pubkey(42))
.features_unchecked(OfferFeatures::unknown())
.build().unwrap()
.request_invoice(vec![1; 32], pubkey(43))
#[test]
fn parses_offer_with_chains() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.chain(Network::Bitcoin)
.chain(Network::Testnet)
.build()
#[test]
fn parses_offer_with_amount() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.amount(Amount::Bitcoin { amount_msats: 1000 })
.build()
.unwrap();
#[test]
fn parses_offer_with_description() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
+ let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let offer = OfferBuilder::new(pubkey(42))
+ .description("foo".to_string())
+ .amount_msats(1000)
+ .build().unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
#[test]
fn parses_offer_with_paths() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.path(BlindedPath {
- introduction_node_id: pubkey(40),
+ introduction_node: IntroductionNode::NodeId(pubkey(40)),
blinding_point: pubkey(41),
blinded_hops: vec![
BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
],
})
.path(BlindedPath {
- introduction_node_id: pubkey(40),
+ introduction_node: IntroductionNode::NodeId(pubkey(40)),
blinding_point: pubkey(41),
blinded_hops: vec![
BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] },
panic!("error parsing offer: {:?}", e);
}
- let mut builder = OfferBuilder::new("foo".into(), pubkey(42));
+ let offer = OfferBuilder::new(pubkey(42))
+ .path(BlindedPath {
+ introduction_node: IntroductionNode::NodeId(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] },
+ ],
+ })
+ .clear_signing_pubkey()
+ .build()
+ .unwrap();
+ if let Err(e) = offer.to_string().parse::<Offer>() {
+ panic!("error parsing offer: {:?}", e);
+ }
+
+ let mut builder = OfferBuilder::new(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);
+ match offer.to_string().parse::<Offer>() {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaths));
+ },
}
}
#[test]
fn parses_offer_with_quantity() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::One)
.build()
.unwrap();
panic!("error parsing offer: {:?}", e);
}
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::Unbounded)
.build()
.unwrap();
panic!("error parsing offer: {:?}", e);
}
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(10).unwrap()))
.build()
.unwrap();
panic!("error parsing offer: {:?}", e);
}
- let offer = OfferBuilder::new("foo".into(), pubkey(42))
+ let offer = OfferBuilder::new(pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
#[test]
fn parses_offer_with_node_id() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
+ let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
#[test]
fn fails_parsing_offer_with_extra_tlv_records() {
- let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
+ let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
let mut encoded_offer = Vec::new();
offer.write(&mut encoded_offer).unwrap();
// Malformed: empty
assert_eq!(
"lno1".parse::<Offer>(),
- Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription)),
+ Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey)),
);
// Malformed: truncated at type
// Missing offer_description
assert_eq!(
- "lno1zcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese".parse::<Offer>(),
+ // TODO: Match the spec once it is updated.
+ "lno1pqpq86qkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg".parse::<Offer>(),
Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription)),
);