//! * For parsing use `str::parse::<Invoice>(&self)` (see the docs of `impl FromStr for Invoice`)
//! * For constructing invoices use the `InvoiceBuilder`
//! * For serializing invoices use the `Display`/`ToString` traits
+pub mod payment;
pub mod utils;
extern crate bech32;
extern crate bitcoin_hashes;
-extern crate lightning;
+#[macro_use] extern crate lightning;
extern crate num_traits;
extern crate secp256k1;
///
/// ```
/// extern crate secp256k1;
+/// extern crate lightning;
/// extern crate lightning_invoice;
/// extern crate bitcoin_hashes;
///
/// use secp256k1::Secp256k1;
/// use secp256k1::key::SecretKey;
///
+/// use lightning::ln::PaymentSecret;
+///
/// use lightning_invoice::{Currency, InvoiceBuilder};
///
/// # fn main() {
/// ).unwrap();
///
/// let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap();
+/// let payment_secret = PaymentSecret([42u8; 32]);
///
/// let invoice = InvoiceBuilder::new(Currency::Bitcoin)
/// .description("Coins pls!".into())
/// .payment_hash(payment_hash)
+/// .payment_secret(payment_secret)
/// .current_timestamp()
/// .min_final_cltv_expiry(144)
/// .build_signed(|hash| {
/// SHA-256 hash
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
-pub struct Sha256(pub sha256::Hash);
+pub struct Sha256(/// (C-not exported) as the native hash types are not currently mapped
+ pub sha256::Hash);
/// Description string
///
}
}
-impl<S: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, S> {
+impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, tb::True> {
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail
/// and MUST produce a recoverable signature valid for the given hash and if applicable also for
/// the included payee public key.
return Err(SemanticError::MultipleDescriptions);
}
+ self.check_payment_secret()?;
+
+ Ok(())
+ }
+
+ /// Checks that there is exactly one payment secret field
+ fn check_payment_secret(&self) -> Result<(), SemanticError> {
+ // "A writer MUST include exactly one `s` field."
+ let payment_secret_count = self.tagged_fields().filter(|&tf| match *tf {
+ TaggedField::PaymentSecret(_) => true,
+ _ => false,
+ }).count();
+ if payment_secret_count < 1 {
+ return Err(SemanticError::NoPaymentSecret);
+ } else if payment_secret_count > 1 {
+ return Err(SemanticError::MultiplePaymentSecrets);
+ }
+
Ok(())
}
/// Check that feature bits are set as required
fn check_feature_bits(&self) -> Result<(), SemanticError> {
- // "If the payment_secret feature is set, MUST include exactly one s field."
- let payment_secret_count = self.tagged_fields().filter(|&tf| match *tf {
- TaggedField::PaymentSecret(_) => true,
- _ => false,
- }).count();
- if payment_secret_count > 1 {
- return Err(SemanticError::MultiplePaymentSecrets);
- }
+ self.check_payment_secret()?;
// "A writer MUST set an s field if and only if the payment_secret feature is set."
- let has_payment_secret = payment_secret_count == 1;
+ // (this requirement has been since removed, and we now require the payment secret
+ // feature bit always).
let features = self.tagged_fields().find(|&tf| match *tf {
TaggedField::Features(_) => true,
_ => false,
});
match features {
- None if has_payment_secret => Err(SemanticError::InvalidFeatures),
- None => Ok(()),
+ None => Err(SemanticError::InvalidFeatures),
Some(TaggedField::Features(features)) => {
if features.requires_unknown_bits() {
Err(SemanticError::InvalidFeatures)
- } else if features.supports_payment_secret() && has_payment_secret {
- Ok(())
- } else if has_payment_secret {
- Err(SemanticError::InvalidFeatures)
- } else if features.supports_payment_secret() {
+ } else if !features.supports_payment_secret() {
Err(SemanticError::InvalidFeatures)
} else {
Ok(())
}
/// Get the payment secret if one was included in the invoice
- pub fn payment_secret(&self) -> Option<&PaymentSecret> {
- self.signed_invoice.payment_secret()
+ pub fn payment_secret(&self) -> &PaymentSecret {
+ self.signed_invoice.payment_secret().expect("was checked by constructor")
}
/// Get the invoice features if they were included in the invoice
.unwrap_or(Duration::from_secs(DEFAULT_EXPIRY_TIME))
}
+ /// Returns whether the invoice has expired.
+ pub fn is_expired(&self) -> bool {
+ Self::is_expired_from_epoch(self.timestamp(), self.expiry_time())
+ }
+
+ /// Returns whether the expiry time from the given epoch has passed.
+ pub(crate) fn is_expired_from_epoch(epoch: &SystemTime, expiry_time: Duration) -> bool {
+ match epoch.elapsed() {
+ Ok(elapsed) => elapsed > expiry_time,
+ Err(_) => false,
+ }
+ }
+
/// Returns the invoice's `min_final_cltv_expiry` time, if present, otherwise
/// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY`].
pub fn min_final_cltv_expiry(&self) -> u64 {
}
/// Returns a list of all routes included in the invoice as the underlying hints
- pub fn route_hints(&self) -> Vec<&RouteHint> {
+ pub fn route_hints(&self) -> Vec<RouteHint> {
find_all_extract!(
self.signed_invoice.known_tagged_fields(), TaggedField::PrivateRoute(ref x), x
- ).map(|route| &**route).collect()
+ ).map(|route| (**route).clone()).collect()
}
/// Returns the currency for which the invoice was issued
self.signed_invoice.currency()
}
+ /// Returns the amount if specified in the invoice as millisatoshis.
+ pub fn amount_milli_satoshis(&self) -> Option<u64> {
+ self.signed_invoice.amount_pico_btc().map(|v| v / 10)
+ }
+
/// Returns the amount if specified in the invoice as pico <currency>.
- pub fn amount_pico_btc(&self) -> Option<u64> {
+ fn amount_pico_btc(&self) -> Option<u64> {
self.signed_invoice.amount_pico_btc()
}
}
/// The supplied expiry time could cause an overflow if added to a `PositiveTimestamp`
ExpiryTimeOutOfBounds,
+
+ /// The supplied millisatoshi amount was greater than the total bitcoin supply.
+ InvalidAmount,
}
impl Display for CreationError {
CreationError::RouteTooLong => f.write_str("The specified route has too many hops and can't be encoded"),
CreationError::TimestampOutOfBounds => f.write_str("The unix timestamp of the supplied date is <0 or can't be represented as `SystemTime`"),
CreationError::ExpiryTimeOutOfBounds => f.write_str("The supplied expiry time could cause an overflow if added to a `PositiveTimestamp`"),
+ CreationError::InvalidAmount => f.write_str("The supplied millisatoshi amount was greater than the total bitcoin supply"),
}
}
}
/// The invoice contains multiple descriptions and/or description hashes which isn't allowed
MultipleDescriptions,
+ /// The invoice is missing the mandatory payment secret, which all modern lightning nodes
+ /// should provide.
+ NoPaymentSecret,
+
/// The invoice contains multiple payment secrets
MultiplePaymentSecrets,
SemanticError::MultiplePaymentHashes => f.write_str("The invoice has multiple payment hashes which isn't allowed"),
SemanticError::NoDescription => f.write_str("No description or description hash are part of the invoice"),
SemanticError::MultipleDescriptions => f.write_str("The invoice contains multiple descriptions and/or description hashes which isn't allowed"),
+ SemanticError::NoPaymentSecret => f.write_str("The invoice is missing the mandatory payment secret"),
SemanticError::MultiplePaymentSecrets => f.write_str("The invoice contains multiple payment secrets"),
SemanticError::InvalidFeatures => f.write_str("The invoice's features are invalid"),
SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"),
let invoice = invoice_template.clone();
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
}.unwrap();
- assert!(Invoice::from_signed(invoice).is_ok());
+ assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
// No payment secret or feature bits
let invoice = {
invoice.data.tagged_fields.push(Features(InvoiceFeatures::empty()).into());
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
}.unwrap();
- assert!(Invoice::from_signed(invoice).is_ok());
+ assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
// Missing payment secret
let invoice = {
invoice.data.tagged_fields.push(Features(InvoiceFeatures::known()).into());
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
}.unwrap();
- assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures));
+ assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
// Multiple payment secrets
let invoice = {
let sign_error_res = builder.clone()
.description("Test".into())
+ .payment_secret(PaymentSecret([0; 32]))
.try_build_signed(|_| {
Err("ImaginaryError")
});
assert!(invoice.check_signature().is_ok());
assert_eq!(invoice.tagged_fields().count(), 10);
+ assert_eq!(invoice.amount_milli_satoshis(), Some(123));
assert_eq!(invoice.amount_pico_btc(), Some(1230));
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
assert_eq!(
InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap()))
);
assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&[21;32][..]).unwrap());
- assert_eq!(invoice.payment_secret(), Some(&PaymentSecret([42; 32])));
+ assert_eq!(invoice.payment_secret(), &PaymentSecret([42; 32]));
assert_eq!(invoice.features(), Some(&InvoiceFeatures::known()));
let raw_invoice = builder.build_raw().unwrap();
let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin)
.description("Test".into())
.payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
+ .payment_secret(PaymentSecret([0; 32]))
.current_timestamp()
.build_raw()
.unwrap()
assert_eq!(invoice.min_final_cltv_expiry(), DEFAULT_MIN_FINAL_CLTV_EXPIRY);
assert_eq!(invoice.expiry_time(), Duration::from_secs(DEFAULT_EXPIRY_TIME));
+ assert!(!invoice.is_expired());
+ }
+
+ #[test]
+ fn test_expiration() {
+ use ::*;
+ use secp256k1::Secp256k1;
+ use secp256k1::key::SecretKey;
+
+ let timestamp = SystemTime::now()
+ .checked_sub(Duration::from_secs(DEFAULT_EXPIRY_TIME * 2))
+ .unwrap();
+ let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin)
+ .description("Test".into())
+ .payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
+ .payment_secret(PaymentSecret([0; 32]))
+ .timestamp(timestamp)
+ .build_raw()
+ .unwrap()
+ .sign::<_, ()>(|hash| {
+ let privkey = SecretKey::from_slice(&[41; 32]).unwrap();
+ let secp_ctx = Secp256k1::new();
+ Ok(secp_ctx.sign_recoverable(hash, &privkey))
+ })
+ .unwrap();
+ let invoice = Invoice::from_signed(signed_invoice).unwrap();
+
+ assert!(invoice.is_expired());
}
}