diff --git a/BTCPayServer.Rating/CurrencyNameTable.cs b/BTCPayServer.Rating/CurrencyNameTable.cs index 309830d81..56aada94a 100644 --- a/BTCPayServer.Rating/CurrencyNameTable.cs +++ b/BTCPayServer.Rating/CurrencyNameTable.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; -using BTCPayServer.Rating; using NBitcoin; using Newtonsoft.Json; @@ -28,14 +27,6 @@ namespace BTCPayServer.Services.Rates } static readonly Dictionary _CurrencyProviders = new Dictionary(); - public string FormatCurrency(string price, string currency) - { - return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency); - } - public string FormatCurrency(decimal price, string currency) - { - return price.ToString("C", GetCurrencyProvider(currency)); - } public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback) { @@ -56,6 +47,7 @@ namespace BTCPayServer.Services.Rates currencyInfo.CurrencySymbol = currency; return currencyInfo; } + public NumberFormatInfo GetNumberFormatInfo(string currency) { var curr = GetCurrencyProvider(currency); @@ -65,6 +57,7 @@ namespace BTCPayServer.Services.Rates return ni; return null; } + public IFormatProvider GetCurrencyProvider(string currency) { lock (_CurrencyProviders) @@ -104,30 +97,6 @@ namespace BTCPayServer.Services.Rates currencyProviders.TryAdd(code, number); } - /// - /// Format a currency like "0.004 $ (USD)", round to significant divisibility - /// - /// The value - /// Currency code - /// - public string DisplayFormatCurrency(decimal value, string currency) - { - var provider = GetNumberFormatInfo(currency, true); - var currencyData = GetCurrencyData(currency, true); - var divisibility = currencyData.Divisibility; - value = value.RoundToSignificant(ref divisibility); - if (divisibility != provider.CurrencyDecimalDigits) - { - provider = (NumberFormatInfo)provider.Clone(); - provider.CurrencyDecimalDigits = divisibility; - } - - if (currencyData.Crypto) - return value.ToString("C", provider); - else - return value.ToString("C", provider) + $" ({currency})"; - } - readonly Dictionary _Currencies; static CurrencyData[] LoadCurrency() diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs index ee7e5264e..35521fd03 100644 --- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -396,21 +396,21 @@ namespace BTCPayServer.Tests } s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1)); - Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat - Assert.Contains("1.10000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before - Assert.Contains("2.20000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate + Assert.Contains("5,500.00 USD", s.Driver.PageSource); // Should propose reimburse in fiat + Assert.Contains("1.10000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before + Assert.Contains("2.20000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate s.Driver.WaitForAndClick(By.Id(rateSelection)); s.Driver.FindElement(By.Id("ok")).Click(); - + s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1)); Assert.Contains("pull-payments", s.Driver.Url); if (rateSelection == "FiatOption") - Assert.Contains("$5,500.00", s.Driver.PageSource); + Assert.Contains("5,500.00 USD", s.Driver.PageSource); if (rateSelection == "CurrentOption") - Assert.Contains("2.20000000 ₿", s.Driver.PageSource); + Assert.Contains("2.20000000 BTC", s.Driver.PageSource); if (rateSelection == "RateThenOption") - Assert.Contains("1.10000000 ₿", s.Driver.PageSource); - + Assert.Contains("1.10000000 BTC", s.Driver.PageSource); + s.GoToInvoice(invoice.Id); s.Driver.FindElement(By.Id("IssueRefund")).Click(); s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1)); diff --git a/BTCPayServer.Tests/Checkoutv2Tests.cs b/BTCPayServer.Tests/Checkoutv2Tests.cs index 3381b28a4..9fc9a5fa6 100644 --- a/BTCPayServer.Tests/Checkoutv2Tests.cs +++ b/BTCPayServer.Tests/Checkoutv2Tests.cs @@ -73,6 +73,14 @@ namespace BTCPayServer.Tests s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC")); s.Driver.ElementDoesNotExist(By.Id("PayByLNURL")); + // Details should show exchange rate + s.Driver.ToggleCollapse("PaymentDetails"); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice")); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat")); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue")); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("sat/byte", s.Driver.FindElement(By.Id("PaymentDetails-RecommendedFee")).Text); + // Switch to LNURL s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click(); TestUtils.Eventually(() => @@ -86,7 +94,7 @@ namespace BTCPayServer.Tests // Default payment method s.GoToHome(); - invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike"); + invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC_LightningLike"); s.GoToInvoiceCheckout(invoiceId); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count); @@ -112,6 +120,14 @@ namespace BTCPayServer.Tests s.GoToInvoiceCheckout(invoiceId); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text); + + // Details should not show exchange rate + s.Driver.ToggleCollapse("PaymentDetails"); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-ExchangeRate")); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat")); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-RecommendedFee")); + Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text); + Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text); // Expire var expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds")); @@ -145,6 +161,10 @@ namespace BTCPayServer.Tests Assert.Contains("Exchange Rate", details.Text); Assert.Contains("Amount Due", details.Text); Assert.Contains("Recommended Fee", details.Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); + Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text); + Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text); // Pay partial amount await Task.Delay(200); @@ -218,6 +238,14 @@ namespace BTCPayServer.Tests Assert.Contains("&lightning=LNBCRT", qrValue); s.Driver.FindElement(By.Id("PayByLNURL")); + // Check details + s.Driver.ToggleCollapse("PaymentDetails"); + Assert.Contains("1 BTC = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); + Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text); + Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text); + // Switch to amount displayed in sats s.GoToHome(); s.GoToStore(StoreNavPages.CheckoutAppearance); @@ -227,7 +255,15 @@ namespace BTCPayServer.Tests s.GoToInvoiceCheckout(invoiceId); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text); - + + // Check details + s.Driver.ToggleCollapse("PaymentDetails"); + Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); + Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text); + Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text); + // BIP21 with LN as default payment method s.GoToHome(); invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike"); @@ -238,6 +274,14 @@ namespace BTCPayServer.Tests Assert.StartsWith("bitcoin:", payUrl); Assert.Contains("&lightning=lnbcrt", payUrl); s.Driver.FindElement(By.Id("PayByLNURL")); + + // Check details + s.Driver.ToggleCollapse("PaymentDetails"); + Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); + Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text); + Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text); // Ensure LNURL is enabled s.GoToHome(); @@ -263,6 +307,14 @@ namespace BTCPayServer.Tests Assert.StartsWith("lnurl", copyAddressLightning); Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue); s.Driver.FindElement(By.Id("PayByLNURL")); + + // Check details + s.Driver.ToggleCollapse("PaymentDetails"); + Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat")); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue")); + s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice")); // Expiry message should not show amount for top-up invoice expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds")); diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 03fe2c645..d28a6e11e 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -51,7 +51,6 @@ namespace BTCPayServer.Tests { public FastTests(ITestOutputHelper helper) : base(helper) { - } class DockerImage { @@ -600,15 +599,16 @@ namespace BTCPayServer.Tests [Fact] public void RoundupCurrenciesCorrectly() { + DisplayFormatter displayFormatter = new (CurrencyNameTable.Instance); foreach (var test in new[] { - (0.0005m, "$0.0005 (USD)", "USD"), (0.001m, "$0.001 (USD)", "USD"), (0.01m, "$0.01 (USD)", "USD"), - (0.1m, "$0.10 (USD)", "USD"), (0.1m, "0,10 € (EUR)", "EUR"), (1000m, "¥1,000 (JPY)", "JPY"), - (1000.0001m, "₹ 1,000.00 (INR)", "INR"), - (0.0m, "$0.00 (USD)", "USD") + (0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"), + (0.1m, "0.10 USD", "USD"), (0.1m, "0,10 EUR", "EUR"), (1000m, "1,000 JPY", "JPY"), + (1000.0001m, "1,000.00 INR", "INR"), + (0.0m, "0.00 USD", "USD") }) { - var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3); + var actual = displayFormatter.Currency(test.Item1, test.Item3); actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well Assert.Equal(test.Item2, actual); } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 9b84755c0..1ce647e25 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -578,8 +578,7 @@ namespace BTCPayServer.Tests Assert.DoesNotContain("invoice-processing", s.Driver.PageSource); }); - Assert.Contains(s.Server.PayTester.GetService().DisplayFormatCurrency(100, "USD"), - s.Driver.PageSource); + Assert.Contains("100.00 USD", s.Driver.PageSource); Assert.Contains(i, s.Driver.PageSource); s.GoToInvoices(s.StoreId); diff --git a/BTCPayServer/Components/StoreRecentInvoices/Default.cshtml b/BTCPayServer/Components/StoreRecentInvoices/Default.cshtml index 8a7f33b54..b0fd7147a 100644 --- a/BTCPayServer/Components/StoreRecentInvoices/Default.cshtml +++ b/BTCPayServer/Components/StoreRecentInvoices/Default.cshtml @@ -1,6 +1,8 @@ @using BTCPayServer.Abstractions.Extensions @using BTCPayServer.Client.Models +@using BTCPayServer.Services @using BTCPayServer.Services.Invoices +@inject DisplayFormatter DisplayFormatter @model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
@@ -63,7 +65,7 @@ } - @invoice.AmountCurrency + @DisplayFormatter.Currency(invoice.Amount, invoice.Currency) } diff --git a/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoiceViewModel.cs b/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoiceViewModel.cs index a3f4eb1da..d32d02cf2 100644 --- a/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoiceViewModel.cs +++ b/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoiceViewModel.cs @@ -7,7 +7,8 @@ public class StoreRecentInvoiceViewModel { public string InvoiceId { get; set; } public string OrderId { get; set; } - public string AmountCurrency { get; set; } + public decimal Amount { get; set; } + public string Currency { get; set; } public InvoiceState Status { get; set; } public DateTimeOffset Date { get; set; } public bool HasRefund { get; set; } diff --git a/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs b/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs index 6a2432c38..f5738ca90 100644 --- a/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs +++ b/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs @@ -61,7 +61,8 @@ public class StoreRecentInvoices : ViewComponent HasRefund = invoice.Refunds.Any(), InvoiceId = invoice.Id, OrderId = invoice.Metadata.OrderId ?? string.Empty, - AmountCurrency = _currencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency), + Amount = invoice.Price, + Currency = invoice.Currency }).ToList(); return View(vm); diff --git a/BTCPayServer/Components/StoreRecentTransactions/Default.cshtml b/BTCPayServer/Components/StoreRecentTransactions/Default.cshtml index e9888fd0c..a2a90f89d 100644 --- a/BTCPayServer/Components/StoreRecentTransactions/Default.cshtml +++ b/BTCPayServer/Components/StoreRecentTransactions/Default.cshtml @@ -1,4 +1,6 @@ @using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Services +@inject DisplayFormatter DisplayFormatter @model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
@@ -49,11 +51,11 @@ @if (tx.Positive) { - @tx.Balance + @DisplayFormatter.Currency(tx.Balance, tx.Currency) } else { - @tx.Balance + @DisplayFormatter.Currency(tx.Balance, tx.Currency) } } diff --git a/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactionViewModel.cs b/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactionViewModel.cs index fb0ac1e88..aea759545 100644 --- a/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactionViewModel.cs +++ b/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactionViewModel.cs @@ -5,6 +5,7 @@ namespace BTCPayServer.Components.StoreRecentTransactions; public class StoreRecentTransactionViewModel { public string Id { get; set; } + public string Currency { get; set; } public string Balance { get; set; } public bool Positive { get; set; } public bool IsConfirmed { get; set; } diff --git a/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactions.cs b/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactions.cs index 4f03736da..1aebb182e 100644 --- a/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactions.cs +++ b/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactions.cs @@ -58,6 +58,7 @@ public class StoreRecentTransactions : ViewComponent Id = tx.TransactionId.ToString(), Positive = tx.BalanceChange.GetValue(network) >= 0, Balance = tx.BalanceChange.ShowMoney(network), + Currency = vm.CryptoCode, IsConfirmed = tx.Confirmations != 0, Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, tx.TransactionId.ToString()), Timestamp = tx.SeenAt diff --git a/BTCPayServer/Controllers/UICustodianAccountsController.cs b/BTCPayServer/Controllers/UICustodianAccountsController.cs index 58554c24a..845acae17 100644 --- a/BTCPayServer/Controllers/UICustodianAccountsController.cs +++ b/BTCPayServer/Controllers/UICustodianAccountsController.cs @@ -13,6 +13,7 @@ using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Models.CustodianAccountViewModels; using BTCPayServer.Payments; +using BTCPayServer.Services; using BTCPayServer.Services.Custodian.Client; using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.Authorization; @@ -32,13 +33,13 @@ namespace BTCPayServer.Controllers { private readonly IEnumerable _custodianRegistry; private readonly CustodianAccountRepository _custodianAccountRepository; - private readonly CurrencyNameTable _currencyNameTable; + private readonly DisplayFormatter _displayFormatter; private readonly BTCPayServerClient _btcPayServerClient; private readonly BTCPayNetworkProvider _networkProvider; private readonly LinkGenerator _linkGenerator; public UICustodianAccountsController( - CurrencyNameTable currencyNameTable, + DisplayFormatter displayFormatter, UserManager userManager, CustodianAccountRepository custodianAccountRepository, IEnumerable custodianRegistry, @@ -47,7 +48,7 @@ namespace BTCPayServer.Controllers LinkGenerator linkGenerator ) { - _currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); + _displayFormatter = displayFormatter; _custodianAccountRepository = custodianAccountRepository; _custodianRegistry = custodianRegistry; _btcPayServerClient = btcPayServerClient; @@ -144,7 +145,7 @@ namespace BTCPayServer.Controllers if (asset.Equals(defaultCurrency)) { assetBalance.FormattedFiatValue = - _currencyNameTable.DisplayFormatCurrency(pair.Value.Qty, defaultCurrency); + _displayFormatter.Currency(pair.Value.Qty, defaultCurrency); assetBalance.FiatValue = pair.Value.Qty; } else @@ -156,11 +157,11 @@ namespace BTCPayServer.Controllers assetBalance.Bid = quote.Bid; assetBalance.Ask = quote.Ask; assetBalance.FormattedBid = - _currencyNameTable.DisplayFormatCurrency(quote.Bid, quote.FromAsset); + _displayFormatter.Currency(quote.Bid, quote.FromAsset); assetBalance.FormattedAsk = - _currencyNameTable.DisplayFormatCurrency(quote.Ask, quote.FromAsset); + _displayFormatter.Currency(quote.Ask, quote.FromAsset); assetBalance.FormattedFiatValue = - _currencyNameTable.DisplayFormatCurrency(pair.Value.Qty * quote.Bid, + _displayFormatter.Currency(pair.Value.Qty * quote.Bid, defaultCurrency); assetBalance.FiatValue = pair.Value.Qty * quote.Bid; } diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs index 7de0b3736..ca7a61f08 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs @@ -20,6 +20,7 @@ using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Models.PaymentRequestViewModels; using BTCPayServer.Payments; using BTCPayServer.Rating; +using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices.Export; @@ -137,10 +138,10 @@ namespace BTCPayServer.Controllers CreatedDate = invoice.InvoiceTime, ExpirationDate = invoice.ExpirationTime, MonitoringDate = invoice.MonitoringExpiration, - Fiat = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency), + Fiat = _displayFormatter.Currency(invoice.Price, invoice.Currency), TaxIncluded = invoice.Metadata.TaxIncluded is null ? null - : _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency), + : _displayFormatter.Currency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency), NotificationUrl = invoice.NotificationURL?.AbsoluteUri, RedirectUrl = invoice.RedirectURL?.AbsoluteUri, TypedMetadata = invoice.Metadata, @@ -229,8 +230,8 @@ namespace BTCPayServer.Controllers Amount = amount, Paid = paid, ReceivedDate = paymentEntity.ReceivedTime.DateTime, - PaidFormatted = _CurrencyNameTable.FormatCurrency(paid, i.Currency), - RateFormatted = _CurrencyNameTable.FormatCurrency(rate, i.Currency), + PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol), + RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol), PaymentMethod = paymentMethodId.ToPrettyString(), Link = link, Id = txId, @@ -354,8 +355,7 @@ namespace BTCPayServer.Controllers var cryptoPaid = paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC); var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility); model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethodDivisibility); - model.RateThenText = - _CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode); + model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodId.CryptoCode); rules = store.GetStoreBlob().GetRateRules(_NetworkProvider); rateResult = await _RateProvider.FetchRate( new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules, @@ -369,13 +369,12 @@ namespace BTCPayServer.Controllers } model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility); - model.CurrentRateText = - _CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode); + model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodId.CryptoCode); model.FiatAmount = paidCurrency; } model.CustomAmount = model.FiatAmount; model.CustomCurrency = invoice.Currency; - model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency); + model.FiatText = _displayFormatter.Currency(model.FiatAmount, invoice.Currency); return View("_RefundModal", model); case RefundSteps.SelectRate: @@ -477,7 +476,6 @@ namespace BTCPayServer.Controllers private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice) { - var overpaid = false; var model = new InvoiceDetailsModel { @@ -500,15 +498,11 @@ namespace BTCPayServer.Controllers { PaymentMethodId = paymentMethodId, PaymentMethod = paymentMethodId.ToPrettyString(), - Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), - paymentMethodId.CryptoCode), - Paid = _CurrencyNameTable.DisplayFormatCurrency( - accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), - paymentMethodId.CryptoCode), - Overpaid = _CurrencyNameTable.DisplayFormatCurrency( - overpaidAmount, paymentMethodId.CryptoCode), + Due = _displayFormatter.Currency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode), + Paid = _displayFormatter.Currency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode), + Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode), Address = data.GetPaymentMethodDetails().GetPaymentDestination(), - Rate = ExchangeRate(data), + Rate = ExchangeRate(data.GetId().CryptoCode, data), PaymentMethodRaw = data }; }).ToList() @@ -794,10 +788,10 @@ namespace BTCPayServer.Controllers CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)), BtcAddress = paymentMethodDetails.GetPaymentDestination(), BtcDue = accounting.Due.ShowMoney(divisibility), + BtcPaid = accounting.Paid.ShowMoney(divisibility), InvoiceCurrency = invoice.Currency, OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility), IsUnsetTopUp = invoice.IsUnsetTopUp(), - OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice), CustomerEmail = invoice.RefundMail, RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail, ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), @@ -805,7 +799,7 @@ namespace BTCPayServer.Controllers MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes, ItemDesc = invoice.Metadata.ItemDesc, - Rate = ExchangeRate(paymentMethod), + Rate = ExchangeRate(network.CryptoCode, paymentMethod, DisplayFormatter.CurrencyFormat.Symbol), MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? receiptUrl ?? "/", ReceiptLink = receiptUrl, RedirectAutomatically = invoice.RedirectAutomatically, @@ -818,7 +812,6 @@ namespace BTCPayServer.Controllers NetworkFeeMode.Never => 0, _ => throw new NotImplementedException() }, - BtcPaid = accounting.Paid.ShowMoney(divisibility), #pragma warning disable CS0618 // Type or member is obsolete Status = invoice.StatusString, #pragma warning restore CS0618 // Type or member is obsolete @@ -865,23 +858,33 @@ namespace BTCPayServer.Controllers model.UISettings = paymentMethodHandler.GetCheckoutUISettings(); model.PaymentMethodId = paymentMethodId.ToString(); model.PaymentType = paymentMethodId.PaymentType.ToString(); + model.OrderAmountFiat = OrderAmountFromInvoice(model.CryptoCode, invoice, DisplayFormatter.CurrencyFormat.Symbol); var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds); model.TimeLeft = expiration.PrettyPrint(); return model; } - private string? OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity) + private string? OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code) { + var currency = invoiceEntity.Currency; + var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats" + // if invoice source currency is the same as currently display currency, no need for "order amount from invoice" - if (cryptoCode == invoiceEntity.Currency) + if (crypto == currency || (crypto == "SATS" && currency == "BTC") || (crypto == "BTC" && currency == "SATS")) return null; - return _CurrencyNameTable.DisplayFormatCurrency(invoiceEntity.Price, invoiceEntity.Currency); + return _displayFormatter.Currency(invoiceEntity.Price, currency, format); } - private string ExchangeRate(PaymentMethod paymentMethod) + + private string? ExchangeRate(string cryptoCode, PaymentMethod paymentMethod, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code) { - string currency = paymentMethod.ParentEntity.Currency; - return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency); + var currency = paymentMethod.ParentEntity.Currency; + var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats" + + if (crypto == currency || (crypto == "SATS" && currency == "BTC") || (crypto == "BTC" && currency == "SATS")) + return null; + + return _displayFormatter.Currency(paymentMethod.Rate, currency, format); } [HttpGet("i/{invoiceId}/status")] @@ -1004,7 +1007,8 @@ namespace BTCPayServer.Controllers InvoiceId = invoice.Id, OrderId = invoice.Metadata.OrderId ?? string.Empty, RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty, - AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency), + Amount = invoice.Price, + Currency = invoice.Currency, CanMarkInvalid = state.CanMarkInvalid(), CanMarkSettled = state.CanMarkComplete(), Details = InvoicePopulatePayments(invoice), diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs index 06dcd691c..296ef295f 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.cs @@ -45,6 +45,7 @@ namespace BTCPayServer.Controllers readonly StoreRepository _StoreRepository; readonly UserManager _UserManager; private readonly CurrencyNameTable _CurrencyNameTable; + private readonly DisplayFormatter _displayFormatter; readonly EventAggregator _EventAggregator; readonly BTCPayNetworkProvider _NetworkProvider; private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; @@ -61,6 +62,7 @@ namespace BTCPayServer.Controllers public UIInvoiceController( InvoiceRepository invoiceRepository, WalletRepository walletRepository, + DisplayFormatter displayFormatter, CurrencyNameTable currencyNameTable, UserManager userManager, RateFetcher rateProvider, @@ -78,6 +80,7 @@ namespace BTCPayServer.Controllers InvoiceActivator invoiceActivator, LinkGenerator linkGenerator) { + _displayFormatter = displayFormatter; _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); diff --git a/BTCPayServer/Controllers/UIPaymentRequestController.cs b/BTCPayServer/Controllers/UIPaymentRequestController.cs index 89a6c7212..db5ee2e61 100644 --- a/BTCPayServer/Controllers/UIPaymentRequestController.cs +++ b/BTCPayServer/Controllers/UIPaymentRequestController.cs @@ -14,6 +14,7 @@ using BTCPayServer.Forms.Models; using BTCPayServer.Models; using BTCPayServer.Models.PaymentRequestViewModels; using BTCPayServer.PaymentRequest; +using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.Rates; @@ -37,6 +38,7 @@ namespace BTCPayServer.Controllers private readonly PaymentRequestService _PaymentRequestService; private readonly EventAggregator _EventAggregator; private readonly CurrencyNameTable _Currencies; + private readonly DisplayFormatter _displayFormatter; private readonly InvoiceRepository _InvoiceRepository; private readonly StoreRepository _storeRepository; @@ -50,6 +52,7 @@ namespace BTCPayServer.Controllers PaymentRequestService paymentRequestService, EventAggregator eventAggregator, CurrencyNameTable currencies, + DisplayFormatter displayFormatter, StoreRepository storeRepository, InvoiceRepository invoiceRepository, FormComponentProviders formProviders, @@ -61,6 +64,7 @@ namespace BTCPayServer.Controllers _PaymentRequestService = paymentRequestService; _EventAggregator = eventAggregator; _Currencies = currencies; + _displayFormatter = displayFormatter; _storeRepository = storeRepository; _InvoiceRepository = invoiceRepository; FormProviders = formProviders; @@ -89,7 +93,7 @@ namespace BTCPayServer.Controllers var blob = data.GetBlob(); return new ViewPaymentRequestViewModel(data) { - AmountFormatted = _Currencies.DisplayFormatCurrency(blob.Amount, blob.Currency) + AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency) }; }).ToList(); diff --git a/BTCPayServer/Controllers/UIPullPaymentController.cs b/BTCPayServer/Controllers/UIPullPaymentController.cs index 4ef8ce47b..5bc027ae9 100644 --- a/BTCPayServer/Controllers/UIPullPaymentController.cs +++ b/BTCPayServer/Controllers/UIPullPaymentController.cs @@ -27,6 +27,7 @@ namespace BTCPayServer.Controllers { private readonly ApplicationDbContextFactory _dbContextFactory; private readonly CurrencyNameTable _currencyNameTable; + private readonly DisplayFormatter _displayFormatter; private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings; private readonly IEnumerable _payoutHandlers; @@ -34,6 +35,7 @@ namespace BTCPayServer.Controllers public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory, CurrencyNameTable currencyNameTable, + DisplayFormatter displayFormatter, PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkJsonSerializerSettings serializerSettings, IEnumerable payoutHandlers, @@ -41,6 +43,7 @@ namespace BTCPayServer.Controllers { _dbContextFactory = dbContextFactory; _currencyNameTable = currencyNameTable; + _displayFormatter = displayFormatter; _pullPaymentHostedService = pullPaymentHostedService; _serializerSettings = serializerSettings; _payoutHandlers = payoutHandlers; @@ -79,12 +82,9 @@ namespace BTCPayServer.Controllers { BrandColor = storeBlob.BrandColor, CssFileId = storeBlob.CssFileId, - AmountFormatted = _currencyNameTable.FormatCurrency(blob.Limit, blob.Currency), AmountCollected = totalPaid, - AmountCollectedFormatted = _currencyNameTable.FormatCurrency(totalPaid, blob.Currency), AmountDue = amountDue, ClaimedAmount = amountDue, - AmountDueFormatted = _currencyNameTable.FormatCurrency(amountDue, blob.Currency), CurrencyData = cd, StartDate = pp.StartDate, LastRefreshed = DateTime.UtcNow, @@ -93,7 +93,6 @@ namespace BTCPayServer.Controllers { Id = entity.Entity.Id, Amount = entity.Blob.Amount, - AmountFormatted = _currencyNameTable.FormatCurrency(entity.Blob.Amount, blob.Currency), Currency = blob.Currency, Status = entity.Entity.State, Destination = entity.Blob.Destination, @@ -200,8 +199,8 @@ namespace BTCPayServer.Controllers var amount = ppBlob.Currency == "SATS" ? new Money(vm.ClaimedAmount, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) : vm.ClaimedAmount; if (destination.destination.Amount != null && amount != destination.destination.Amount) { - var implied = _currencyNameTable.DisplayFormatCurrency(destination.destination.Amount.Value, paymentMethodId.CryptoCode); - var provided = _currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency); + var implied = _displayFormatter.Currency(destination.destination.Amount.Value, paymentMethodId.CryptoCode, DisplayFormatter.CurrencyFormat.Symbol); + var provided = _displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol); ModelState.AddModelError(nameof(vm.ClaimedAmount), $"Amount implied in destination ({implied}) does not match the payout amount provided ({provided})."); } @@ -235,7 +234,7 @@ namespace BTCPayServer.Controllers TempData.SetStatusMessageModel(new StatusMessageModel { - Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.", + Message = $"Your claim request of {_displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.", Severity = StatusMessageModel.StatusSeverity.Success }); diff --git a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs index 8ad6ebb64..9041a0b38 100644 --- a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs +++ b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs @@ -34,6 +34,7 @@ namespace BTCPayServer.Controllers private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly IEnumerable _payoutHandlers; private readonly CurrencyNameTable _currencyNameTable; + private readonly DisplayFormatter _displayFormatter; private readonly PullPaymentHostedService _pullPaymentService; private readonly ApplicationDbContextFactory _dbContextFactory; private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings; @@ -49,6 +50,7 @@ namespace BTCPayServer.Controllers public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider, IEnumerable payoutHandlers, CurrencyNameTable currencyNameTable, + DisplayFormatter displayFormatter, PullPaymentHostedService pullPaymentHostedService, ApplicationDbContextFactory dbContextFactory, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings) @@ -56,6 +58,7 @@ namespace BTCPayServer.Controllers _btcPayNetworkProvider = btcPayNetworkProvider; _payoutHandlers = payoutHandlers; _currencyNameTable = currencyNameTable; + _displayFormatter = displayFormatter; _pullPaymentService = pullPaymentHostedService; _dbContextFactory = dbContextFactory; _jsonSerializerSettings = jsonSerializerSettings; @@ -532,7 +535,7 @@ namespace BTCPayServer.Controllers PullPaymentName = ppBlob?.Name ?? item.PullPayment?.Id, Date = item.Payout.Date, PayoutId = item.Payout.Id, - Amount = _currencyNameTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode), + Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode), Destination = payoutBlob.Destination }; var handler = _payoutHandlers diff --git a/BTCPayServer/HostedServices/PullPaymentHostedService.cs b/BTCPayServer/HostedServices/PullPaymentHostedService.cs index b4a22a596..7cecd49d5 100644 --- a/BTCPayServer/HostedServices/PullPaymentHostedService.cs +++ b/BTCPayServer/HostedServices/PullPaymentHostedService.cs @@ -251,6 +251,7 @@ namespace BTCPayServer.HostedServices IEnumerable payoutHandlers, ILogger logger, Logs logs, + DisplayFormatter displayFormatter, CurrencyNameTable currencyNameTable) : base(logs) { _dbContextFactory = dbContextFactory; @@ -262,6 +263,7 @@ namespace BTCPayServer.HostedServices _payoutHandlers = payoutHandlers; _logger = logger; _currencyNameTable = currencyNameTable; + _displayFormatter = displayFormatter; } Channel _Channel; @@ -274,6 +276,7 @@ namespace BTCPayServer.HostedServices private readonly IEnumerable _payoutHandlers; private readonly ILogger _logger; private readonly CurrencyNameTable _currencyNameTable; + private readonly DisplayFormatter _displayFormatter; private readonly CompositeDisposable _subscriptions = new CompositeDisposable(); internal override Task[] InitializeTasks() @@ -754,7 +757,7 @@ namespace BTCPayServer.HostedServices Completed = totalCompleted, CompletedFormatted = totalCompleted.ToString("C", nfi), Limit = ppBlob.Limit.RoundToSignificant(currencyData.Divisibility), - LimitFormatted = _currencyNameTable.DisplayFormatCurrency(ppBlob.Limit, ppBlob.Currency), + LimitFormatted = _displayFormatter.Currency(ppBlob.Limit, ppBlob.Currency), ResetIn = period?.End is { } nr ? ZeroIfNegative(nr - now).TimeString() : null, EndIn = pp.EndDate is { } end ? ZeroIfNegative(end - now).TimeString() : null, }; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index ee37e4528..f40aed70f 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -280,6 +280,7 @@ namespace BTCPayServer.Hosting services.AddTransient(); services.AddSingleton(); services.TryAddTransient(); + services.TryAddTransient(); services.TryAddSingleton(o => { diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index e2f0ee6e8..3bafcbdbb 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -28,7 +28,8 @@ namespace BTCPayServer.Models.InvoicingModels public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid; public bool ShowCheckout { get; set; } public string ExceptionStatus { get; set; } - public string AmountCurrency { get; set; } + public decimal Amount { get; set; } + public string Currency { get; set; } public InvoiceDetailsModel Details { get; set; } public bool HasRefund { get; set; } diff --git a/BTCPayServer/Models/ViewPullPaymentModel.cs b/BTCPayServer/Models/ViewPullPaymentModel.cs index 9d4a0b2b7..09a811d62 100644 --- a/BTCPayServer/Models/ViewPullPaymentModel.cs +++ b/BTCPayServer/Models/ViewPullPaymentModel.cs @@ -82,7 +82,6 @@ namespace BTCPayServer.Models public decimal ClaimedAmount { get; set; } public decimal MinimumClaim { get; set; } public string Destination { get; set; } - public string AmountDueFormatted { get; set; } public decimal Amount { get; set; } public string Id { get; set; } public string Currency { get; set; } @@ -97,8 +96,6 @@ namespace BTCPayServer.Models public DateTimeOffset StartDate { get; set; } public DateTime LastRefreshed { get; set; } public CurrencyData CurrencyData { get; set; } - public string AmountCollectedFormatted { get; set; } - public string AmountFormatted { get; set; } public bool Archived { get; set; } public bool AutoApprove { get; set; } diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index 8c69f8ef1..feed74464 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -5,6 +5,7 @@ using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Models.PaymentRequestViewModels; using BTCPayServer.Payments; +using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.PaymentRequests; @@ -20,17 +21,20 @@ namespace BTCPayServer.PaymentRequest private readonly BTCPayNetworkProvider _BtcPayNetworkProvider; private readonly AppService _AppService; private readonly CurrencyNameTable _currencies; + private readonly DisplayFormatter _displayFormatter; public PaymentRequestService( PaymentRequestRepository paymentRequestRepository, BTCPayNetworkProvider btcPayNetworkProvider, AppService appService, + DisplayFormatter displayFormatter, CurrencyNameTable currencies) { _PaymentRequestRepository = paymentRequestRepository; _BtcPayNetworkProvider = btcPayNetworkProvider; _AppService = appService; _currencies = currencies; + _displayFormatter = displayFormatter; } public async Task UpdatePaymentRequestStateIfNeeded(string id) @@ -90,11 +94,11 @@ namespace BTCPayServer.PaymentRequest return new ViewPaymentRequestViewModel(pr) { Archived = pr.Archived, - AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency), + AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), AmountCollected = paymentStats.TotalCurrency, - AmountCollectedFormatted = _currencies.FormatCurrency(paymentStats.TotalCurrency, blob.Currency), + AmountCollectedFormatted = _displayFormatter.Currency(paymentStats.TotalCurrency, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), AmountDue = amountDue, - AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency), + AmountDueFormatted = _displayFormatter.Currency(amountDue, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), CurrencyData = _currencies.GetCurrencyData(blob.Currency, true), LastUpdated = DateTime.UtcNow, FormId = blob.FormId, @@ -128,8 +132,8 @@ namespace BTCPayServer.PaymentRequest Amount = amount, Paid = paid, ReceivedDate = paymentEntity.ReceivedTime.DateTime, - PaidFormatted = _currencies.FormatCurrency(paid, blob.Currency), - RateFormatted = _currencies.FormatCurrency(rate, blob.Currency), + PaidFormatted = _displayFormatter.Currency(paid, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), + RateFormatted = _displayFormatter.Currency(rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), PaymentMethod = paymentMethodId.ToPrettyString(), Link = link, Id = txId, @@ -147,7 +151,7 @@ namespace BTCPayServer.PaymentRequest { Id = entity.Id, Amount = entity.Price, - AmountFormatted = _currencies.FormatCurrency(entity.Price, blob.Currency), + AmountFormatted = _displayFormatter.Currency(entity.Price, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), Currency = entity.Currency, ExpiryDate = entity.ExpirationTime.DateTime, State = state, diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index abf00fda9..12866f044 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -10,7 +10,6 @@ using BTCPayServer.Models; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Rates; using NBitcoin; using NBitcoin.DataEncoders; using NBXplorer.Models; @@ -24,14 +23,14 @@ namespace BTCPayServer.Payments.Bitcoin private readonly BTCPayNetworkProvider _networkProvider; private readonly IFeeProviderFactory _FeeRateProviderFactory; private readonly NBXplorerDashboard _dashboard; - private readonly CurrencyNameTable _currencyNameTable; + private readonly DisplayFormatter _displayFormatter; private readonly Services.Wallets.BTCPayWalletProvider _WalletProvider; private readonly Dictionary _bech32Prefix; public BitcoinLikePaymentHandler(ExplorerClientProvider provider, BTCPayNetworkProvider networkProvider, IFeeProviderFactory feeRateProviderFactory, - CurrencyNameTable currencyNameTable, + DisplayFormatter displayFormatter, NBXplorerDashboard dashboard, Services.Wallets.BTCPayWalletProvider walletProvider) { @@ -40,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin _FeeRateProviderFactory = feeRateProviderFactory; _dashboard = dashboard; _WalletProvider = walletProvider; - _currencyNameTable = currencyNameTable; + _displayFormatter = displayFormatter; _bech32Prefix = networkProvider.GetAll().OfType() .Where(network => network.NBitcoinNetwork?.Consensus?.SupportSegwit is true).ToDictionary(network => network.CryptoCode, @@ -144,7 +143,7 @@ namespace BTCPayServer.Payments.Bitcoin if (model.Activated && amountInSats) { - base.PreparePaymentModelForAmountInSats(model, paymentMethod, _currencyNameTable); + base.PreparePaymentModelForAmountInSats(model, paymentMethod, _displayFormatter); } } diff --git a/BTCPayServer/Payments/IPaymentMethodHandler.cs b/BTCPayServer/Payments/IPaymentMethodHandler.cs index 13304880c..76f754b20 100644 --- a/BTCPayServer/Payments/IPaymentMethodHandler.cs +++ b/BTCPayServer/Payments/IPaymentMethodHandler.cs @@ -6,6 +6,7 @@ using BTCPayServer.Data; using BTCPayServer.Logging; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Rating; +using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using NBitcoin; @@ -100,7 +101,7 @@ namespace BTCPayServer.Payments return null; } - public virtual void PreparePaymentModelForAmountInSats(PaymentModel model, IPaymentMethod paymentMethod, CurrencyNameTable currencyNameTable) + public virtual void PreparePaymentModelForAmountInSats(PaymentModel model, IPaymentMethod paymentMethod, DisplayFormatter displayFormatter) { var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name) { @@ -111,7 +112,9 @@ namespace BTCPayServer.Payments model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture); model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture); model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi); - model.Rate = currencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, model.InvoiceCurrency); + model.Rate = model.InvoiceCurrency is "BTC" or "SATS" + ? null + : displayFormatter.Currency(paymentMethod.Rate / 100_000_000, model.InvoiceCurrency, DisplayFormatter.CurrencyFormat.Symbol); } public Task CreatePaymentMethodDetails(InvoiceLogs logs, diff --git a/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentHandler.cs b/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentHandler.cs index 89592a278..9af7def36 100644 --- a/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentHandler.cs +++ b/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentHandler.cs @@ -9,8 +9,8 @@ using BTCPayServer.Lightning; using BTCPayServer.Logging; using BTCPayServer.Models; using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Services; using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Rates; using Microsoft.Extensions.Options; namespace BTCPayServer.Payments.Lightning @@ -18,17 +18,17 @@ namespace BTCPayServer.Payments.Lightning public class LNURLPayPaymentHandler : PaymentMethodHandlerBase { private readonly BTCPayNetworkProvider _networkProvider; - private readonly CurrencyNameTable _currencyNameTable; + private readonly DisplayFormatter _displayFormatter; private readonly LightningLikePaymentHandler _lightningLikePaymentHandler; public LNURLPayPaymentHandler( BTCPayNetworkProvider networkProvider, - CurrencyNameTable currencyNameTable, + DisplayFormatter displayFormatter, IOptions options, LightningLikePaymentHandler lightningLikePaymentHandler) { _networkProvider = networkProvider; - _currencyNameTable = currencyNameTable; + _displayFormatter = displayFormatter; _lightningLikePaymentHandler = lightningLikePaymentHandler; Options = options; } @@ -115,7 +115,7 @@ namespace BTCPayServer.Payments.Lightning if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC") { - base.PreparePaymentModelForAmountInSats(model, paymentMethod, _currencyNameTable); + base.PreparePaymentModelForAmountInSats(model, paymentMethod, _displayFormatter); } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 9c0c13b71..a33b8c9f1 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -14,7 +14,6 @@ using BTCPayServer.Models; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Rates; using Microsoft.Extensions.Options; using NBitcoin; @@ -27,21 +26,21 @@ namespace BTCPayServer.Payments.Lightning private readonly LightningClientFactoryService _lightningClientFactory; private readonly BTCPayNetworkProvider _networkProvider; private readonly SocketFactory _socketFactory; - private readonly CurrencyNameTable _currencyNameTable; + private readonly DisplayFormatter _displayFormatter; public LightningLikePaymentHandler( NBXplorerDashboard dashboard, LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider networkProvider, SocketFactory socketFactory, - CurrencyNameTable currencyNameTable, + DisplayFormatter displayFormatter, IOptions options) { _Dashboard = dashboard; _lightningClientFactory = lightningClientFactory; _networkProvider = networkProvider; _socketFactory = socketFactory; - _currencyNameTable = currencyNameTable; + _displayFormatter = displayFormatter; Options = options; } @@ -221,7 +220,7 @@ namespace BTCPayServer.Payments.Lightning if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC") { - base.PreparePaymentModelForAmountInSats(model, paymentMethod, _currencyNameTable); + base.PreparePaymentModelForAmountInSats(model, paymentMethod, _displayFormatter); } } public override string GetCryptoImage(PaymentMethodId paymentMethodId) diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index b943a9925..344025547 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -35,12 +35,14 @@ namespace BTCPayServer.Services.Apps readonly ApplicationDbContextFactory _ContextFactory; private readonly InvoiceRepository _InvoiceRepository; readonly CurrencyNameTable _Currencies; + private readonly DisplayFormatter _displayFormatter; private readonly StoreRepository _storeRepository; private readonly HtmlSanitizer _HtmlSanitizer; public CurrencyNameTable Currencies => _Currencies; public AppService(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository, CurrencyNameTable currencies, + DisplayFormatter displayFormatter, StoreRepository storeRepository, HtmlSanitizer htmlSanitizer) { @@ -49,6 +51,7 @@ namespace BTCPayServer.Services.Apps _Currencies = currencies; _storeRepository = storeRepository; _HtmlSanitizer = htmlSanitizer; + _displayFormatter = displayFormatter; } public async Task GetAppInfo(string appId) @@ -599,7 +602,7 @@ namespace BTCPayServer.Services.Apps if (pValue != null) { price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture); - price.Formatted = Currencies.FormatCurrency(pValue.Value.Value, currency); + price.Formatted = _displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol); } break; case "fixed": @@ -607,7 +610,7 @@ namespace BTCPayServer.Services.Apps case null: price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed; price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture); - price.Formatted = Currencies.FormatCurrency(pValue.Value.Value, currency); + price.Formatted = _displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol); break; } diff --git a/BTCPayServer/Services/DisplayFormatter.cs b/BTCPayServer/Services/DisplayFormatter.cs new file mode 100644 index 000000000..37abdc6c2 --- /dev/null +++ b/BTCPayServer/Services/DisplayFormatter.cs @@ -0,0 +1,57 @@ +using System; +using System.Globalization; +using BTCPayServer.Rating; +using BTCPayServer.Services.Rates; + +namespace BTCPayServer.Services; + +public class DisplayFormatter +{ + private readonly CurrencyNameTable _currencyNameTable; + + public DisplayFormatter(CurrencyNameTable currencyNameTable) + { + _currencyNameTable = currencyNameTable; + } + + public enum CurrencyFormat + { + Code, + Symbol, + CodeAndSymbol + } + + /// + /// Format a currency, rounded to significant divisibility + /// + /// The value + /// Currency code + /// The format, defaults to amount + code, e.g. 1.234,56 USD + /// Formatted amount and currency string + public string Currency(decimal value, string currency, CurrencyFormat format = CurrencyFormat.Code) + { + var provider = _currencyNameTable.GetNumberFormatInfo(currency, true); + var currencyData = _currencyNameTable.GetCurrencyData(currency, true); + var divisibility = currencyData.Divisibility; + value = value.RoundToSignificant(ref divisibility); + if (divisibility != provider.CurrencyDecimalDigits) + { + provider = (NumberFormatInfo)provider.Clone(); + provider.CurrencyDecimalDigits = divisibility; + } + var formatted = value.ToString("C", provider); + + return format switch + { + CurrencyFormat.Code => $"{formatted.Replace(provider.CurrencySymbol, "").Trim()} {currency}", + CurrencyFormat.Symbol => formatted, + CurrencyFormat.CodeAndSymbol => $"{formatted} ({currency})", + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null) + }; + } + + public string Currency(string value, string currency, CurrencyFormat format = CurrencyFormat.Code) + { + return Currency(decimal.Parse(value, CultureInfo.InvariantCulture), currency, format); + } +} diff --git a/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml index 3a7f2ff6d..5a718781b 100644 --- a/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml @@ -1,6 +1,8 @@ @using System.Globalization @using BTCPayServer.Payments @using BTCPayServer.Payments.Bitcoin +@using BTCPayServer.Services +@inject DisplayFormatter DisplayFormatter @model IEnumerable @{ @@ -78,7 +80,7 @@ @(payment.CryptoPaymentData.KeyPath?.ToString()?? "Unknown") @payment.DepositAddress - @payment.CryptoPaymentData.GetValue() + @DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto) @if (!string.IsNullOrEmpty(payment.AdditionalInformation)) {
(@payment.AdditionalInformation)
diff --git a/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml b/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml index 07ee61027..61ada3a97 100644 --- a/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml +++ b/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml @@ -183,37 +183,37 @@