diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 9d04f16d0..b717e0bfc 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.1 @@ -8,7 +8,7 @@ - + diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 9feaab1f5..3f0b16985 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -129,19 +129,19 @@ namespace BTCPayServer.Tests { Exchange = "coinaverage", CurrencyPair = CurrencyPair.Parse("BTC_USD"), - Value = 5000m + BidAsk = new BidAsk(5000m) }); coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() { Exchange = "coinaverage", CurrencyPair = CurrencyPair.Parse("BTC_CAD"), - Value = 4500m + BidAsk = new BidAsk(4500m) }); coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() { Exchange = "coinaverage", CurrencyPair = CurrencyPair.Parse("LTC_USD"), - Value = 500m + BidAsk = new BidAsk(500m) }); rateProvider.DirectProviders.Add("coinaverage", coinAverageMock); } diff --git a/BTCPayServer.Tests/Dockerfile b/BTCPayServer.Tests/Dockerfile index 7f0fab553..9724bd446 100644 --- a/BTCPayServer.Tests/Dockerfile +++ b/BTCPayServer.Tests/Dockerfile @@ -1,4 +1,4 @@ -FROM microsoft/dotnet:2.1.300-rc1-sdk-alpine3.7 +FROM microsoft/dotnet:2.1.300-sdk-alpine3.7 WORKDIR /app # caches restore result by copying csproj file separately COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj diff --git a/BTCPayServer.Tests/RateRulesTest.cs b/BTCPayServer.Tests/RateRulesTest.cs index 09c26fc73..53e89f842 100644 --- a/BTCPayServer.Tests/RateRulesTest.cs +++ b/BTCPayServer.Tests/RateRulesTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Text; using BTCPayServer.Rating; using Xunit; +using System.Globalization; namespace BTCPayServer.Tests { @@ -94,12 +95,12 @@ namespace BTCPayServer.Tests Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType().ToArray())); } var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD")); - rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), 5000); + rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m)); rule2.Reevaluate(); Assert.True(rule2.HasError); Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true)); Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false)); - rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 2000.4m); + rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m)); rule2.Reevaluate(); Assert.False(rule2.HasError); Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true)); @@ -116,7 +117,7 @@ namespace BTCPayServer.Tests rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")); Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString()); - rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m); + rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m)); Assert.True(rule2.Reevaluate()); Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true)); Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value); @@ -124,10 +125,10 @@ namespace BTCPayServer.Tests // Test inverse rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE")); Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString()); - rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m); + rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m)); Assert.True(rule2.Reevaluate()); 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); + Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value); //////// // Make sure kraken is not converted to CurrencyPair @@ -135,8 +136,44 @@ namespace BTCPayServer.Tests 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); + rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m)); Assert.True(rule2.Reevaluate()); + + // Make sure can handle pairs + 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"), new BidAsk(6000m, 6100m)); + Assert.True(rule2.Reevaluate()); + Assert.Equal("(6000, 6100)", rule2.ToString(true)); + Assert.Equal(6000m, rule2.Value.Value); + rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC")); + rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m)); + Assert.True(rule2.Reevaluate()); + Assert.Equal("1 / (6000, 6100)", rule2.ToString(true)); + Assert.Equal(1m / 6100m, rule2.Value.Value); + + // Make sure the inverse has more priority than X_X or CDNT_X + builder = new StringBuilder(); + builder.AppendLine("EUR_CDNT = 10"); + builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;"); + builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;"); + builder.AppendLine("X_X = coinaverage(X_X);"); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR")); + rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m)); + Assert.True(rule2.Reevaluate()); + Assert.Equal("1 / 10", rule2.ToString(false)); + + // Make sure an inverse can be solved on an exchange + builder = new StringBuilder(); + builder.AppendLine("X_X = coinaverage(X_X);"); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC")); + rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m)); + Assert.True(rule2.Reevaluate()); + Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true)); } } } diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 2c0ddf526..a47268c48 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -189,99 +189,6 @@ namespace BTCPayServer.Tests HttpClient _Http = new HttpClient(); - class MockHttpRequest : HttpRequest - { - Uri serverUri; - public MockHttpRequest(Uri serverUri) - { - this.serverUri = serverUri; - } - public override HttpContext HttpContext => throw new NotImplementedException(); - - public override string Method - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public override string Scheme - { - get => serverUri.Scheme; - set => throw new NotImplementedException(); - } - public override bool IsHttps - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public override HostString Host - { - get => new HostString(serverUri.Host, serverUri.Port); - set => throw new NotImplementedException(); - } - public override PathString PathBase - { - get => ""; - set => throw new NotImplementedException(); - } - public override PathString Path - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public override QueryString QueryString - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public override IQueryCollection Query - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public override string Protocol - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - public override IHeaderDictionary Headers => throw new NotImplementedException(); - - public override IRequestCookieCollection Cookies - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public override long? ContentLength - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public override string ContentType - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public override Stream Body - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - public override bool HasFormContentType => throw new NotImplementedException(); - - public override IFormCollection Form - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - public override Task ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - throw new NotImplementedException(); - } - } - - public BTCPayServerTester PayTester { get; set; diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 7b5e290c0..0b8872215 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1245,7 +1245,7 @@ namespace BTCPayServer.Tests Assert.Equal("orange", vmview.Items[1].Title); Assert.Equal(10.0m, vmview.Items[1].Price.Value); Assert.Equal("$5.00", vmview.Items[0].Price.Formatted); - Assert.IsType(apps.ViewPointOfSale(appId, 0, "orange").Result); + Assert.IsType(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result); var invoice = user.BitPay.GetInvoices().First(); Assert.Equal(10.00m, invoice.Price); Assert.Equal("CAD", invoice.Currency); @@ -1308,6 +1308,8 @@ namespace BTCPayServer.Tests var repo = tester.PayTester.GetService(); var ctx = tester.PayTester.GetService().CreateContext(); Assert.Equal(0, invoice.CryptoInfo[0].TxCount); + Assert.True(invoice.MinerFees.ContainsKey("BTC")); + Assert.Equal(100m, invoice.MinerFees["BTC"].SatoshiPerBytes); Eventually(() => { var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() @@ -1447,10 +1449,10 @@ namespace BTCPayServer.Tests var quadri = new QuadrigacxRateProvider(); var rates = quadri.GetRatesAsync().GetAwaiter().GetResult(); Assert.NotEmpty(rates); - Assert.NotEqual(0.0m, rates.First().Value); - Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value); - Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value); - Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value); + Assert.NotEqual(0.0m, rates.First().BidAsk.Bid); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid); Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD"))); } @@ -1475,7 +1477,7 @@ namespace BTCPayServer.Tests e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") || e.CurrencyPair == new CurrencyPair("BTC", "EUR") || e.CurrencyPair == new CurrencyPair("BTC", "USDT")) - && e.Value > 1.0m // 1BTC will always be more than 1USD + && e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD ); } } diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 82c1ee9ec..568d25c48 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -63,7 +63,7 @@ services: nbxplorer: - image: nicolasdorier/nbxplorer:1.0.2.6 + image: nicolasdorier/nbxplorer:1.0.2.8 ports: - "32838:32838" expose: @@ -111,7 +111,7 @@ services: - "bitcoin_datadir:/data" customer_lightningd: - image: nicolasdorier/clightning:0.0.0.16-dev + image: nicolasdorier/clightning:0.0.0.20-dev environment: EXPOSE_TCP: "true" LIGHTNINGD_OPT: | @@ -134,7 +134,7 @@ services: - bitcoind lightning-charged: - image: shesek/lightning-charge:0.3.9 + image: shesek/lightning-charge:0.3.12 environment: NETWORK: regtest API_TOKEN: foiewnccewuify @@ -153,7 +153,7 @@ services: - merchant_lightningd merchant_lightningd: - image: nicolasdorier/clightning:0.0.0.14-dev + image: nicolasdorier/clightning:0.0.0.20-dev environment: EXPOSE_TCP: "true" LIGHTNINGD_OPT: | diff --git a/BTCPayServer/BTCPayNetworkProvider.Polis.cs b/BTCPayServer/BTCPayNetworkProvider.Polis.cs new file mode 100644 index 000000000..69640dab1 --- /dev/null +++ b/BTCPayServer/BTCPayNetworkProvider.Polis.cs @@ -0,0 +1,34 @@ +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 InitPolis() + { + var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS"); + Add(new BTCPayNetwork() + { + CryptoCode = nbxplorerNetwork.CryptoCode, + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}", + NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, + NBXplorerNetwork = nbxplorerNetwork, + UriScheme = "polis", + DefaultRateRules = new[] + { + "POLIS_X = POLIS_BTC * BTC_X", + "POLIS_BTC = cryptopia(POLIS_BTC)" + }, + CryptoImagePath = "imlegacy/polis.png", + DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'") + }); + } + } +} diff --git a/BTCPayServer/BTCPayNetworkProvider.cs b/BTCPayServer/BTCPayNetworkProvider.cs index 1aadb7bc3..aec5a9d18 100644 --- a/BTCPayServer/BTCPayNetworkProvider.cs +++ b/BTCPayServer/BTCPayNetworkProvider.cs @@ -50,6 +50,7 @@ namespace BTCPayServer InitDogecoin(); InitBitcoinGold(); InitMonacoin(); + InitPolis(); } /// diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index ca3aa0595..3a9fb0a14 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.1 - 1.0.2.21 + 1.0.2.31 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -37,24 +37,24 @@ - + - - + + - - - - - + + + + + - - + + diff --git a/BTCPayServer/Controllers/AccessTokenController.cs b/BTCPayServer/Controllers/AccessTokenController.cs index ab6f15028..3ae9494c8 100644 --- a/BTCPayServer/Controllers/AccessTokenController.cs +++ b/BTCPayServer/Controllers/AccessTokenController.cs @@ -77,6 +77,7 @@ namespace BTCPayServer.Controllers { new PairingCodeResponse() { + Policies = new Newtonsoft.Json.Linq.JArray(), PairingCode = pairingEntity.Id, PairingExpiration = pairingEntity.Expiration, DateCreated = pairingEntity.CreatedTime, diff --git a/BTCPayServer/Controllers/AppsController.PointOfSale.cs b/BTCPayServer/Controllers/AppsController.PointOfSale.cs index 27290739b..22f227a0b 100644 --- a/BTCPayServer/Controllers/AppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/AppsController.PointOfSale.cs @@ -17,6 +17,8 @@ using YamlDotNet.RepresentationModel; using System.IO; using BTCPayServer.Services.Rates; using System.Globalization; +using System.Text; +using System.Text.Encodings.Web; namespace BTCPayServer.Controllers { @@ -57,15 +59,56 @@ namespace BTCPayServer.Controllers var app = await GetOwnedApp(appId, AppType.PointOfSale); if (app == null) return NotFound(); - var settings = app.GetSettings(); - return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template }); + var vm = new UpdatePointOfSaleViewModel() + { + Title = settings.Title, + ShowCustomAmount = settings.ShowCustomAmount, + Currency = settings.Currency, + Template = settings.Template + }; + if (HttpContext?.Request != null) + { + var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos"; + var encoder = HtmlEncoder.Default; + if (settings.ShowCustomAmount) + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine($"
"); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($"
"); + vm.Example1 = builder.ToString(); + } + try + { + var items = Parse(settings.Template, settings.Currency); + var builder = new StringBuilder(); + builder.AppendLine($"
"); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($" "); + builder.AppendLine($"
"); + vm.Example2 = builder.ToString(); + } + catch { } + vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3"; + } + + vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}"; + return View(vm); } [HttpPost] [Route("{appId}/settings/pos")] public async Task UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm) { - if (_Currencies.GetCurrencyData(vm.Currency) == null) + if (_Currencies.GetCurrencyData(vm.Currency, false) == null) ModelState.AddModelError(nameof(vm.Currency), "Invalid currency"); try { @@ -102,8 +145,9 @@ namespace BTCPayServer.Controllers if (app == null) return NotFound(); var settings = app.GetSettings(); - var currency = _Currencies.GetCurrencyData(settings.Currency); + var currency = _Currencies.GetCurrencyData(settings.Currency, false); double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility)); + return View(new ViewPointOfSaleViewModel() { Title = settings.Title, @@ -163,7 +207,13 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{appId}/pos")] [IgnoreAntiforgeryToken] - public async Task ViewPointOfSale(string appId, decimal amount, string choiceKey) + public async Task ViewPointOfSale(string appId, + decimal amount, + string email, + string orderId, + string notificationUrl, + string redirectUrl, + string choiceKey) { var app = await GetApp(appId, AppType.PointOfSale); if (string.IsNullOrEmpty(choiceKey) && amount <= 0) @@ -173,7 +223,7 @@ namespace BTCPayServer.Controllers if (app == null) return NotFound(); var settings = app.GetSettings(); - if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount) + if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount) { return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); } @@ -190,16 +240,22 @@ namespace BTCPayServer.Controllers } else { + if (!settings.ShowCustomAmount) + return NotFound(); price = amount; title = settings.Title; } - var store = await GetStore(app); var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice() { ItemDesc = title, Currency = settings.Currency, Price = price, + BuyerEmail = email, + OrderId = orderId, + NotificationURL = notificationUrl, + RedirectURL = redirectUrl, + FullNotifications = true }, store, HttpContext.Request.GetAbsoluteRoot()); return Redirect(invoice.Data.Url); } diff --git a/BTCPayServer/Controllers/HomeController.cs b/BTCPayServer/Controllers/HomeController.cs index 336ea6118..49e7036f1 100644 --- a/BTCPayServer/Controllers/HomeController.cs +++ b/BTCPayServer/Controllers/HomeController.cs @@ -14,5 +14,24 @@ namespace BTCPayServer.Controllers { return View("Home"); } + + public IActionResult About() + { + ViewData["Message"] = "Your application description page."; + + return View(); + } + + public IActionResult Contact() + { + ViewData["Message"] = "Your contact page."; + + return View(); + } + + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } } } diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 7515c18da..0b04d6a36 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); } @@ -244,14 +244,14 @@ namespace BTCPayServer.Controllers BtcAddress = paymentMethodDetails.GetPaymentDestination(), BtcDue = accounting.Due.ToString(), OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(), - OrderAmountFiat = OrderAmountFiat(invoice.ProductInformation), + 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 : @@ -289,15 +289,24 @@ 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 = currencies.GetNumberFormatInfo(currency); - var currencyData = currencies.GetCurrencyData(currency); + var provider = currencies.GetNumberFormatInfo(currency, true); + var currencyData = currencies.GetCurrencyData(currency, true); var divisibility = currencyData.Divisibility; while (true) { @@ -314,17 +323,11 @@ namespace BTCPayServer.Controllers provider = (NumberFormatInfo)provider.Clone(); provider.CurrencyDecimalDigits = divisibility; } - return price.ToString("C", provider) + $" ({currency})"; - } - private string OrderAmountFiat(ProductInformation productInformation) - { - // check if invoice source currency is crypto... if it is there is no "order amount in fiat" - if (_NetworkProvider.GetNetwork(productInformation.Currency) != null) - { - return null; - } - - return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable); + + if (currencyData.Crypto) + return price.ToString("C", provider); + else + return price.ToString("C", provider) + $" ({currency})"; } [HttpGet] @@ -422,7 +425,7 @@ namespace BTCPayServer.Controllers { Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"), ShowCheckout = invoice.Status == "new", - Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago", + Date = invoice.InvoiceTime, InvoiceId = invoice.Id, OrderId = invoice.OrderId ?? string.Empty, RedirectUrl = invoice.RedirectURL ?? string.Empty, diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index f10757151..d449aa173 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -109,6 +109,9 @@ namespace BTCPayServer.Controllers } entity.ProductInformation = Map(invoice); entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite; + if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute)) + entity.RedirectURL = null; + entity.Status = "new"; entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); diff --git a/BTCPayServer/Controllers/RateController.cs b/BTCPayServer/Controllers/RateController.cs index 9f5f9a4a2..8c35924f5 100644 --- a/BTCPayServer/Controllers/RateController.cs +++ b/BTCPayServer/Controllers/RateController.cs @@ -89,7 +89,7 @@ namespace BTCPayServer.Controllers CryptoCode = r.Pair.Left, Code = r.Pair.Right, CurrencyPair = r.Pair.ToString(), - Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name, + Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name, Value = r.Value.Value }).Where(n => n.Name != null).ToArray()); } diff --git a/BTCPayServer/CurrencyValue.cs b/BTCPayServer/CurrencyValue.cs index 26d98c3b5..ef3f2d812 100644 --- a/BTCPayServer/CurrencyValue.cs +++ b/BTCPayServer/CurrencyValue.cs @@ -21,7 +21,7 @@ namespace BTCPayServer return false; var currency = match.Groups.Last().Value.ToUpperInvariant(); - var currencyData = _CurrencyTable.GetCurrencyData(currency); + var currencyData = _CurrencyTable.GetCurrencyData(currency, false); if (currencyData == null) return false; v = Math.Round(v, currencyData.Divisibility); diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index e6e7e3520..151042737 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -36,28 +36,6 @@ namespace BTCPayServer { public static class Extensions { - public static string Prettify(this TimeSpan timeSpan) - { - if (timeSpan.TotalMinutes < 1) - { - return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}"; - } - if (timeSpan.TotalHours < 1) - { - return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}"; - } - if (timeSpan.Days < 1) - { - return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}"; - } - return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}"; - } - - private static string Plural(int totalDays) - { - return totalDays > 1 ? "s" : string.Empty; - } - public static string PrettyPrint(this TimeSpan expiration) { StringBuilder builder = new StringBuilder(); diff --git a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs index 1f5fad6dc..0f8d4fe6c 100644 --- a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs @@ -20,5 +20,9 @@ namespace BTCPayServer.Models.AppViewModels [Display(Name = "User can input custom amount")] public bool ShowCustomAmount { get; set; } + public string Example1 { get; internal set; } + public string Example2 { get; internal set; } + public string ExampleCallback { get; internal set; } + public string InvoiceUrl { get; internal set; } } } diff --git a/BTCPayServer/Models/ErrorViewModel.cs b/BTCPayServer/Models/ErrorViewModel.cs new file mode 100644 index 000000000..b32ee4e43 --- /dev/null +++ b/BTCPayServer/Models/ErrorViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace BTCPayServer.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} \ No newline at end of file diff --git a/BTCPayServer/Models/InvoiceResponse.cs b/BTCPayServer/Models/InvoiceResponse.cs index c7ad99dff..47eeb58b6 100644 --- a/BTCPayServer/Models/InvoiceResponse.cs +++ b/BTCPayServer/Models/InvoiceResponse.cs @@ -224,21 +224,21 @@ namespace BTCPayServer.Models { get; set; } - + [JsonProperty("paymentSubtotals")] public Dictionary PaymentSubtotals { get; set; } [JsonProperty("paymentTotals")] public Dictionary PaymentTotals { get; set; } - [JsonProperty("amountPaid")] + [JsonProperty("amountPaid", DefaultValueHandling = DefaultValueHandling.Include)] public long AmountPaid { get; set; } [JsonProperty("minerFees")] - public long MinerFees { get; set; } + public Dictionary MinerFees { get; set; } [JsonProperty("exchangeRates")] - public Dictionary> ExchangeRates{ get; set; } + public Dictionary> ExchangeRates { get; set; } [JsonProperty("supportedTransactionCurrencies")] public Dictionary SupportedTransactionCurrencies { get; set; } @@ -246,7 +246,7 @@ namespace BTCPayServer.Models [JsonProperty("addresses")] public Dictionary Addresses { get; set; } [JsonProperty("paymentCodes")] - public Dictionary PaymentCodes{get; set;} + public Dictionary PaymentCodes { get; set; } } public class Flags { diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index 63385532d..5dfdd5260 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -33,10 +33,7 @@ namespace BTCPayServer.Models.InvoicingModels public class InvoiceModel { - public string Date - { - get; set; - } + public DateTimeOffset Date { get; set; } public string OrderId { get; set; } public string RedirectUrl { get; set; } diff --git a/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs b/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs index a6e0c1346..bb0505385 100644 --- a/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs @@ -44,7 +44,7 @@ namespace BTCPayServer.Models.StoreViewModels public string ScriptTest { get; set; } public CoinAverageExchange[] AvailableExchanges { get; set; } - [Display(Name = "Multiply the rate by ...")] + [Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")] [Range(0.01, 10.0)] public double RateMultiplier { diff --git a/BTCPayServer/Models/TokenRequest.cs b/BTCPayServer/Models/TokenRequest.cs index 2479af21c..62dc53f22 100644 --- a/BTCPayServer/Models/TokenRequest.cs +++ b/BTCPayServer/Models/TokenRequest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Text; using NBitcoin; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Models { @@ -44,6 +45,9 @@ namespace BTCPayServer.Models public class PairingCodeResponse { + [JsonProperty(PropertyName = "policies")] + public JArray Policies { get; set; } + [JsonProperty(PropertyName = "pairingCode")] public string PairingCode { diff --git a/BTCPayServer/Rating/ExchangeRates.cs b/BTCPayServer/Rating/ExchangeRates.cs index 868c70a3b..67531c931 100644 --- a/BTCPayServer/Rating/ExchangeRates.cs +++ b/BTCPayServer/Rating/ExchangeRates.cs @@ -41,9 +41,9 @@ namespace BTCPayServer.Rating } else { - if (rate.Value.HasValue) + if (rate.BidAsk != null) { - _AllRates[key].Value = rate.Value; + _AllRates[key].BidAsk = rate.BidAsk; } } } @@ -58,39 +58,175 @@ namespace BTCPayServer.Rating return GetEnumerator(); } - public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value) + public void SetRate(string exchangeName, CurrencyPair currencyPair, BidAsk bidAsk) { if (ByExchange.TryGetValue(exchangeName, out var rates)) { var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair); - if (rate != null) - rate.Value = value; + if(rate != null) + { + rate.BidAsk = bidAsk; + } + var invPair = currencyPair.Inverse(); + var invRate = rates.FirstOrDefault(r => r.CurrencyPair == invPair); + if (invRate != null) + { + invRate.BidAsk = bidAsk?.Inverse(); + } } } - public decimal? GetRate(string exchangeName, CurrencyPair currencyPair) + public BidAsk GetRate(string exchangeName, CurrencyPair currencyPair) { if (currencyPair.Left == currencyPair.Right) - return 1.0m; + return BidAsk.One; if (ByExchange.TryGetValue(exchangeName, out var rates)) { var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair); if (rate != null) - return rate.Value; + return rate.BidAsk; } return null; } } + public class BidAsk + { + + private readonly static BidAsk _One = new BidAsk(1.0m); + public static BidAsk One + { + get + { + return _One; + } + } + + private readonly static BidAsk _Zero = new BidAsk(0.0m); + public static BidAsk Zero + { + get + { + return _Zero; + } + } + public BidAsk(decimal bid, decimal ask) + { + if (bid > ask) + throw new ArgumentException("the bid should be lower than ask", nameof(bid)); + _Ask = ask; + _Bid = bid; + } + public BidAsk(decimal v) : this(v, v) + { + + } + + private readonly decimal _Bid; + public decimal Bid + { + get + { + return _Bid; + } + } + + + private readonly decimal _Ask; + public decimal Ask + { + get + { + return _Ask; + } + } + public BidAsk Inverse() + { + return new BidAsk(1.0m / Ask, 1.0m / Bid); + } + + public static BidAsk operator +(BidAsk a, BidAsk b) + { + return new BidAsk(a.Bid + b.Bid, a.Ask + b.Ask); + } + + public static BidAsk operator +(BidAsk a) + { + return new BidAsk(a.Bid, a.Ask); + } + + public static BidAsk operator -(BidAsk a) + { + return new BidAsk(-a.Bid, -a.Ask); + } + + public static BidAsk operator *(BidAsk a, BidAsk b) + { + return new BidAsk(a.Bid * b.Bid, a.Ask * b.Ask); + } + + public static BidAsk operator /(BidAsk a, BidAsk b) + { + // This one is tricky. + // BTC_EUR = (6000, 6100) + // Implicit rule give + // EUR_BTC = 1 / BTC_EUR + // Or + // EUR_BTC = (1, 1) / BTC_EUR + // Naive calculation would give us ( 1/6000, 1/6100) = (0.000166, 0.000163) + // However, this is an invalid BidAsk!!! because 0.000166 > 0.000163 + // So instead, we need to calculate (1/6100, 1/6000) + return new BidAsk(a.Bid / b.Ask, a.Ask / b.Bid); + } + + public static BidAsk operator -(BidAsk a, BidAsk b) + { + return new BidAsk(a.Bid - b.Bid, a.Ask - b.Ask); + } + + + public override bool Equals(object obj) + { + BidAsk item = obj as BidAsk; + if (item == null) + return false; + return Bid == item.Bid && Ask == item.Ask; + } + public static bool operator ==(BidAsk a, BidAsk b) + { + if (System.Object.ReferenceEquals(a, b)) + return true; + if (((object)a == null) || ((object)b == null)) + return false; + return a.Bid == b.Bid && a.Ask == b.Ask; + } + + public static bool operator !=(BidAsk a, BidAsk b) + { + return !(a == b); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(StringComparison.InvariantCulture); + } + + public override string ToString() + { + if (Bid == Ask) + return Bid.ToString(CultureInfo.InvariantCulture); + return $"({Bid.ToString(CultureInfo.InvariantCulture)} , {Ask.ToString(CultureInfo.InvariantCulture)})"; + } + } public class ExchangeRate { public string Exchange { get; set; } public CurrencyPair CurrencyPair { get; set; } - public decimal? Value { get; set; } + public BidAsk BidAsk { get; set; } public override string ToString() { - if (Value == null) + if (BidAsk == null) return $"{Exchange}({CurrencyPair})"; - return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}"; + return $"{Exchange}({CurrencyPair}) == {BidAsk.ToString()}"; } } } diff --git a/BTCPayServer/Rating/RateRules.cs b/BTCPayServer/Rating/RateRules.cs index e7fe02e0d..7d2b898a6 100644 --- a/BTCPayServer/Rating/RateRules.cs +++ b/BTCPayServer/Rating/RateRules.cs @@ -18,6 +18,7 @@ namespace BTCPayServer.Rating UnsupportedOperator, MissingArgument, DivideByZero, + InvalidNegative, PreprocessError, RateUnavailable, InvalidExchangeName, @@ -139,7 +140,7 @@ namespace BTCPayServer.Rating } return new RateRule(this, currencyPair, candidate); } - + public ExpressionSyntax FindBestCandidate(CurrencyPair p) { var invP = p.Inverse(); @@ -147,9 +148,9 @@ namespace BTCPayServer.Rating foreach (var pair in new[] { (Pair: p, Priority: 0, Inverse: false), - (Pair: new CurrencyPair(p.Left, "X"), Priority: 1, Inverse: false), - (Pair: new CurrencyPair("X", p.Right), Priority: 1, Inverse: false), - (Pair: invP, Priority: 2, Inverse: true), + (Pair: invP, Priority: 1, Inverse: true), + (Pair: new CurrencyPair(p.Left, "X"), Priority: 2, Inverse: false), + (Pair: new CurrencyPair("X", p.Right), Priority: 2, Inverse: false), (Pair: new CurrencyPair(invP.Left, "X"), Priority: 3, Inverse: true), (Pair: new CurrencyPair("X", invP.Right), Priority: 3, Inverse: true), (Pair: new CurrencyPair("X", "X"), Priority: 4, Inverse: false) @@ -216,8 +217,7 @@ namespace BTCPayServer.Rating } else { - var token = SyntaxFactory.ParseToken(rate.Value.ToString(CultureInfo.InvariantCulture)); - return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, token); + return RateRules.CreateExpression(rate.ToString()); } } } @@ -225,7 +225,7 @@ namespace BTCPayServer.Rating class CalculateWalker : CSharpSyntaxWalker { - public Stack Values = new Stack(); + public Stack Values = new Stack(); public List Errors = new List(); public override void VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node) @@ -254,7 +254,15 @@ namespace BTCPayServer.Rating switch (node.Kind()) { case SyntaxKind.UnaryMinusExpression: - Values.Push(-Values.Pop()); + var v = Values.Pop(); + if(v.Bid == v.Ask) + { + Values.Push(-v); + } + else + { + Errors.Add(RateRulesErrors.InvalidNegative); + } break; case SyntaxKind.UnaryPlusExpression: Values.Push(+Values.Pop()); @@ -299,7 +307,7 @@ namespace BTCPayServer.Rating Values.Push(a * b); break; case SyntaxKind.DivideExpression: - if (b == decimal.Zero) + if (a.Ask == decimal.Zero || b.Ask == decimal.Zero) { Errors.Add(RateRulesErrors.DivideByZero); } @@ -309,19 +317,48 @@ namespace BTCPayServer.Rating } break; case SyntaxKind.SubtractExpression: - Values.Push(a - b); + if (b.Bid == b.Ask) + { + Values.Push(a - b); + } + else + { + Errors.Add(RateRulesErrors.InvalidNegative); + } break; default: throw new NotSupportedException("Should never happen"); } } + Stack _TupleValues = null; + public override void VisitTupleExpression(TupleExpressionSyntax node) + { + _TupleValues = new Stack(); + base.VisitTupleExpression(node); + if(_TupleValues.Count != 2) + { + Errors.Add(RateRulesErrors.MissingArgument); + } + else + { + var ask = _TupleValues.Pop(); + var bid = _TupleValues.Pop(); + Values.Push(new BidAsk(bid, ask)); + } + _TupleValues = null; + } + public override void VisitLiteralExpression(LiteralExpressionSyntax node) { switch (node.Kind()) { case SyntaxKind.NumericLiteralExpression: - Values.Push(decimal.Parse(node.ToString(), CultureInfo.InvariantCulture)); + var v = decimal.Parse(node.ToString(), CultureInfo.InvariantCulture); + if (_TupleValues == null) + Values.Push(new BidAsk(v)); + else + _TupleValues.Push(v); break; } } @@ -487,7 +524,7 @@ namespace BTCPayServer.Rating Errors.AddRange(calculate.Errors); return false; } - _Value = calculate.Values.Pop(); + _Value = calculate.Values.Pop().Bid; _EvaluatedNode = result; return true; } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 27204ed0f..c770e4a27 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -13,6 +13,7 @@ using NBXplorer; using NBXplorer.DerivationStrategy; using BTCPayServer.Payments; using NBitpayClient; +using BTCPayServer.Payments.Bitcoin; namespace BTCPayServer.Services.Invoices { @@ -338,9 +339,8 @@ namespace BTCPayServer.Services.Invoices Status = Status, Currency = ProductInformation.Currency, Flags = new Flags() { Refundable = Refundable }, - PaymentSubtotals = new Dictionary(), - PaymentTotals= new Dictionary(), + PaymentTotals = new Dictionary(), SupportedTransactionCurrencies = new Dictionary(), Addresses = new Dictionary(), PaymentCodes = new Dictionary(), @@ -349,9 +349,9 @@ namespace BTCPayServer.Services.Invoices dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id; dto.CryptoInfo = new List(); + dto.MinerFees = new Dictionary(); foreach (var info in this.GetPaymentMethods(networkProvider)) { - var accounting = info.Calculate(); var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); var subtotalPrice = accounting.TotalDue - accounting.NetworkFee; @@ -375,7 +375,7 @@ namespace BTCPayServer.Services.Invoices cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(); cryptoInfo.Address = address; - + cryptoInfo.ExRates = exrates; var paymentId = info.GetId(); var scheme = info.Network.UriScheme; @@ -383,6 +383,10 @@ namespace BTCPayServer.Services.Invoices if (paymentId.PaymentType == PaymentTypes.BTCLike) { + var minerInfo = new MinerFeeInfo(); + minerInfo.TotalFee = accounting.NetworkFee.Satoshi; + minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate.GetFee(1).Satoshi; + dto.MinerFees.TryAdd(paymentId.CryptoCode, minerInfo); var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode; cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() { @@ -392,7 +396,7 @@ namespace BTCPayServer.Services.Invoices BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}", }; } - + if (paymentId.PaymentType == PaymentTypes.LightningLike) { cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() diff --git a/BTCPayServer/Services/Mails/EmailSettings.cs b/BTCPayServer/Services/Mails/EmailSettings.cs index 93b062eec..c7952942f 100644 --- a/BTCPayServer/Services/Mails/EmailSettings.cs +++ b/BTCPayServer/Services/Mails/EmailSettings.cs @@ -55,7 +55,7 @@ namespace BTCPayServer.Services.Mails public SmtpClient CreateSmtpClient() { SmtpClient client = new SmtpClient(Server, Port.Value); - client.EnableSsl = true; + client.EnableSsl = EnableSSL; client.UseDefaultCredentials = false; client.Credentials = new NetworkCredential(Login, Password); client.DeliveryMethod = SmtpDeliveryMethod.Network; diff --git a/BTCPayServer/Services/Rates/BitpayRateProvider.cs b/BTCPayServer/Services/Rates/BitpayRateProvider.cs index 0898fd883..3571ad1b8 100644 --- a/BTCPayServer/Services/Rates/BitpayRateProvider.cs +++ b/BTCPayServer/Services/Rates/BitpayRateProvider.cs @@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Rates { return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false)) .AllRates - .Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value }) + .Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), BidAsk = new BidAsk(r.Value) }) .ToList()); } } diff --git a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs index 0ee5c26b1..79562daa1 100644 --- a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs +++ b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs @@ -54,7 +54,7 @@ namespace BTCPayServer.Services.Rates public const string CoinAverageName = "coinaverage"; public CoinAverageRateProvider() { - + } static HttpClient _Client = new HttpClient(); @@ -69,10 +69,30 @@ namespace BTCPayServer.Services.Rates public ICoinAverageAuthenticator Authenticator { get; set; } - private bool TryToDecimal(JProperty p, out decimal v) + private bool TryToBidAsk(JProperty p, out BidAsk bidAsk) { - JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"]; - return decimal.TryParse(token.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v); + bidAsk = null; + if (Exchange == CoinAverageName) + { + JToken last = p.Value["last"]; + if (!decimal.TryParse(last.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v) || + v <= 0) + return false; + bidAsk = new BidAsk(v); + return true; + } + else + { + JToken bid = p.Value["bid"]; + JToken ask = p.Value["ask"]; + if (!decimal.TryParse(bid.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) || + !decimal.TryParse(ask.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) || + v1 > v2 || + v1 <= 0 || v2 <= 0) + return false; + bidAsk = new BidAsk(v1, v2); + return true; + } } public async Task GetRatesAsync() @@ -108,10 +128,10 @@ namespace BTCPayServer.Services.Rates { ExchangeRate exchangeRate = new ExchangeRate(); exchangeRate.Exchange = Exchange; - if (!TryToDecimal(prop, out decimal value)) + if (!TryToBidAsk(prop, out var value)) continue; - exchangeRate.Value = value; - if(CurrencyPair.TryParse(prop.Name, out var pair)) + exchangeRate.BidAsk = value; + if (CurrencyPair.TryParse(prop.Name, out var pair)) { exchangeRate.CurrencyPair = pair; exchangeRates.Add(exchangeRate); diff --git a/BTCPayServer/Services/Rates/CurrencyNameTable.cs b/BTCPayServer/Services/Rates/CurrencyNameTable.cs index 97e748722..a709890e0 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 { @@ -41,13 +42,26 @@ namespace BTCPayServer.Services.Rates static Dictionary _CurrencyProviders = new Dictionary(); - public NumberFormatInfo GetNumberFormatInfo(string currency) + public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback) { var data = GetCurrencyProvider(currency); if (data is NumberFormatInfo nfi) return nfi; - return ((CultureInfo)data).NumberFormat; + if (data is CultureInfo ci) + return ci.NumberFormat; + if (!useFallback) + return null; + return CreateFallbackCurrencyFormatInfo(currency); } + + private NumberFormatInfo CreateFallbackCurrencyFormatInfo(string currency) + { + var usd = GetNumberFormatInfo("USD", false); + var currencyInfo = (NumberFormatInfo)usd.Clone(); + currencyInfo.CurrencySymbol = currency; + return currencyInfo; + } + public IFormatProvider GetCurrencyProvider(string currency) { lock (_CurrencyProviders) @@ -125,17 +139,31 @@ namespace BTCPayServer.Services.Rates { Code = network.CryptoCode, Divisibility = 8, - Name = network.CryptoCode + Name = network.CryptoCode, + Crypto = true }); } return dico.Values.ToArray(); } - public CurrencyData GetCurrencyData(string currency) + public CurrencyData GetCurrencyData(string currency, bool useFallback) { CurrencyData result; - _Currencies.TryGetValue(currency.ToUpperInvariant(), out result); + if(!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result)) + { + if(useFallback) + { + var usd = GetCurrencyData("USD", false); + result = new CurrencyData() + { + Code = currency, + Crypto = true, + Name = currency, + Divisibility = usd.Divisibility + }; + } + } return result; } diff --git a/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs b/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs index 9aeaf7fb1..6efdad8d3 100644 --- a/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs +++ b/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs @@ -61,7 +61,7 @@ namespace BTCPayServer.Services.Rates var rate = new ExchangeRate(); rate.CurrencyPair = pair; rate.Exchange = _ExchangeName; - rate.Value = ticker.Value.Bid; + rate.BidAsk = new BidAsk(ticker.Value.Bid, ticker.Value.Ask); return rate; } catch (ArgumentException) diff --git a/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs b/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs index 10fb75189..381392ca9 100644 --- a/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs +++ b/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs @@ -14,13 +14,19 @@ namespace BTCPayServer.Services.Rates public const string QuadrigacxName = "quadrigacx"; static HttpClient _Client = new HttpClient(); - private bool TryToDecimal(JObject p, out decimal v) + private bool TryToBidAsk(JObject p, out BidAsk v) { - v = 0.0m; - JToken token = p.Property("bid")?.Value; - if (token == null) + v = null; + JToken bid = p.Property("bid")?.Value; + JToken ask = p.Property("ask")?.Value; + if (bid == null || ask == null) return false; - return decimal.TryParse(token.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v); + if (!decimal.TryParse(bid.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) || + !decimal.TryParse(bid.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) || + v1 <= 0m || v2 <= 0m || v1 > v2) + return false; + v = new BidAsk(v1, v2); + return true; } public async Task GetRatesAsync() @@ -37,9 +43,9 @@ namespace BTCPayServer.Services.Rates continue; rate.CurrencyPair = pair; rate.Exchange = QuadrigacxName; - if (!TryToDecimal((JObject)prop.Value, out var v)) + if (!TryToBidAsk((JObject)prop.Value, out var v)) continue; - rate.Value = v; + rate.BidAsk = v; exchangeRates.Add(rate); } return exchangeRates; diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index 1e9304634..766def9b6 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -38,6 +38,31 @@ +
+
Host button externally
+

You can host point of sale buttons in an external website with the following code.

+ @if(Model.Example1 != null) + { + For anything with a custom amount +
@Model.Example1
+ } + @if(Model.Example2 != null) + { + For a specific item of your template +
@Model.Example2
+ } +

A POST callback will be sent to notification with the following form will be sent to notificationUrl once the enough is paid and once again once there is enough confirmations to the payment:

+
@Model.ExampleCallback
+

Never trust anything but id, ignore the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:

+

+

    +
  • Build the invoice's url by yourself do not trust the url field, this can be spoofed to use attacker's server.
  • +
  • Send a GET request to the invoice's url with Content-Type: application/json
  • +
  • Verify that the orderId is from your backend, that the price is correct and that status is either confirmed or complete
  • +
  • You can then ship your order
  • +
+

+
@@ -47,3 +72,10 @@ + +@section Scripts { + + + +} + diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index e00aa4547..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 }}
diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index 343d3c613..cb3cc5699 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -53,15 +53,15 @@ Created date - @Model.CreatedDate + @Model.CreatedDate.ToBrowserDate() Expiration date - @Model.ExpirationDate + @Model.ExpirationDate.ToBrowserDate() Monitoring date - @Model.MonitoringDate + @Model.MonitoringDate.ToBrowserDate() Transaction speed @@ -289,7 +289,7 @@ @foreach(var evt in Model.Events) { - @evt.Timestamp + @evt.Timestamp.ToBrowserDate() @evt.Message } diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index d6e6915f8..22e40ae10 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -66,7 +66,7 @@ @foreach(var invoice in Model.Invoices) { - @invoice.Date + @invoice.Date.ToTimeAgo() @if(invoice.RedirectUrl != string.Empty) { diff --git a/BTCPayServer/Views/Server/Rates.cshtml b/BTCPayServer/Views/Server/Rates.cshtml index bbf407366..1dcf9bea6 100644 --- a/BTCPayServer/Views/Server/Rates.cshtml +++ b/BTCPayServer/Views/Server/Rates.cshtml @@ -40,7 +40,7 @@ - + @@ -48,7 +48,7 @@ - +
Quota period@Model.RateLimits.TotalPeriod.Prettify()@Model.RateLimits.TotalPeriod.TimeString()
Requests quota
Quota reset in@Model.RateLimits.CounterReset.Prettify()@Model.RateLimits.CounterReset.TimeString()
} diff --git a/BTCPayServer/Views/Shared/Error.cshtml b/BTCPayServer/Views/Shared/Error.cshtml new file mode 100644 index 000000000..245c075d9 --- /dev/null +++ b/BTCPayServer/Views/Shared/Error.cshtml @@ -0,0 +1,22 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if(Model != null && Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/BTCPayServer/Views/Stores/AddLightningNode.cshtml b/BTCPayServer/Views/Stores/AddLightningNode.cshtml index fc9c552c9..5a1d49355 100644 --- a/BTCPayServer/Views/Stores/AddLightningNode.cshtml +++ b/BTCPayServer/Views/Stores/AddLightningNode.cshtml @@ -16,7 +16,7 @@