Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions lnvps_api/src/provisioner/lnvps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use isocountry::CountryCode;
use lnvps_api_common::retry::{OpResult, Pipeline, RetryPolicy};
use lnvps_api_common::{
AvailableIp, CostResult, HostCapacityService, NetworkProvisioner, NewPaymentInfo,
PricingEngine, UpgradeConfig, UpgradeCostQuote,
PricingEngine, UpgradeConfig, UpgradeCostQuote, round_msat_to_sat,
};
use lnvps_api_common::{ExchangeRateService, op_fatal};
use lnvps_db::{
Expand Down Expand Up @@ -362,7 +362,8 @@ impl LNVpsProvisioner {
"Lightning payment must be in BTC"
);
const INVOICE_EXPIRE: u64 = 600;
let invoice_amount = converted.amount.value() + tax;
// Round to nearest satoshi for wallet compatibility
let invoice_amount = round_msat_to_sat(converted.amount.value() + tax);
let desc = match payment_type {
SubscriptionPaymentType::Purchase => {
format!("Subscription purchase: {}", subscription.name)
Expand Down Expand Up @@ -496,7 +497,8 @@ impl LNVpsProvisioner {
"Cannot create invoices for non-BTC currency"
);
const INVOICE_EXPIRE: u64 = 600;
let total_amount = p.amount + p.tax;
// Round to nearest satoshi for wallet compatibility
let total_amount = round_msat_to_sat(p.amount + p.tax);
info!(
"Creating invoice for {vm_id} for {} sats",
total_amount / 1000
Expand Down Expand Up @@ -1163,10 +1165,10 @@ mod tests {
assert_eq!(vm.id, payment.vm_id);
assert_eq!(payment.tax, (payment.amount as f64 * 0.01).floor() as u64);

// check invoice amount matches amount+tax
// check invoice amount matches rounded amount+tax
let inv = node.invoices.lock().await;
if let Some(i) = inv.get(&hex::encode(payment.id)) {
assert_eq!(i.amount, payment.amount + payment.tax);
assert_eq!(i.amount, round_msat_to_sat(payment.amount + payment.tax));
} else {
bail!("Invoice doesnt exist");
}
Expand Down
40 changes: 37 additions & 3 deletions lnvps_api_common/src/pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ use std::ops::{Add, Sub};
use std::str::FromStr;
use std::sync::Arc;

/// Round milli-satoshi amount up to the nearest satoshi.
///
/// Some Lightning wallets don't handle milli-sats correctly, so we round
/// amounts to whole satoshis. We round up to avoid underpayment.
pub fn round_msat_to_sat(msat: u64) -> u64 {
msat.div_ceil(1000) * 1000
}

fn round_to_sat(amount: CurrencyAmount) -> CurrencyAmount {
debug_assert_eq!(amount.currency(), Currency::BTC);
CurrencyAmount::from_u64(Currency::BTC, round_msat_to_sat(amount.value()))
}

/// Result of calculating upgrade costs including both immediate upgrade cost and new renewal cost
#[derive(Debug, Clone)]
pub struct UpgradeCostQuote {
Expand Down Expand Up @@ -739,12 +752,15 @@ impl PricingEngine {
(c, PaymentMethod::Lightning) if c != Currency::BTC => {
// convert to BTC if price is not already in bitcoin
let ticker = self.get_ticker(Currency::BTC, c).await?;
ticker.convert_with_rate(list_price)?
let mut converted = ticker.convert_with_rate(list_price)?;
// Round to nearest satoshi for wallet compatibility
converted.amount = round_to_sat(converted.amount);
converted
}
(c, PaymentMethod::Lightning) if c == Currency::BTC => {
// pass-through price as BTC
// pass-through price as BTC, rounded to nearest satoshi
ConvertedCurrencyAmount {
amount: CurrencyAmount::from_u64(list_price.currency(), list_price.value()),
amount: round_to_sat(list_price),
rate: TickerRate::passthrough(Currency::BTC),
}
}
Expand Down Expand Up @@ -821,6 +837,24 @@ mod tests {
VmCustomPricingDisk, VmCustomTemplate,
};

#[test]
fn test_round_msat_to_sat_exact() {
// Exact satoshi amounts should stay the same
assert_eq!(round_msat_to_sat(1000), 1000);
assert_eq!(round_msat_to_sat(5000), 5000);
assert_eq!(round_msat_to_sat(0), 0);
}

#[test]
fn test_round_msat_to_sat_rounds_up() {
// Sub-satoshi amounts should round up to the next satoshi
assert_eq!(round_msat_to_sat(1), 1000);
assert_eq!(round_msat_to_sat(999), 1000);
assert_eq!(round_msat_to_sat(1001), 2000);
assert_eq!(round_msat_to_sat(1500), 2000);
assert_eq!(round_msat_to_sat(1999), 2000);
}

const MOCK_RATE: f32 = 100_000.0;
const SECONDS_PER_MONTH: f64 = 30.0 * 24.0 * 3600.0; // 30 days * 24 hours * 3600 seconds

Expand Down