diff --git a/BTCPayServer.Tests/RateRulesTest.cs b/BTCPayServer.Tests/RateRulesTest.cs index f12736855..09c26fc73 100644 --- a/BTCPayServer.Tests/RateRulesTest.cs +++ b/BTCPayServer.Tests/RateRulesTest.cs @@ -35,9 +35,9 @@ namespace BTCPayServer.Tests rules.ToString()); var tests = new[] { + (Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"), (Pair: "BTC_USD", Expected: "gdax(BTC_USD)"), (Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"), - (Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"), (Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"), (Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"), }; @@ -81,9 +81,9 @@ namespace BTCPayServer.Tests var tests2 = new[] { + (Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"), (Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"), (Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"), - (Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"), (Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"), (Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"), }; @@ -129,6 +129,14 @@ namespace BTCPayServer.Tests Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true)); Assert.Equal(( 1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value); //////// + + // Make sure kraken is not converted to CurrencyPair + builder = new StringBuilder(); + builder.AppendLine("BTC_USD = kraken(BTC_USD)"); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD")); + rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), 1000m); + Assert.True(rule2.Reevaluate()); } } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 5aee1d360..b5e3df28a 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -913,6 +913,11 @@ namespace BTCPayServer.Tests Assert.Single(invoice.CryptoInfo); Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); + Assert.True(invoice.PaymentCodes.ContainsKey("LTC")); + Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC")); + Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled); + Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC")); + Assert.True(invoice.PaymentTotals.ContainsKey("LTC")); var cashCow = tester.LTCExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); var firstPayment = Money.Coins(0.1m); @@ -1047,6 +1052,16 @@ namespace BTCPayServer.Tests Assert.Single(checkout.AvailableCryptos); Assert.Equal("BTC", checkout.CryptoCode); + Assert.Single(invoice.PaymentCodes); + Assert.Single(invoice.SupportedTransactionCurrencies); + Assert.Single(invoice.SupportedTransactionCurrencies); + Assert.Single(invoice.PaymentSubtotals); + Assert.Single(invoice.PaymentTotals); + Assert.True(invoice.PaymentCodes.ContainsKey("BTC")); + Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC")); + Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled); + Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC")); + Assert.True(invoice.PaymentTotals.ContainsKey("BTC")); ////////////////////// // Retry now with LTC enabled @@ -1095,6 +1110,18 @@ namespace BTCPayServer.Tests checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value; Assert.Equal(2, checkout.AvailableCryptos.Count); Assert.Equal("LTC", checkout.CryptoCode); + + + Assert.Equal(2, invoice.PaymentCodes.Count()); + Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count()); + Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count()); + Assert.Equal(2, invoice.PaymentSubtotals.Count()); + Assert.Equal(2, invoice.PaymentTotals.Count()); + Assert.True(invoice.PaymentCodes.ContainsKey("LTC")); + Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC")); + Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled); + Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC")); + Assert.True(invoice.PaymentTotals.ContainsKey("LTC")); } } diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index d2321ba1e..9f97dc2cf 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -89,7 +89,7 @@ services: - "bitcoin_datadir:/data" customer_lightningd: - image: nicolasdorier/clightning:0.0.0.14-dev + image: nicolasdorier/clightning:0.0.0.16-dev environment: EXPOSE_TCP: "true" LIGHTNINGD_OPT: | diff --git a/BTCPayServer/BTCPayNetworkProvider.Monacoin.cs b/BTCPayServer/BTCPayNetworkProvider.Monacoin.cs new file mode 100644 index 000000000..bb086132b --- /dev/null +++ b/BTCPayServer/BTCPayNetworkProvider.Monacoin.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services.Rates; +using NBitcoin; +using NBXplorer; + +namespace BTCPayServer +{ + public partial class BTCPayNetworkProvider + { + public void InitMonacoin() + { + var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA"); + Add(new BTCPayNetwork() + { + CryptoCode = nbxplorerNetwork.CryptoCode, + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}", + NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, + NBXplorerNetwork = nbxplorerNetwork, + UriScheme = "monacoin", + DefaultRateRules = new[] + { + "MONA_X = MONA_BTC * BTC_X", + "MONA_BTC = zaif(MONA_BTC)" + }, + CryptoImagePath = "imlegacy/monacoin.png", + LightningImagePath = "imlegacy/mona-lightning.svg", + DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'") + }); + } + } +} diff --git a/BTCPayServer/BTCPayNetworkProvider.cs b/BTCPayServer/BTCPayNetworkProvider.cs index 10bed3762..aec5a9d18 100644 --- a/BTCPayServer/BTCPayNetworkProvider.cs +++ b/BTCPayServer/BTCPayNetworkProvider.cs @@ -49,6 +49,7 @@ namespace BTCPayServer InitLitecoin(); InitDogecoin(); InitBitcoinGold(); + InitMonacoin(); InitPolis(); } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index c76e706bb..ca3aa0595 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.1 - 1.0.2.18 + 1.0.2.21 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -41,7 +41,7 @@ - + diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 546bf9b68..699fcd564 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -85,7 +85,7 @@ namespace BTCPayServer.Controllers { cryptoPayment.Address = onchainMethod.DepositAddress; } - cryptoPayment.Rate = FormatCurrency(data); + cryptoPayment.Rate = ExchangeRate(data); cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21; model.CryptoPayments.Add(cryptoPayment); } @@ -242,15 +242,16 @@ namespace BTCPayServer.Controllers CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri, CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri, BtcAddress = paymentMethodDetails.GetPaymentDestination(), - OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(), BtcDue = accounting.Due.ToString(), + OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(), + OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation), CustomerEmail = invoice.RefundMail, RequiresRefundEmail = storeBlob.RequiresRefundEmail, ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes, ItemDesc = invoice.ProductInformation.ItemDesc, - Rate = FormatCurrency(paymentMethod), + Rate = ExchangeRate(paymentMethod), MerchantRefLink = invoice.RedirectURL ?? "/", StoreName = store.StoreName, InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 : @@ -288,14 +289,23 @@ namespace BTCPayServer.Controllers return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath)); } - private string FormatCurrency(PaymentMethod paymentMethod) + private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation) + { + // if invoice source currency is the same as currently display currency, no need for "order amount from invoice" + if (cryptoCode == productInformation.Currency) + return null; + + return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable); + } + private string ExchangeRate(PaymentMethod paymentMethod) { string currency = paymentMethod.ParentEntity.ProductInformation.Currency; return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable); } + public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies) { - var provider = ((CultureInfo)currencies.GetCurrencyProvider(currency)).NumberFormat; + var provider = currencies.GetNumberFormatInfo(currency); var currencyData = currencies.GetCurrencyData(currency); var divisibility = currencyData.Divisibility; while (true) @@ -308,12 +318,16 @@ namespace BTCPayServer.Controllers } divisibility++; } - if(divisibility != provider.CurrencyDecimalDigits) + if (divisibility != provider.CurrencyDecimalDigits) { provider = (NumberFormatInfo)provider.Clone(); provider.CurrencyDecimalDigits = divisibility; } - return price.ToString("C", provider) + $" ({currency})"; + + if (currencyData.Crypto) + return price.ToString("C", provider); + else + return price.ToString("C", provider) + $" ({currency})"; } [HttpGet] diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 0a2d9a057..6605e11c8 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -198,7 +198,11 @@ namespace BTCPayServer.HostedServices PosData = dto.PosData, Price = dto.Price, Status = dto.Status, - BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) } + BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }, + PaymentSubtotals = dto.PaymentSubtotals, + PaymentTotals = dto.PaymentTotals, + AmountPaid = dto.AmountPaid, + ExchangeRates = dto.ExchangeRates }; // We keep backward compatibility with bitpay by passing BTC info to the notification diff --git a/BTCPayServer/Models/InvoiceResponse.cs b/BTCPayServer/Models/InvoiceResponse.cs index fd3bab95c..c7ad99dff 100644 --- a/BTCPayServer/Models/InvoiceResponse.cs +++ b/BTCPayServer/Models/InvoiceResponse.cs @@ -224,6 +224,29 @@ namespace BTCPayServer.Models { get; set; } + + [JsonProperty("paymentSubtotals")] + public Dictionary PaymentSubtotals { get; set; } + + [JsonProperty("paymentTotals")] + public Dictionary PaymentTotals { get; set; } + + [JsonProperty("amountPaid")] + public long AmountPaid { get; set; } + + [JsonProperty("minerFees")] + public long MinerFees { get; set; } + + [JsonProperty("exchangeRates")] + public Dictionary> ExchangeRates{ get; set; } + + [JsonProperty("supportedTransactionCurrencies")] + public Dictionary SupportedTransactionCurrencies { get; set; } + + [JsonProperty("addresses")] + public Dictionary Addresses { get; set; } + [JsonProperty("paymentCodes")] + public Dictionary PaymentCodes{get; set;} } public class Flags { @@ -233,4 +256,5 @@ namespace BTCPayServer.Models get; set; } } + } diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs index e3dcaf149..da28a8975 100644 --- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs +++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs @@ -37,6 +37,7 @@ namespace BTCPayServer.Models.InvoicingModels public string TimeLeft { get; set; } public string Rate { get; set; } public string OrderAmount { get; set; } + public string OrderAmountFiat { get; set; } public string InvoiceBitcoinUrl { get; set; } public string InvoiceBitcoinUrlQR { get; set; } public int TxCount { get; set; } diff --git a/BTCPayServer/Rating/RateRules.cs b/BTCPayServer/Rating/RateRules.cs index d4f72618f..e7fe02e0d 100644 --- a/BTCPayServer/Rating/RateRules.cs +++ b/BTCPayServer/Rating/RateRules.cs @@ -347,17 +347,23 @@ namespace BTCPayServer.Rating class FlattenExpressionRewriter : CSharpSyntaxRewriter { RateRules parent; + CurrencyPair pair; + int nested = 0; public FlattenExpressionRewriter(RateRules parent, CurrencyPair pair) { - Context.Push(pair); + this.pair = pair; this.parent = parent; } public ExchangeRates ExchangeRates = new ExchangeRates(); - public Stack Context { get; set; } = new Stack(); bool IsInvocation; public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) { + if (IsInvocation) + { + Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier); + return RateRules.CreateExpression($"ERR_INVALID_CURRENCY_PAIR({node.ToString()})"); + } IsInvocation = true; _ExchangeName = node.Expression.ToString(); var result = base.VisitInvocationExpression(node); @@ -365,18 +371,27 @@ namespace BTCPayServer.Rating return result; } + bool IsArgumentList; + public override SyntaxNode VisitArgumentList(ArgumentListSyntax node) + { + IsArgumentList = true; + var result = base.VisitArgumentList(node); + IsArgumentList = false; + return result; + } + string _ExchangeName = null; public List Errors = new List(); const int MaxNestedCount = 8; public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) { - if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair)) + if ( + (!IsInvocation || IsArgumentList) && + CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair)) { - var ctx = Context.Peek(); - - var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? ctx.Left : currentPair.Left, - right: currentPair.Right == "X" ? ctx.Right : currentPair.Right); + var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? pair.Left : currentPair.Left, + right: currentPair.Right == "X" ? pair.Right : currentPair.Right); if (IsInvocation) // eg. replace bittrex(BTC_X) to bittrex(BTC_USD) { ExchangeRates.Add(new ExchangeRate() { CurrencyPair = replacedPair, Exchange = _ExchangeName }); @@ -385,13 +400,13 @@ namespace BTCPayServer.Rating else // eg. replace BTC_X to BTC_USD, then replace by the expression for BTC_USD { var bestCandidate = parent.FindBestCandidate(replacedPair); - if (Context.Count > MaxNestedCount) + if (nested > MaxNestedCount) { Errors.Add(RateRulesErrors.TooMuchNestedCalls); return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})"); } - Context.Push(replacedPair); - var replaced = Visit(bestCandidate); + var innerFlatten = CreateNewContext(replacedPair); + var replaced = innerFlatten.Visit(bestCandidate); if (replaced is ExpressionSyntax expression) { var hasBinaryOps = new HasBinaryOperations(); @@ -401,7 +416,6 @@ namespace BTCPayServer.Rating replaced = SyntaxFactory.ParenthesizedExpression(expression); } } - Context.Pop(); if (Errors.Contains(RateRulesErrors.TooMuchNestedCalls)) { return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})"); @@ -411,6 +425,16 @@ namespace BTCPayServer.Rating } return base.VisitIdentifierName(node); } + + private FlattenExpressionRewriter CreateNewContext(CurrencyPair pair) + { + return new FlattenExpressionRewriter(parent, pair) + { + Errors = Errors, + nested = nested + 1, + ExchangeRates = ExchangeRates, + }; + } } private SyntaxNode expression; FlattenExpressionRewriter flatten; diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 1142e792d..27204ed0f 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -12,6 +12,7 @@ using NBXplorer.Models; using NBXplorer; using NBXplorer.DerivationStrategy; using BTCPayServer.Payments; +using NBitpayClient; namespace BTCPayServer.Services.Invoices { @@ -336,19 +337,35 @@ namespace BTCPayServer.Services.Invoices ExpirationTime = ExpirationTime, Status = Status, Currency = ProductInformation.Currency, - Flags = new Flags() { Refundable = Refundable } + Flags = new Flags() { Refundable = Refundable }, + + PaymentSubtotals = new Dictionary(), + PaymentTotals= new Dictionary(), + SupportedTransactionCurrencies = new Dictionary(), + Addresses = new Dictionary(), + PaymentCodes = new Dictionary(), + ExchangeRates = new Dictionary>() }; dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id; dto.CryptoInfo = new List(); foreach (var info in this.GetPaymentMethods(networkProvider)) { + var accounting = info.Calculate(); var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); - cryptoInfo.CryptoCode = info.GetId().CryptoCode; + var subtotalPrice = accounting.TotalDue - accounting.NetworkFee; + var cryptoCode = info.GetId().CryptoCode; + var address = info.GetPaymentMethodDetails()?.GetPaymentDestination(); + var exrates = new Dictionary + { + { ProductInformation.Currency, cryptoInfo.Rate } + }; + + cryptoInfo.CryptoCode = cryptoCode; cryptoInfo.PaymentType = info.GetId().PaymentType.ToString(); cryptoInfo.Rate = info.Rate; - cryptoInfo.Price = (accounting.TotalDue - accounting.NetworkFee).ToString(); + cryptoInfo.Price = subtotalPrice.ToString(); cryptoInfo.Due = accounting.Due.ToString(); cryptoInfo.Paid = accounting.Paid.ToString(); @@ -357,11 +374,9 @@ namespace BTCPayServer.Services.Invoices cryptoInfo.TxCount = accounting.TxCount; cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(); - cryptoInfo.Address = info.GetPaymentMethodDetails()?.GetPaymentDestination(); - cryptoInfo.ExRates = new Dictionary - { - { ProductInformation.Currency, cryptoInfo.Rate } - }; + cryptoInfo.Address = address; + + cryptoInfo.ExRates = exrates; var paymentId = info.GetId(); var scheme = info.Network.UriScheme; cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}"; @@ -396,10 +411,23 @@ namespace BTCPayServer.Services.Invoices dto.BTCDue = cryptoInfo.Due; dto.PaymentUrls = cryptoInfo.PaymentUrls; } + #pragma warning restore CS0618 dto.CryptoInfo.Add(cryptoInfo); + + dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls); + dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi); + dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi); + dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency() + { + Enabled = true + }); + dto.Addresses.Add(paymentId.ToString(), address); + dto.ExchangeRates.TryAdd(cryptoCode, exrates); } + //dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice + Populate(ProductInformation, dto); Populate(BuyerInformation, dto); diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 33253d0ec..61c79259e 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -112,7 +112,7 @@ namespace BTCPayServer.Services.Invoices invoice.StoreId = storeId; using (var context = _ContextFactory.CreateContext()) { - context.Invoices.Add(new InvoiceData() + context.Invoices.Add(new Data.InvoiceData() { StoreDataId = storeId, Id = invoice.Id, @@ -267,7 +267,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null) return; var invoiceEntity = ToObject(invoiceData.Blob, null); @@ -307,7 +307,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null) return; invoiceData.Status = status; @@ -320,7 +320,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData?.Status != "paid") return; invoiceData.Status = "invalid"; @@ -331,7 +331,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - IQueryable query = + IQueryable query = context .Invoices .Include(o => o.Payments) @@ -351,7 +351,7 @@ namespace BTCPayServer.Services.Invoices } } - private InvoiceEntity ToEntity(InvoiceData invoice) + private InvoiceEntity ToEntity(Data.InvoiceData invoice) { var entity = ToObject(invoice.Blob, null); #pragma warning disable CS0618 @@ -386,7 +386,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - IQueryable query = context + IQueryable query = context .Invoices .Include(o => o.Payments) .Include(o => o.RefundAddresses); diff --git a/BTCPayServer/Services/Rates/CurrencyNameTable.cs b/BTCPayServer/Services/Rates/CurrencyNameTable.cs index 01c6b57fa..d826ed746 100644 --- a/BTCPayServer/Services/Rates/CurrencyNameTable.cs +++ b/BTCPayServer/Services/Rates/CurrencyNameTable.cs @@ -31,6 +31,7 @@ namespace BTCPayServer.Services.Rates get; internal set; } + public bool Crypto { get; set; } } public class CurrencyNameTable { @@ -40,6 +41,14 @@ namespace BTCPayServer.Services.Rates } static Dictionary _CurrencyProviders = new Dictionary(); + + public NumberFormatInfo GetNumberFormatInfo(string currency) + { + var data = GetCurrencyProvider(currency); + if (data is NumberFormatInfo nfi) + return nfi; + return ((CultureInfo)data).NumberFormat; + } public IFormatProvider GetCurrencyProvider(string currency) { lock (_CurrencyProviders) @@ -54,7 +63,11 @@ namespace BTCPayServer.Services.Rates } catch { } } - AddCurrency(_CurrencyProviders, "BTC", 8, "BTC"); + + foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll()) + { + AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode); + } } return _CurrencyProviders.TryGet(currency); } @@ -106,6 +119,18 @@ namespace BTCPayServer.Services.Rates info.Symbol = splitted[3]; } } + + foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll()) + { + dico.TryAdd(network.CryptoCode, new CurrencyData() + { + Code = network.CryptoCode, + Divisibility = 8, + Name = network.CryptoCode, + Crypto = true + }); + } + return dico.Values.ToArray(); } diff --git a/BTCPayServer/Validation/UriAttribute.cs b/BTCPayServer/Validation/UriAttribute.cs index e6bb01608..0e9c75e08 100644 --- a/BTCPayServer/Validation/UriAttribute.cs +++ b/BTCPayServer/Validation/UriAttribute.cs @@ -9,8 +9,9 @@ namespace BTCPayServer.Validation { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { + var str = value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture); Uri uri; - bool valid = Uri.TryCreate(Convert.ToString(value, CultureInfo.InvariantCulture), UriKind.Absolute, out uri); + bool valid = string.IsNullOrWhiteSpace(str) || Uri.TryCreate(str, UriKind.Absolute, out uri); if (!valid) { diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index b50e3a707..aa596e9fc 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -73,7 +73,7 @@ {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} - + 1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }} @@ -87,6 +87,12 @@ {{$t("Order Amount")}} {{srvModel.orderAmount}} {{ srvModel.cryptoCode }} + + + + {{srvModel.orderAmountFiat}} + + {{$t("Network Cost")}} @@ -133,7 +139,7 @@ - + diff --git a/BTCPayServer/wwwroot/checkout/css/normalizer.css b/BTCPayServer/wwwroot/checkout/css/normalizer.css index bb566cb11..e8a539b3e 100644 --- a/BTCPayServer/wwwroot/checkout/css/normalizer.css +++ b/BTCPayServer/wwwroot/checkout/css/normalizer.css @@ -10328,6 +10328,7 @@ All mobile class names should be prefixed by m- */ .wrong-email .payment-tabs { pointer-events: none; margin-top: -2.95rem; + z-index: -1; margin-bottom: 1rem; } @@ -10412,10 +10413,6 @@ All mobile class names should be prefixed by m- */ transform: translateY(20px); } -.payment-tabs { - z-index: 1; -} - .single-item-order { z-index: 2; } @@ -11146,31 +11143,13 @@ language-selector { line-items { background: #FBFBFB; - height: 25px; - border-top: 0; + border-top: 1px solid rgba(238, 238, 238, 0.5); z-index: 2; - position: relative; - display: block; - overflow: hidden; - height: 0; - transition: height 250ms ease; + display: none; } - line-items.expanded { - height: 120px; - border-top: 1px solid rgba(238, 238, 238, 0.5); - } - - line-items.expanded.paid-over { - height: 295px; - } - - line-items.expanded.paid-partial-expired, line-items.expanded.paid-full { - height: 272px; - } - line-items .line-items { - padding: 1rem; + padding: 10px 1rem; color: #565D6E; } @@ -11198,6 +11177,10 @@ line-items { padding: 2px 0; } + line-items .line-items_fiatvalue { + margin-top: -5px; + } + line-items .line-items__item__label { flex-grow: 1; display: flex; diff --git a/BTCPayServer/wwwroot/checkout/js/core.js b/BTCPayServer/wwwroot/checkout/js/core.js index 89120ef3f..9feefed82 100644 --- a/BTCPayServer/wwwroot/checkout/js/core.js +++ b/BTCPayServer/wwwroot/checkout/js/core.js @@ -237,8 +237,12 @@ $(document).ready(function () { }); // Expand Line-Items + var lineItemsExpanded = false; $(".buyerTotalLine").click(function () { $("line-items").toggleClass("expanded"); + lineItemsExpanded ? $("line-items").slideUp() : $("line-items").slideDown(); + lineItemsExpanded = !lineItemsExpanded; + $(".buyerTotalLine").toggleClass("expanded"); $(".single-item-order__right__btc-price__chevron").toggleClass("expanded"); }); diff --git a/BTCPayServer/wwwroot/imlegacy/mona-lightning.svg b/BTCPayServer/wwwroot/imlegacy/mona-lightning.svg new file mode 100644 index 000000000..730f401d4 --- /dev/null +++ b/BTCPayServer/wwwroot/imlegacy/mona-lightning.svg @@ -0,0 +1,986 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BTCPayServer/wwwroot/imlegacy/monacoin.png b/BTCPayServer/wwwroot/imlegacy/monacoin.png new file mode 100644 index 000000000..0b0dab968 Binary files /dev/null and b/BTCPayServer/wwwroot/imlegacy/monacoin.png differ