diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 199fbe79a..bcb15245d 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -480,8 +480,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) @@ -673,6 +674,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) } @@ -1019,6 +1021,16 @@ impl Invoice { 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> { // "If the payment_secret feature is set, MUST include exactly one s field." @@ -1099,6 +1111,7 @@ impl Invoice { invoice.check_field_counts()?; invoice.check_feature_bits()?; invoice.check_signature()?; + invoice.check_amount()?; Ok(invoice) } @@ -1408,6 +1421,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 { @@ -1421,6 +1437,7 @@ impl Display for SemanticError { 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"), } } } @@ -1670,7 +1687,7 @@ mod test { .current_timestamp(); let invoice = builder.clone() - .amount_pico_btc(15000) + .amount_milli_satoshis(1500) .build_raw() .unwrap(); @@ -1679,7 +1696,7 @@ mod test { let invoice = builder.clone() - .amount_pico_btc(1500) + .amount_milli_satoshis(150) .build_raw() .unwrap(); @@ -1810,7 +1827,7 @@ mod test { ]); 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)) @@ -1830,7 +1847,7 @@ 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_pico_btc(), Some(1230)); assert_eq!(invoice.currency(), Currency::BitcoinTestnet); assert_eq!( invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(), diff --git a/lightning-invoice/src/utils.rs b/lightning-invoice/src/utils.rs index f419f5f7f..c491538aa 100644 --- a/lightning-invoice/src/utils.rs +++ b/lightning-invoice/src/utils.rs @@ -68,7 +68,7 @@ where .basic_mpp() .min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into()); if let Some(amt) = amt_msat { - invoice = invoice.amount_pico_btc(amt * 10); + invoice = invoice.amount_milli_satoshis(amt); } for hint in route_hints { invoice = invoice.private_route(hint); diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index 807308044..c2f82b099 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -49,7 +49,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch\ 9zw97j25emudupq63nyw24cg27h2rspfj9srp".to_owned(), InvoiceBuilder::new(Currency::Bitcoin) - .amount_pico_btc(2500000000) + .amount_milli_satoshis(250_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) .payment_hash(sha256::Hash::from_hex( "0001020304050607080900010203040506070809000102030405060708090102" @@ -78,7 +78,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { dhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7k\ hhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7".to_owned(), InvoiceBuilder::new(Currency::Bitcoin) - .amount_pico_btc(20000000000) + .amount_milli_satoshis(2_000_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) .payment_hash(sha256::Hash::from_hex( "0001020304050607080900010203040506070809000102030405060708090102" @@ -110,7 +110,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { "0001020304050607080900010203040506070809000102030405060708090102" ).unwrap()) .description("coffee beans".to_string()) - .amount_pico_btc(20000000000) + .amount_milli_satoshis(2_000_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) .payment_secret(PaymentSecret([42; 32])) .build_raw() @@ -172,4 +172,7 @@ fn test_bolt_invalid_invoices() { assert_eq!(Invoice::from_str( "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg" ), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix))); + assert_eq!(Invoice::from_str( + "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu7hqtk93pkf7sw55rdv4k9z2vj050rxdr6za9ekfs3nlt5lr89jqpdmxsmlj9urqumg0h9wzpqecw7th56tdms40p2ny9q4ddvjsedzcplva53s" + ), Err(ParseOrSemanticError::SemanticError(SemanticError::ImpreciseAmount))); }