X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning-invoice%2Fsrc%2Flib.rs;h=04ba08bd8552c34379576b59047de719ef200ae3;hb=9fcc626ee4d23a276fc8dd87ddb2538f4d5565f9;hp=064b3626537c2d93b753e6dd9095522010131a77;hpb=bac5f4930f78c0aa95f5652bb16dd2cbdca66eca;p=rust-lightning diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 064b3626..04ba08bd 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -15,11 +15,12 @@ //! * For parsing use `str::parse::(&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; @@ -30,7 +31,7 @@ use lightning::ln::PaymentSecret; use lightning::ln::features::InvoiceFeatures; #[cfg(any(doc, test))] use lightning::routing::network_graph::RoutingFees; -use lightning::routing::router::RouteHintHop; +use lightning::routing::router::RouteHint; use secp256k1::key::PublicKey; use secp256k1::{Message, Secp256k1}; @@ -60,12 +61,16 @@ const MAX_EXPIRY_TIME: u64 = 60 * 60 * 24 * 356; /// Default expiry time as defined by [BOLT 11]. /// /// [BOLT 11]: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md -const DEFAULT_EXPIRY_TIME: u64 = 3600; +pub const DEFAULT_EXPIRY_TIME: u64 = 3600; /// Default minimum final CLTV expiry as defined by [BOLT 11]. /// +/// Note that this is *not* the same value as rust-lightning's minimum CLTV expiry, which is +/// provided in [`MIN_FINAL_CLTV_EXPIRY`]. +/// /// [BOLT 11]: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md -const DEFAULT_MIN_FINAL_CLTV_EXPIRY: u64 = 18; +/// [`MIN_FINAL_CLTV_EXPIRY`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY +pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY: u64 = 18; /// This function is used as a static assert for the size of `SystemTime`. If the crate fails to /// compile due to it this indicates that your system uses unexpected bounds for `SystemTime`. You @@ -123,6 +128,7 @@ pub fn check_platform() { /// /// ``` /// extern crate secp256k1; +/// extern crate lightning; /// extern crate lightning_invoice; /// extern crate bitcoin_hashes; /// @@ -132,6 +138,8 @@ pub fn check_platform() { /// use secp256k1::Secp256k1; /// use secp256k1::key::SecretKey; /// +/// use lightning::ln::PaymentSecret; +/// /// use lightning_invoice::{Currency, InvoiceBuilder}; /// /// # fn main() { @@ -144,10 +152,12 @@ pub fn check_platform() { /// ).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| { @@ -317,7 +327,7 @@ impl SiPrefix { } /// Enum representing the crypto currencies (or networks) supported by this library -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub enum Currency { /// Bitcoin mainnet Bitcoin, @@ -338,7 +348,7 @@ pub enum Currency { /// Tagged field which may have an unknown tag /// /// (C-not exported) as we don't currently support TaggedField -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub enum RawTaggedField { /// Parsed tagged field with known tag KnownSemantics(TaggedField), @@ -353,7 +363,7 @@ pub enum RawTaggedField { /// (C-not exported) As we don't yet support enum variants with the same name the struct contained /// in the variant. #[allow(missing_docs)] -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub enum TaggedField { PaymentHash(Sha256), Description(Description), @@ -362,24 +372,25 @@ pub enum TaggedField { ExpiryTime(ExpiryTime), MinFinalCltvExpiry(MinFinalCltvExpiry), Fallback(Fallback), - Route(RouteHint), + PrivateRoute(PrivateRoute), PaymentSecret(PaymentSecret), Features(InvoiceFeatures), } /// SHA-256 hash -#[derive(Eq, PartialEq, Debug, Clone)] -pub struct Sha256(pub sha256::Hash); +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct Sha256(/// (C-not exported) as the native hash types are not currently mapped + pub sha256::Hash); /// Description string /// /// # Invariants /// The description can be at most 639 __bytes__ long -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct Description(String); /// Payee public key -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct PayeePubKey(pub PublicKey); /// Positive duration that defines when (relatively to the timestamp) in the future the invoice @@ -389,17 +400,17 @@ pub struct PayeePubKey(pub PublicKey); /// The number of seconds this expiry time represents has to be in the range /// `0...(SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME)` to avoid overflows when adding it to a /// timestamp -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct ExpiryTime(Duration); /// `min_final_cltv_expiry` to use for the last HTLC in the route -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct MinFinalCltvExpiry(pub u64); // TODO: better types instead onf byte arrays /// Fallback address in case no LN payment is possible #[allow(missing_docs)] -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub enum Fallback { SegWitProgram { version: u5, @@ -410,7 +421,7 @@ pub enum Fallback { } /// Recoverable signature -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct InvoiceSignature(pub RecoverableSignature); /// Private routing information @@ -418,8 +429,8 @@ pub struct InvoiceSignature(pub RecoverableSignature); /// # Invariants /// The encoded route has to be <1024 5bit characters long (<=639 bytes or <=12 hops) /// -#[derive(Eq, PartialEq, Debug, Clone)] -pub struct RouteHint(Vec); +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct PrivateRoute(RouteHint); /// Tag constants as specified in BOLT11 #[allow(missing_docs)] @@ -431,7 +442,7 @@ pub mod constants { pub const TAG_EXPIRY_TIME: u8 = 6; pub const TAG_MIN_FINAL_CLTV_EXPIRY: u8 = 24; pub const TAG_FALLBACK: u8 = 9; - pub const TAG_ROUTE: u8 = 3; + pub const TAG_PRIVATE_ROUTE: u8 = 3; pub const TAG_PAYMENT_SECRET: u8 = 16; pub const TAG_FEATURES: u8 = 5; } @@ -476,8 +487,9 @@ impl InvoiceBui } } - /// Sets the amount in pico BTC. The optimal SI prefix is choosen automatically. - pub fn amount_pico_btc(mut self, amount: u64) -> Self { + /// Sets the amount in millisatoshis. The optimal SI prefix is chosen automatically. + pub fn amount_milli_satoshis(mut self, amount_msat: u64) -> Self { + let amount = amount_msat * 10; // Invoices are denominated in "pico BTC" let biggest_possible_si_prefix = SiPrefix::values_desc() .iter() .find(|prefix| amount % prefix.multiplier() == 0) @@ -509,9 +521,9 @@ impl InvoiceBui } /// Adds a private route. - pub fn route(mut self, route: Vec) -> Self { - match RouteHint::new(route) { - Ok(r) => self.tagged_fields.push(TaggedField::Route(r)), + pub fn private_route(mut self, hint: RouteHint) -> Self { + match PrivateRoute::new(hint) { + Ok(r) => self.tagged_fields.push(TaggedField::PrivateRoute(r)), Err(e) => self.error = Some(e), } self @@ -629,7 +641,7 @@ impl InvoiceBuilder InvoiceBuilder { +impl InvoiceBuilder { /// 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. @@ -669,6 +681,7 @@ impl InvoiceBuilder { invoice.check_field_counts().expect("should be ensured by type signature of builder"); invoice.check_feature_bits().expect("should be ensured by type signature of builder"); + invoice.check_amount().expect("should be ensured by type signature of builder"); Ok(invoice) } @@ -747,7 +760,7 @@ impl SignedRawInvoice { /// Finds the first element of an enum stream of a given variant and extracts one member of the /// variant. If no element was found `None` gets returned. /// -/// The following example would extract the first +/// The following example would extract the first B. /// ``` /// use Enum::* /// @@ -761,11 +774,35 @@ impl SignedRawInvoice { /// assert_eq!(find_extract!(elements.iter(), Enum::B(ref x), x), Some(3u16)) /// ``` macro_rules! find_extract { - ($iter:expr, $enm:pat, $enm_var:ident) => { + ($iter:expr, $enm:pat, $enm_var:ident) => { + find_all_extract!($iter, $enm, $enm_var).next() + }; +} + +/// Finds the all elements of an enum stream of a given variant and extracts one member of the +/// variant through an iterator. +/// +/// The following example would extract all A. +/// ``` +/// use Enum::* +/// +/// enum Enum { +/// A(u8), +/// B(u16) +/// } +/// +/// let elements = vec![A(1), A(2), B(3), A(4)] +/// +/// assert_eq!( +/// find_all_extract!(elements.iter(), Enum::A(ref x), x).collect::>(), +/// vec![1u8, 2u8, 4u8]) +/// ``` +macro_rules! find_all_extract { + ($iter:expr, $enm:pat, $enm_var:ident) => { $iter.filter_map(|tf| match *tf { $enm => Some($enm_var), _ => None, - }).next() + }) }; } @@ -886,17 +923,11 @@ impl RawInvoice { /// (C-not exported) as we don't support Vec<&NonOpaqueType> pub fn fallbacks(&self) -> Vec<&Fallback> { - self.known_tagged_fields().filter_map(|tf| match tf { - &TaggedField::Fallback(ref f) => Some(f), - _ => None, - }).collect::>() + find_all_extract!(self.known_tagged_fields(), TaggedField::Fallback(ref x), x).collect() } - pub fn routes(&self) -> Vec<&RouteHint> { - self.known_tagged_fields().filter_map(|tf| match tf { - &TaggedField::Route(ref r) => Some(r), - _ => None, - }).collect::>() + pub fn private_routes(&self) -> Vec<&PrivateRoute> { + find_all_extract!(self.known_tagged_fields(), TaggedField::PrivateRoute(ref x), x).collect() } pub fn amount_pico_btc(&self) -> Option { @@ -994,35 +1025,54 @@ impl Invoice { return Err(SemanticError::MultipleDescriptions); } + self.check_payment_secret()?; + 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." + /// 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 { + if payment_secret_count < 1 { + return Err(SemanticError::NoPaymentSecret); + } else if payment_secret_count > 1 { return Err(SemanticError::MultiplePaymentSecrets); } + Ok(()) + } + + /// Check that amount is a whole number of millisatoshis + fn check_amount(&self) -> Result<(), SemanticError> { + if let Some(amount_pico_btc) = self.amount_pico_btc() { + if amount_pico_btc % 10 != 0 { + return Err(SemanticError::ImpreciseAmount); + } + } + Ok(()) + } + + /// Check that feature bits are set as required + fn check_feature_bits(&self) -> Result<(), SemanticError> { + 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.supports_payment_secret() && has_payment_secret { - Ok(()) - } else if has_payment_secret { + if features.requires_unknown_bits() { Err(SemanticError::InvalidFeatures) - } else if features.supports_payment_secret() { + } else if !features.supports_payment_secret() { Err(SemanticError::InvalidFeatures) } else { Ok(()) @@ -1037,7 +1087,9 @@ impl Invoice { match self.signed_invoice.recover_payee_pub_key() { Err(secp256k1::Error::InvalidRecoveryId) => return Err(SemanticError::InvalidRecoveryId), - Err(_) => panic!("no other error may occur"), + Err(secp256k1::Error::InvalidSignature) => + return Err(SemanticError::InvalidSignature), + Err(e) => panic!("no other error may occur, got {:?}", e), Ok(_) => {}, } @@ -1048,14 +1100,21 @@ impl Invoice { Ok(()) } - /// Constructs an `Invoice` from a `SignedInvoice` by checking all its invariants. + /// Constructs an `Invoice` from a `SignedRawInvoice` by checking all its invariants. /// ``` /// use lightning_invoice::*; /// - /// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\ - /// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\ - /// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\ - /// ky03ylcqca784w"; + /// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\ + /// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\ + /// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\ + /// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\ + /// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\ + /// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\ + /// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\ + /// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\ + /// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\ + /// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\ + /// j5r6drg6k6zcqj0fcwg"; /// /// let signed = invoice.parse::().unwrap(); /// @@ -1068,6 +1127,7 @@ impl Invoice { invoice.check_field_counts()?; invoice.check_feature_bits()?; invoice.check_signature()?; + invoice.check_amount()?; Ok(invoice) } @@ -1108,8 +1168,8 @@ impl Invoice { } /// 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 @@ -1129,6 +1189,19 @@ impl 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 { @@ -1145,8 +1218,15 @@ impl Invoice { } /// Returns a list of all routes included in the invoice - pub fn routes(&self) -> Vec<&RouteHint> { - self.signed_invoice.routes() + pub fn private_routes(&self) -> Vec<&PrivateRoute> { + self.signed_invoice.private_routes() + } + + /// Returns a list of all routes included in the invoice as the underlying hints + pub fn route_hints(&self) -> Vec { + find_all_extract!( + self.signed_invoice.known_tagged_fields(), TaggedField::PrivateRoute(ref x), x + ).map(|route| (**route).clone()).collect() } /// Returns the currency for which the invoice was issued @@ -1154,8 +1234,13 @@ impl Invoice { self.signed_invoice.currency() } + /// Returns the amount if specified in the invoice as millisatoshis. + pub fn amount_milli_satoshis(&self) -> Option { + self.signed_invoice.amount_pico_btc().map(|v| v / 10) + } + /// Returns the amount if specified in the invoice as pico . - pub fn amount_pico_btc(&self) -> Option { + fn amount_pico_btc(&self) -> Option { self.signed_invoice.amount_pico_btc() } } @@ -1177,7 +1262,7 @@ impl TaggedField { TaggedField::ExpiryTime(_) => constants::TAG_EXPIRY_TIME, TaggedField::MinFinalCltvExpiry(_) => constants::TAG_MIN_FINAL_CLTV_EXPIRY, TaggedField::Fallback(_) => constants::TAG_FALLBACK, - TaggedField::Route(_) => constants::TAG_ROUTE, + TaggedField::PrivateRoute(_) => constants::TAG_PRIVATE_ROUTE, TaggedField::PaymentSecret(_) => constants::TAG_PAYMENT_SECRET, TaggedField::Features(_) => constants::TAG_FEATURES, }; @@ -1268,32 +1353,32 @@ impl ExpiryTime { } } -impl RouteHint { - /// Create a new (partial) route from a list of hops - pub fn new(hops: Vec) -> Result { - if hops.len() <= 12 { - Ok(RouteHint(hops)) +impl PrivateRoute { + /// Creates a new (partial) route from a list of hops + pub fn new(hops: RouteHint) -> Result { + if hops.0.len() <= 12 { + Ok(PrivateRoute(hops)) } else { Err(CreationError::RouteTooLong) } } - /// Returrn the underlying vector of hops - pub fn into_inner(self) -> Vec { + /// Returns the underlying list of hops + pub fn into_inner(self) -> RouteHint { self.0 } } -impl Into> for RouteHint { - fn into(self) -> Vec { +impl Into for PrivateRoute { + fn into(self) -> RouteHint { self.into_inner() } } -impl Deref for RouteHint { - type Target = Vec; +impl Deref for PrivateRoute { + type Target = RouteHint; - fn deref(&self) -> &Vec { + fn deref(&self) -> &RouteHint { &self.0 } } @@ -1359,6 +1444,10 @@ pub enum SemanticError { /// 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, @@ -1370,6 +1459,9 @@ pub enum SemanticError { /// The invoice's signature is invalid InvalidSignature, + + /// The invoice's amount was not a whole number of millisatoshis + ImpreciseAmount, } impl Display for SemanticError { @@ -1379,10 +1471,12 @@ impl Display for SemanticError { 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"), SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"), + SemanticError::ImpreciseAmount => f.write_str("The invoice's amount was not a whole number of millisatoshis"), } } } @@ -1594,7 +1688,7 @@ mod test { 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 = { @@ -1602,7 +1696,7 @@ mod test { 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 = { @@ -1610,7 +1704,7 @@ mod test { 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 = { @@ -1632,7 +1726,7 @@ mod test { .current_timestamp(); let invoice = builder.clone() - .amount_pico_btc(15000) + .amount_milli_satoshis(1500) .build_raw() .unwrap(); @@ -1641,7 +1735,7 @@ mod test { let invoice = builder.clone() - .amount_pico_btc(1500) + .amount_milli_satoshis(150) .build_raw() .unwrap(); @@ -1652,6 +1746,7 @@ mod test { #[test] fn test_builder_fail() { use ::*; + use lightning::routing::router::RouteHintHop; use std::iter::FromIterator; use secp256k1::key::PublicKey; @@ -1686,15 +1781,16 @@ mod test { htlc_minimum_msat: None, htlc_maximum_msat: None, }; - let too_long_route = vec![route_hop; 13]; + let too_long_route = RouteHint(vec![route_hop; 13]); let long_route_res = builder.clone() .description("Test".into()) - .route(too_long_route) + .private_route(too_long_route) .build_raw(); assert_eq!(long_route_res, Err(CreationError::RouteTooLong)); let sign_error_res = builder.clone() .description("Test".into()) + .payment_secret(PaymentSecret([0; 32])) .try_build_signed(|_| { Err("ImaginaryError") }); @@ -1704,6 +1800,7 @@ mod test { #[test] fn test_builder_ok() { use ::*; + use lightning::routing::router::RouteHintHop; use secp256k1::Secp256k1; use secp256k1::key::{SecretKey, PublicKey}; use std::time::{UNIX_EPOCH, Duration}; @@ -1719,7 +1816,7 @@ mod test { ).unwrap(); let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key); - let route_1 = vec![ + let route_1 = RouteHint(vec![ RouteHintHop { src_node_id: public_key.clone(), short_channel_id: de::parse_int_be(&[123; 8], 256).expect("short chan ID slice too big?"), @@ -1742,9 +1839,9 @@ mod test { htlc_minimum_msat: None, htlc_maximum_msat: None, } - ]; + ]); - let route_2 = vec![ + let route_2 = RouteHint(vec![ RouteHintHop { src_node_id: public_key.clone(), short_channel_id: 0, @@ -1767,17 +1864,17 @@ mod test { htlc_minimum_msat: None, htlc_maximum_msat: None, } - ]; + ]); let builder = InvoiceBuilder::new(Currency::BitcoinTestnet) - .amount_pico_btc(123) + .amount_milli_satoshis(123) .timestamp(UNIX_EPOCH + Duration::from_secs(1234567)) .payee_pub_key(public_key.clone()) .expiry_time(Duration::from_secs(54321)) .min_final_cltv_expiry(144) .fallback(Fallback::PubKeyHash([0;20])) - .route(route_1.clone()) - .route(route_2.clone()) + .private_route(route_1.clone()) + .private_route(route_2.clone()) .description_hash(sha256::Hash::from_slice(&[3;32][..]).unwrap()) .payment_hash(sha256::Hash::from_slice(&[21;32][..]).unwrap()) .payment_secret(PaymentSecret([42; 32])) @@ -1790,7 +1887,8 @@ mod test { assert!(invoice.check_signature().is_ok()); assert_eq!(invoice.tagged_fields().count(), 10); - assert_eq!(invoice.amount_pico_btc(), Some(123)); + assert_eq!(invoice.amount_milli_satoshis(), Some(123)); + assert_eq!(invoice.amount_pico_btc(), Some(1230)); assert_eq!(invoice.currency(), Currency::BitcoinTestnet); assert_eq!( invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(), @@ -1800,13 +1898,13 @@ mod test { assert_eq!(invoice.expiry_time(), Duration::from_secs(54321)); assert_eq!(invoice.min_final_cltv_expiry(), 144); assert_eq!(invoice.fallbacks(), vec![&Fallback::PubKeyHash([0;20])]); - assert_eq!(invoice.routes(), vec![&RouteHint(route_1), &RouteHint(route_2)]); + assert_eq!(invoice.private_routes(), vec![&PrivateRoute(route_1), &PrivateRoute(route_2)]); assert_eq!( invoice.description(), 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(); @@ -1822,6 +1920,7 @@ mod test { 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() @@ -1835,5 +1934,33 @@ mod test { 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()); } }