diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 45047255e..9d04f16d0 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp2.1 false NU1701,CA1816,CA1308,CA1810,CA2208 diff --git a/BTCPayServer.Tests/Dockerfile b/BTCPayServer.Tests/Dockerfile index 978238de0..7f0fab553 100644 --- a/BTCPayServer.Tests/Dockerfile +++ b/BTCPayServer.Tests/Dockerfile @@ -1,4 +1,4 @@ -FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch +FROM microsoft/dotnet:2.1.300-rc1-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 d82b287d1..f12736855 100644 --- a/BTCPayServer.Tests/RateRulesTest.cs +++ b/BTCPayServer.Tests/RateRulesTest.cs @@ -113,12 +113,21 @@ namespace BTCPayServer.Tests builder.AppendLine("DOGE_BTC = 2000"); Assert.True(RateRules.TryParse(builder.ToString(), out rules)); rules.GlobalMultiplier = 1.1m; + 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); 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); + + // 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); + 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); //////// } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 529851724..2bbcaba46 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -229,6 +229,73 @@ namespace BTCPayServer.Tests #pragma warning restore CS0618 } + [Fact] + public void CanAcceptInvoiceWithTolerance() + { + var entity = new InvoiceEntity(); +#pragma warning disable CS0618 + entity.Payments = new List(); + entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) }); + entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.PaymentTolerance = 0; + + + var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike); + var accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(1.1m), accounting.Due); + Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); + Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue); + + entity.PaymentTolerance = 10; + accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue); + + entity.PaymentTolerance = 100; + accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue); + + } + + [Fact] + public void CanAcceptInvoiceWithTolerance2() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + + // Set tolerance to 50% + var stores = user.GetController(); + var vm = Assert.IsType(Assert.IsType(stores.UpdateStore()).Model); + Assert.Equal(0.0, vm.PaymentTolerance); + vm.PaymentTolerance = 50.0; + Assert.IsType(stores.UpdateStore(vm).Result); + + var invoice = user.BitPay.CreateInvoice(new Invoice() + { + Buyer = new Buyer() { email = "test@fwf.com" }, + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + // Pays 75% + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); + tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Satoshis((decimal)invoice.BtcDue.Satoshi * 0.75m)); + + Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("paid", localInvoice.Status); + }); + } + } + [Fact] public void CanPayUsingBIP70() { @@ -241,7 +308,7 @@ namespace BTCPayServer.Tests var invoice = user.BitPay.CreateInvoice(new Invoice() { Buyer = new Buyer() { email = "test@fwf.com" }, - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -373,7 +440,7 @@ namespace BTCPayServer.Tests var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 0.01, + Price = 0.01m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -406,7 +473,7 @@ namespace BTCPayServer.Tests var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 0.01, + Price = 0.01m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -434,7 +501,7 @@ namespace BTCPayServer.Tests await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5)); var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice() { - Price = 0.01, + Price = 0.01m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -487,7 +554,7 @@ namespace BTCPayServer.Tests acc.RegisterDerivationScheme("BTC"); var invoice = acc.BitPay.CreateInvoice(new Invoice() { - Price = 5.0, + Price = 5.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -531,6 +598,53 @@ namespace BTCPayServer.Tests } } + [Fact] + public void CanListInvoices() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var acc = tester.NewAccount(); + acc.GrantAccess(); + acc.RegisterDerivationScheme("BTC"); + // First we try payment with a merchant having only BTC + var invoice = acc.BitPay.CreateInvoice(new Invoice() + { + Price = 500, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + var cashCow = tester.ExplorerNode; + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); + var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10); + cashCow.SendToAddress(invoiceAddress, firstPayment); + Eventually(() => + { + invoice = acc.BitPay.GetInvoice(invoice.Id); + Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid); + }); + + + AssertSearchInvoice(acc, true, invoice.Id, $"storeid:{acc.StoreId}"); + AssertSearchInvoice(acc, false, invoice.Id, $"storeid:blah"); + AssertSearchInvoice(acc, true, invoice.Id, $"{invoice.Id}"); + AssertSearchInvoice(acc, true, invoice.Id, $"exceptionstatus:paidPartial"); + AssertSearchInvoice(acc, false, invoice.Id, $"exceptionstatus:paidOver"); + AssertSearchInvoice(acc, true, invoice.Id, $"unusual:true"); + AssertSearchInvoice(acc, false, invoice.Id, $"unusual:false"); + } + } + + private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter) + { + var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController().ListInvoices(filter).Result).Model; + Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId)); + } + [Fact] public void CanRBFPayment() { @@ -542,7 +656,7 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("BTC"); var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD" }, Facade.Merchant); var payment1 = invoice.BtcDue + Money.Coins(0.0001m); @@ -642,7 +756,7 @@ namespace BTCPayServer.Tests message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey))); var invoice = new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD" }; message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json"); @@ -684,7 +798,7 @@ namespace BTCPayServer.Tests storeController.Rates(vm).Wait(); var invoice2 = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -708,7 +822,7 @@ namespace BTCPayServer.Tests // First we try payment with a merchant having only BTC var invoice1 = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -726,7 +840,7 @@ namespace BTCPayServer.Tests var invoice2 = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -782,7 +896,7 @@ namespace BTCPayServer.Tests // Despite it is called BitcoinAddress it should be LTC because BTC is not available Assert.Null(invoice.BitcoinAddress); - Assert.NotEqual(1.0, invoice.Rate); + Assert.NotEqual(1.0m, invoice.Rate); Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due); @@ -869,7 +983,7 @@ namespace BTCPayServer.Tests // First we try payment with a merchant having only BTC var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -901,7 +1015,7 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("LTC"); invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1023,7 +1137,7 @@ namespace BTCPayServer.Tests var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 1.5, + Price = 1.5m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1036,7 +1150,7 @@ namespace BTCPayServer.Tests invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5.5, + Price = 5.5m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1085,7 +1199,7 @@ namespace BTCPayServer.Tests Assert.Equal("$5.00", vmview.Items[0].Price.Formatted); Assert.IsType(apps.ViewPointOfSale(appId, 0, "orange").Result); var invoice = user.BitPay.GetInvoices().First(); - Assert.Equal(10.00, invoice.Price); + Assert.Equal(10.00m, invoice.Price); Assert.Equal("CAD", invoice.Currency); Assert.Equal("orange", invoice.ItemDesc); } @@ -1136,7 +1250,7 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("BTC"); var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1241,12 +1355,12 @@ namespace BTCPayServer.Tests { var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("complete", localInvoice.Status); - Assert.NotEqual(0.0, localInvoice.Rate); + Assert.NotEqual(0.0m, localInvoice.Rate); }); invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1340,7 +1454,7 @@ namespace BTCPayServer.Tests private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider) { - return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings()); + return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, null, new CoinAverageSettings()); } [Fact] diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 0ea276438..d2321ba1e 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -46,7 +46,7 @@ services: - lightning-charged nbxplorer: - image: nicolasdorier/nbxplorer:1.0.2.2 + image: nicolasdorier/nbxplorer:1.0.2.6 ports: - "32838:32838" expose: @@ -89,14 +89,15 @@ services: - "bitcoin_datadir:/data" customer_lightningd: - image: nicolasdorier/clightning:0.0.0.11-dev + image: nicolasdorier/clightning:0.0.0.14-dev environment: EXPOSE_TCP: "true" LIGHTNINGD_OPT: | bitcoin-datadir=/etc/bitcoin bitcoin-rpcconnect=bitcoind network=regtest - ipaddr=customer_lightningd + bind-addr=0.0.0.0 + announce-addr=customer_lightningd log-level=debug dev-broadcast-interval=1000 ports: @@ -130,13 +131,14 @@ services: - merchant_lightningd merchant_lightningd: - image: nicolasdorier/clightning:0.0.0.11-dev + image: nicolasdorier/clightning:0.0.0.14-dev environment: EXPOSE_TCP: "true" LIGHTNINGD_OPT: | bitcoin-datadir=/etc/bitcoin bitcoin-rpcconnect=bitcoind - ipaddr=merchant_lightningd + bind-addr=0.0.0.0 + announce-addr=merchant_lightningd network=regtest log-level=debug dev-broadcast-interval=1000 diff --git a/BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs b/BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs new file mode 100644 index 000000000..c75c3dd18 --- /dev/null +++ b/BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs @@ -0,0 +1,29 @@ +using NBitcoin; + +namespace BTCPayServer +{ + public partial class BTCPayNetworkProvider + { + public void InitBitcoinGold() + { + var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG"); + Add(new BTCPayNetwork() + { + CryptoCode = nbxplorerNetwork.CryptoCode, + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}", + NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, + NBXplorerNetwork = nbxplorerNetwork, + UriScheme = "bitcoingold", + DefaultRateRules = new[] + { + "BTG_X = BTG_BTC * BTC_X", + "BTG_BTC = bitfinex(BTG_BTC)", + }, + CryptoImagePath = "imlegacy/btg-symbol.svg", + LightningImagePath = "imlegacy/btg-symbol.svg", + DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'") + }); + } + } +} diff --git a/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs b/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs index 9fbff33a9..e12ceaff7 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs +++ b/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs @@ -20,7 +20,11 @@ namespace BTCPayServer NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "dogecoin", - DefaultRateRules = new[] { "DOGE_X = bittrex(DOGE_BTC) * BTC_X" }, + DefaultRateRules = new[] + { + "DOGE_X = DOGE_BTC * BTC_X", + "DOGE_BTC = bittrex(DOGE_BTC)" + }, CryptoImagePath = "imlegacy/dogecoin.png", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'") diff --git a/BTCPayServer/BTCPayNetworkProvider.cs b/BTCPayServer/BTCPayNetworkProvider.cs index 4af88a67d..10bed3762 100644 --- a/BTCPayServer/BTCPayNetworkProvider.cs +++ b/BTCPayServer/BTCPayNetworkProvider.cs @@ -48,6 +48,7 @@ namespace BTCPayServer InitBitcoin(); InitLitecoin(); InitDogecoin(); + InitBitcoinGold(); InitPolis(); } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index cd4e8d50f..61a2a615b 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -1,8 +1,8 @@ - + Exe - netcoreapp2.0 - 1.0.2.2 + netcoreapp2.1 + 1.0.2.16 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -30,38 +30,35 @@ - + - + - - + + - - + + - + - - - + + - - + + - - diff --git a/BTCPayServer/Controllers/AppsController.PointOfSale.cs b/BTCPayServer/Controllers/AppsController.PointOfSale.cs index 1cb1218f5..27290739b 100644 --- a/BTCPayServer/Controllers/AppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/AppsController.PointOfSale.cs @@ -162,7 +162,8 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{appId}/pos")] - public async Task ViewPointOfSale(string appId, double amount, string choiceKey) + [IgnoreAntiforgeryToken] + public async Task ViewPointOfSale(string appId, decimal amount, string choiceKey) { var app = await GetApp(appId, AppType.PointOfSale); if (string.IsNullOrEmpty(choiceKey) && amount <= 0) @@ -177,7 +178,7 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); } string title = null; - double price = 0.0; + var price = 0.0m; if (!string.IsNullOrEmpty(choiceKey)) { var choices = Parse(settings.Template, settings.Currency); @@ -185,7 +186,7 @@ namespace BTCPayServer.Controllers if (choice == null) return NotFound(); title = choice.Title; - price = (double)choice.Price.Value; + price = choice.Price.Value; } else { diff --git a/BTCPayServer/Controllers/AppsController.cs b/BTCPayServer/Controllers/AppsController.cs index 92cce3217..143f6d92a 100644 --- a/BTCPayServer/Controllers/AppsController.cs +++ b/BTCPayServer/Controllers/AppsController.cs @@ -176,24 +176,19 @@ namespace BTCPayServer.Controllers using (var ctx = _ContextFactory.CreateContext()) { return await ctx.UserStore - .Where(us => us.ApplicationUserId == userId) - .Select(us => new - { - IsOwner = us.Role == StoreRoles.Owner, - StoreId = us.StoreDataId, - StoreName = us.StoreData.StoreName, - Apps = us.StoreData.Apps - }) - .SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel() - { - IsOwner = us.IsOwner, - AppName = app.Name, - AppType = app.AppType, - Id = app.Id, - StoreId = us.StoreId, - StoreName = us.StoreName - })) - .ToArrayAsync(); + .Where(us => us.ApplicationUserId == userId) + .Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, + (us, app) => + new ListAppsViewModel.ListAppViewModel() + { + IsOwner = us.Role == StoreRoles.Owner, + StoreId = us.StoreDataId, + StoreName = us.StoreData.StoreName, + AppName = app.Name, + AppType = app.AppType, + Id = app.Id + }) + .ToArrayAsync(); } } diff --git a/BTCPayServer/Controllers/HomeController.cs b/BTCPayServer/Controllers/HomeController.cs index 49e7036f1..336ea6118 100644 --- a/BTCPayServer/Controllers/HomeController.cs +++ b/BTCPayServer/Controllers/HomeController.cs @@ -14,24 +14,5 @@ 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.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index 52b6f41f8..aac4ee764 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -13,11 +13,14 @@ using BTCPayServer.Data; using BTCPayServer.Services.Invoices; using Microsoft.AspNetCore.Cors; using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Authorization; +using BTCPayServer.Security; namespace BTCPayServer.Controllers { [EnableCors("BitpayAPI")] [BitpayAPIConstraint] + [Authorize(Policies.CanUseStore.Key)] public class InvoiceControllerAPI : Controller { private InvoiceController _InvoiceController; @@ -43,9 +46,10 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("invoices/{id}")] + [AllowAnonymous] public async Task> GetInvoice(string id, string token) { - var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id); + var invoice = await _InvoiceRepository.GetInvoice(null, id); if (invoice == null) throw new BitpayHttpException(404, "Object not found"); var resp = invoice.EntityToDTO(_NetworkProvider); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 7c14ebdd7..f2b8b423a 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -51,7 +51,10 @@ namespace BTCPayServer.Controllers StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }), Id = invoice.Id, Status = invoice.Status, - TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low", + TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : + invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : + invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" : + "low", RefundEmail = invoice.RefundMail, CreatedDate = invoice.InvoiceTime, ExpirationDate = invoice.ExpirationTime, @@ -201,6 +204,12 @@ namespace BTCPayServer.Controllers var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr); var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode); + if (network == null && isDefaultCrypto) + { + network = _NetworkProvider.GetAll().FirstOrDefault(); + paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); + paymentMethodIdStr = paymentMethodId.ToString(); + } if (invoice == null || network == null) return null; if (!invoice.Support(paymentMethodId)) @@ -210,6 +219,7 @@ namespace BTCPayServer.Controllers var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First(); network = paymentMethodTemp.Network; paymentMethodId = paymentMethodTemp.GetId(); + paymentMethodIdStr = paymentMethodId.ToString(); } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider); @@ -370,14 +380,19 @@ namespace BTCPayServer.Controllers Count = count, Skip = skip, UserId = GetUserId(), + Unusual = !filterString.Filters.ContainsKey("unusual") ? null + : !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null + : r, Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null, + ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null, StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null })) { model.SearchTerm = searchTerm; model.Invoices.Add(new InvoiceModel() { - Status = invoice.Status, + Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"), + ShowCheckout = invoice.Status == "new", Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago", InvoiceId = invoice.Id, OrderId = invoice.OrderId ?? string.Empty, diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 96299651b..f10757151 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -98,6 +98,7 @@ namespace BTCPayServer.Controllers entity.ExtendedNotifications = invoice.ExtendedNotifications; entity.NotificationURL = notificationUri?.AbsoluteUri; entity.BuyerInformation = Map(invoice); + entity.PaymentTolerance = storeBlob.PaymentTolerance; //Another way of passing buyer info to support FillBuyerInfo(invoice.Buyer, entity.BuyerInformation); if (entity?.BuyerInformation?.BuyerEmail != null) @@ -276,6 +277,7 @@ namespace BTCPayServer.Controllers return defaultPolicy; var mappings = new Dictionary(); mappings.Add("low", SpeedPolicy.LowSpeed); + mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed); mappings.Add("medium", SpeedPolicy.MediumSpeed); mappings.Add("high", SpeedPolicy.HighSpeed); if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy)) diff --git a/BTCPayServer/Controllers/RateController.cs b/BTCPayServer/Controllers/RateController.cs index 35a000497..9f5f9a4a2 100644 --- a/BTCPayServer/Controllers/RateController.cs +++ b/BTCPayServer/Controllers/RateController.cs @@ -36,11 +36,12 @@ namespace BTCPayServer.Controllers [BitpayAPIConstraint] public async Task GetRates(string currencyPairs, string storeId) { + storeId = storeId ?? this.HttpContext.GetStoreData()?.Id; var result = await GetRates2(currencyPairs, storeId); - var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[]; + var rates = (result as JsonResult)?.Value as Rate[]; if (rates == null) return result; - return Json(new DataWrapper(rates)); + return Json(new DataWrapper(rates)); } [Route("api/rates")] @@ -54,8 +55,9 @@ namespace BTCPayServer.Controllers return result; } - - var store = await _StoreRepo.FindStore(storeId); + var store = this.HttpContext.GetStoreData(); + if(store == null || store.Id != storeId) + store = await _StoreRepo.FindStore(storeId); if (store == null) { var result = Json(new BitpayErrorsModel() { Error = "Store not found" }); @@ -86,6 +88,7 @@ namespace BTCPayServer.Controllers { CryptoCode = r.Pair.Left, Code = r.Pair.Right, + CurrencyPair = r.Pair.ToString(), Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name, Value = r.Value.Value }).Where(n => n.Name != null).ToArray()); @@ -106,6 +109,14 @@ namespace BTCPayServer.Controllers get; set; } + + [JsonProperty(PropertyName = "currencyPair")] + public string CurrencyPair + { + get; + set; + } + [JsonProperty(PropertyName = "code")] public string Code { diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 54ec88151..2c10440d0 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -243,10 +243,7 @@ namespace BTCPayServer.Controllers { try { - if(string.IsNullOrWhiteSpace(model.Settings.From) - || string.IsNullOrWhiteSpace(model.TestEmail) - || string.IsNullOrWhiteSpace(model.Settings.Login) - || string.IsNullOrWhiteSpace(model.Settings.Server)) + if(!model.Settings.IsComplete()) { model.StatusMessage = "Error: Required fields missing"; return View(model); diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 6a480a39a..944c97f4f 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -10,6 +10,7 @@ using BTCPayServer.Data; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Services; +using LedgerWallet; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBXplorer.DerivationStrategy; @@ -264,7 +265,7 @@ namespace BTCPayServer.Controllers { var strategy = GetDirectDerivationStrategy(store, network); var strategyBase = GetDerivationStrategy(store, network); - if (strategy == null || !await hw.SupportDerivation(network, strategy)) + if (strategy == null || await hw.GetKeyPath(network, strategy) == null) { throw new Exception($"This store is not configured to use this ledger"); } @@ -286,11 +287,76 @@ namespace BTCPayServer.Controllers var unspentCoins = await wallet.GetUnspentCoins(strategyBase); var changeAddress = await change; - var transaction = await hw.SendToAddress(strategy, unspentCoins, network, - new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) }, - feeRateValue, - changeAddress.Item1, - changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee); + var send = new[] { ( + destination: destinationAddress as IDestination, + amount: amountBTC, + substractFees: subsctractFeesValue) }; + + foreach (var element in send) + { + if (element.destination == null) + throw new ArgumentNullException(nameof(element.destination)); + if (element.amount == null) + throw new ArgumentNullException(nameof(element.amount)); + if (element.amount <= Money.Zero) + throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero"); + } + + var foundKeyPath = await hw.GetKeyPath(network, strategy); + if (foundKeyPath == null) + { + throw new HardwareWalletException($"This store is not configured to use this ledger"); + } + + TransactionBuilder builder = new TransactionBuilder(); + builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee; + builder.SetConsensusFactory(network.NBitcoinNetwork); + builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray()); + + foreach (var element in send) + { + builder.Send(element.destination, element.amount); + if (element.substractFees) + builder.SubtractFees(); + } + builder.SetChange(changeAddress.Item1); + builder.SendEstimatedFees(feeRateValue); + builder.Shuffle(); + var unsigned = builder.BuildTransaction(false); + + var keypaths = new Dictionary(); + foreach (var c in unspentCoins) + { + keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); + } + + var hasChange = unsigned.Outputs.Count == 2; + var usedCoins = builder.FindSpentCoins(unsigned); + + Dictionary parentTransactions = new Dictionary(); + + if(!strategy.Segwit) + { + var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet(); + var explorer = _ExplorerProvider.GetExplorerClient(network); + var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList(); + foreach(var getTransactionAsync in getTransactionAsyncs) + { + var tx = (await getTransactionAsync.Op); + if(tx == null) + throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found"); + parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction); + } + } + + var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest + { + InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash), + InputCoin = c, + KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]), + PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey + }).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null); + try { var broadcastResult = await wallet.BroadcastTransactionsAsync(new List() { transaction }); @@ -336,8 +402,6 @@ namespace BTCPayServer.Controllers var directStrategy = strategy as DirectDerivationStrategy; if (directStrategy == null) directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy; - if (!directStrategy.Segwit) - return null; return directStrategy; } diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 6a22d7f6d..410739b18 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -431,6 +431,7 @@ namespace BTCPayServer.Controllers vm.MonitoringExpiration = storeBlob.MonitoringExpiration; vm.InvoiceExpiration = storeBlob.InvoiceExpiration; vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate; + vm.PaymentTolerance = storeBlob.PaymentTolerance; return View(vm); } @@ -496,6 +497,7 @@ namespace BTCPayServer.Controllers blob.MonitoringExpiration = model.MonitoringExpiration; blob.InvoiceExpiration = model.InvoiceExpiration; blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty; + blob.PaymentTolerance = model.PaymentTolerance; if (StoreData.SetStoreBlob(blob)) { @@ -644,11 +646,11 @@ namespace BTCPayServer.Controllers { var stores = await _Repo.GetStoresByUserId(userId); model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); - } - if (model.Stores.Count() == 0) - { - StatusMessage = "Error: You need to be owner of at least one store before pairing"; - return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); + if (model.Stores.Count() == 0) + { + StatusMessage = "Error: You need to be owner of at least one store before pairing"; + return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); + } } return View(model); } diff --git a/BTCPayServer/Data/ApplicationDbContextFactory.cs b/BTCPayServer/Data/ApplicationDbContextFactory.cs index be1d86ffd..8e6171d49 100644 --- a/BTCPayServer/Data/ApplicationDbContextFactory.cs +++ b/BTCPayServer/Data/ApplicationDbContextFactory.cs @@ -41,10 +41,12 @@ namespace BTCPayServer.Data public void ConfigureHangfireBuilder(IGlobalConfiguration builder) { - if (_Type == DatabaseType.Sqlite) - builder.UseMemoryStorage(); //Sql provider does not support multiple workers - else if (_Type == DatabaseType.Postgres) - builder.UsePostgreSqlStorage(_ConnectionString); + builder.UseMemoryStorage(); + //We always use memory storage because of incompatibilities with the latest postgres in 2.1 + //if (_Type == DatabaseType.Sqlite) + // builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers + //else if (_Type == DatabaseType.Postgres) + // builder.UsePostgreSqlStorage(_ConnectionString); } } } diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 30c86ad4e..9fa4224a2 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -247,6 +247,7 @@ namespace BTCPayServer.Data { InvoiceExpiration = 15; MonitoringExpiration = 60; + PaymentTolerance = 0; RequiresRefundEmail = true; } public bool NetworkFeeDisabled @@ -326,6 +327,10 @@ namespace BTCPayServer.Data } } + [DefaultValue(0)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public double PaymentTolerance { get; set; } + public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider) { if (!RateScripting || diff --git a/BTCPayServer/Events/NBXplorerStateChangedEvent.cs b/BTCPayServer/Events/NBXplorerStateChangedEvent.cs index 15280bb19..5774140ff 100644 --- a/BTCPayServer/Events/NBXplorerStateChangedEvent.cs +++ b/BTCPayServer/Events/NBXplorerStateChangedEvent.cs @@ -6,21 +6,6 @@ using BTCPayServer.HostedServices; namespace BTCPayServer.Events { - public class NBXplorerErrorEvent - { - public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage) - { - Message = errorMessage; - Network = network; - } - public string Message { get; set; } - public BTCPayNetwork Network { get; set; } - - public override string ToString() - { - return $"{Network.CryptoCode}: NBXplorer error `{Message}`"; - } - } public class NBXplorerStateChangedEvent { public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState) diff --git a/BTCPayServer/HostedServices/CssThemeManager.cs b/BTCPayServer/HostedServices/CssThemeManager.cs index e3afcf35b..b0e32673b 100644 --- a/BTCPayServer/HostedServices/CssThemeManager.cs +++ b/BTCPayServer/HostedServices/CssThemeManager.cs @@ -41,6 +41,13 @@ namespace BTCPayServer.HostedServices { get { return _creativeStartUri; } } + + public bool ShowRegister { get; set; } + + internal void Update(PoliciesSettings data) + { + ShowRegister = !data.LockSubscription; + } } public class CssThemeManagerHostedService : BaseAsyncService @@ -58,10 +65,19 @@ namespace BTCPayServer.HostedServices { return new[] { - CreateLoopTask(ListenForThemeChanges) + CreateLoopTask(ListenForThemeChanges), + CreateLoopTask(ListenForPoliciesChanges), }; } + async Task ListenForPoliciesChanges() + { + await new SynchronizationContextRemover(); + var data = (await _SettingsRepository.GetSettingAsync()) ?? new PoliciesSettings(); + _CssThemeManager.Update(data); + await _SettingsRepository.WaitSettingsChanged(Cancellation); + } + async Task ListenForThemeChanges() { await new SynchronizationContextRemover(); diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 86f24d7d4..0a2d9a057 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -207,7 +207,7 @@ namespace BTCPayServer.HostedServices if (btcCryptoInfo != null) { #pragma warning disable CS0618 - notification.Rate = (double)dto.Rate; + notification.Rate = dto.Rate; notification.Url = dto.Url; notification.BTCDue = dto.BTCDue; notification.BTCPaid = dto.BTCPaid; @@ -305,7 +305,10 @@ namespace BTCPayServer.HostedServices leases.Add(_EventAggregator.Subscribe(async e => { var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId); - await SaveEvent(invoice.Id, e); + List tasks = new List(); + + // Awaiting this later help make sure invoices should arrive in order + tasks.Add(SaveEvent(invoice.Id, e)); // we need to use the status in the event and not in the invoice. The invoice might now be in another status. if (invoice.FullNotifications) @@ -315,20 +318,22 @@ namespace BTCPayServer.HostedServices e.Name == "invoice_failedToConfirm" || e.Name == "invoice_markedInvalid" || e.Name == "invoice_failedToConfirm" || - e.Name == "invoice_completed" + e.Name == "invoice_completed" || + e.Name == "invoice_expiredPaidPartial" ) - await Notify(invoice); + tasks.Add(Notify(invoice)); } if (e.Name == "invoice_confirmed") { - await Notify(invoice); + tasks.Add(Notify(invoice)); } if (invoice.ExtendedNotifications) { - await Notify(invoice, e.EventCode, e.Name); + tasks.Add(Notify(invoice, e.EventCode, e.Name)); } + await Task.WhenAll(tasks.ToArray()); })); diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 5aef0a738..a6a2be0d0 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -68,6 +68,8 @@ namespace BTCPayServer.HostedServices context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired")); invoice.Status = "expired"; + if(invoice.ExceptionStatus == "paidPartial") + context.Events.Add(new InvoiceEvent(invoice, 2000, "invoice_expiredPaidPartial")); } var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray(); @@ -78,7 +80,7 @@ namespace BTCPayServer.HostedServices var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode); if (invoice.Status == "new" || invoice.Status == "expired") { - if (accounting.Paid >= accounting.TotalDue) + if (accounting.Paid >= accounting.MinimumTotalDue) { if (invoice.Status == "new") { @@ -96,17 +98,17 @@ namespace BTCPayServer.HostedServices } } - if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial") + if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial") { - invoice.ExceptionStatus = "paidPartial"; - context.MarkDirty(); + invoice.ExceptionStatus = "paidPartial"; + context.MarkDirty(); } } // Just make sure RBF did not cancelled a payment if (invoice.Status == "paid") { - if (accounting.Paid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver") + if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver") { invoice.ExceptionStatus = null; context.MarkDirty(); @@ -118,7 +120,7 @@ namespace BTCPayServer.HostedServices context.MarkDirty(); } - if (accounting.Paid < accounting.TotalDue) + if (accounting.Paid < accounting.MinimumTotalDue) { invoice.Status = "new"; invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial"; @@ -134,14 +136,14 @@ namespace BTCPayServer.HostedServices (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed - (confirmedAccounting.Paid < accounting.TotalDue)) + (confirmedAccounting.Paid < accounting.MinimumTotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm")); invoice.Status = "invalid"; context.MarkDirty(); } - else if (confirmedAccounting.Paid >= accounting.TotalDue) + else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed")); @@ -153,7 +155,7 @@ namespace BTCPayServer.HostedServices if (invoice.Status == "confirmed") { var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network)); - if (completedAccounting.Paid >= accounting.TotalDue) + if (completedAccounting.Paid >= accounting.MinimumTotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed")); invoice.Status = "complete"; @@ -289,7 +291,7 @@ namespace BTCPayServer.HostedServices if (updateContext.Dirty) { await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus); - updateContext.Events.Add(new InvoiceDataChangedEvent(invoice)); + updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice)); } foreach (var evt in updateContext.Events) diff --git a/BTCPayServer/HostedServices/NBXplorerWaiter.cs b/BTCPayServer/HostedServices/NBXplorerWaiter.cs index 3ac9b52ce..72eb0f9dc 100644 --- a/BTCPayServer/HostedServices/NBXplorerWaiter.cs +++ b/BTCPayServer/HostedServices/NBXplorerWaiter.cs @@ -192,7 +192,7 @@ namespace BTCPayServer.HostedServices { State = NBXplorerState.NotConnected; status = null; - _Aggregator.Publish(new NBXplorerErrorEvent(_Network, error)); + Logs.PayServer.LogError($"{_Network.CryptoCode}: NBXplorer error `{error}`"); } _Dashboard.Publish(_Network, State, status, error); diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index aa3189932..fe6216073 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -12,7 +12,6 @@ using NBitcoin; using BTCPayServer.Data; using Microsoft.EntityFrameworkCore; using System.IO; -using Microsoft.Data.Sqlite; using NBXplorer; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; diff --git a/BTCPayServer/Hosting/BTCpayMiddleware.cs b/BTCPayServer/Hosting/BTCpayMiddleware.cs index 2f229d12d..a74c8d723 100644 --- a/BTCPayServer/Hosting/BTCpayMiddleware.cs +++ b/BTCPayServer/Hosting/BTCpayMiddleware.cs @@ -79,12 +79,13 @@ namespace BTCPayServer.Hosting if (!httpContext.Request.Path.HasValue) return false; + var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase); var path = httpContext.Request.Path.Value; if ( bitpayAuth && path == "/invoices" && httpContext.Request.Method == "POST" && - (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase)) + isJson) return true; if ( @@ -94,9 +95,9 @@ namespace BTCPayServer.Hosting return true; if ( - bitpayAuth && path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) && - httpContext.Request.Method == "GET") + httpContext.Request.Method == "GET" && + (isJson || httpContext.Request.Query.ContainsKey("token"))) return true; if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) && diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index f983f7603..b5107aa6f 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -35,7 +35,6 @@ using Hangfire.Annotations; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Threading; using Microsoft.Extensions.Options; -using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.AspNetCore.Mvc.Cors.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core; using System.Net; @@ -104,10 +103,6 @@ namespace BTCPayServer.Hosting b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin(); }); }); - services.Configure>(o => - { - o.Value.DeveloperMode = _Env.IsDevelopment(); - }); // Needed to debug U2F for ledger support //services.Configure(kestrel => @@ -146,12 +141,8 @@ namespace BTCPayServer.Hosting if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); - app.UseBrowserLink(); } - //App insight do not that by itself... - loggerFactory.AddApplicationInsights(prov, LogLevel.Information); - app.UsePayServer(); app.UseStaticFiles(); app.UseAuthentication(); diff --git a/BTCPayServer/Logging/ConsoleLogger.cs b/BTCPayServer/Logging/ConsoleLogger.cs index ba4e7650b..bb442cdbb 100644 --- a/BTCPayServer/Logging/ConsoleLogger.cs +++ b/BTCPayServer/Logging/ConsoleLogger.cs @@ -1,13 +1,14 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Console; -using Microsoft.Extensions.Logging.Console.Internal; -using System; +using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions.Internal; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Logging.Console.Internal; namespace BTCPayServer.Logging { @@ -20,19 +21,18 @@ namespace BTCPayServer.Logging } public ILogger CreateLogger(string categoryName) { - return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor); + return new CustomerConsoleLogger(categoryName, (a, b) => true, null, _Processor); } public void Dispose() { - } } /// /// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category /// - public class CustomConsoleLogger : ILogger + public class CustomerConsoleLogger : ILogger { private static readonly string _loglevelPadding = ": "; private static readonly string _messagePadding; @@ -47,19 +47,33 @@ namespace BTCPayServer.Logging [ThreadStatic] private static StringBuilder _logBuilder; - static CustomConsoleLogger() + static CustomerConsoleLogger() { var logLevelString = GetLogLevelString(LogLevel.Information); _messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length); _newLineWithMessagePadding = Environment.NewLine + _messagePadding; } - public CustomConsoleLogger(string name, Func filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor) + public CustomerConsoleLogger(string name, Func filter, bool includeScopes) + : this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor()) { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Filter = filter ?? ((category, logLevel) => true); - IncludeScopes = includeScopes; + } + internal CustomerConsoleLogger(string name, Func filter, IExternalScopeProvider scopeProvider) + : this(name, filter, scopeProvider, new ConsoleLoggerProcessor()) + { + } + + internal CustomerConsoleLogger(string name, Func filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + Filter = filter ?? ((category, logLevel) => true); + ScopeProvider = scopeProvider; _queueProcessor = loggerProcessor; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -80,7 +94,12 @@ namespace BTCPayServer.Logging } set { - _queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value)); + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _queueProcessor.Console = value; } } @@ -92,13 +111,13 @@ namespace BTCPayServer.Logging } set { - _filter = value ?? throw new ArgumentNullException(nameof(value)); - } - } + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } - public bool IncludeScopes - { - get; set; + _filter = value; + } } public string Name @@ -106,6 +125,16 @@ namespace BTCPayServer.Logging get; } + internal IExternalScopeProvider ScopeProvider + { + get; set; + } + + public bool DisableColors + { + get; set; + } + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { if (!IsEnabled(logLevel)) @@ -154,10 +183,7 @@ namespace BTCPayServer.Logging while (lenAfter++ < 18) logBuilder.Append(" "); // scope information - if (IncludeScopes) - { - GetScopeInformation(logBuilder); - } + GetScopeInformation(logBuilder); if (!string.IsNullOrEmpty(message)) { @@ -202,18 +228,15 @@ namespace BTCPayServer.Logging public bool IsEnabled(LogLevel logLevel) { + if (logLevel == LogLevel.None) + { + return false; + } + return Filter(Name, logLevel); } - public IDisposable BeginScope(TState state) - { - if (state == null) - { - throw new ArgumentNullException(nameof(state)); - } - - return ConsoleLogScope.Push(Name, state); - } + public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance; private static string GetLogLevelString(LogLevel logLevel) { @@ -238,6 +261,11 @@ namespace BTCPayServer.Logging private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel) { + if (DisableColors) + { + return new ConsoleColors(null, null); + } + // We must explicitly set the background color if we are setting the foreground color, // since just setting one can look bad on the users console. switch (logLevel) @@ -259,30 +287,25 @@ namespace BTCPayServer.Logging } } - private void GetScopeInformation(StringBuilder builder) + private void GetScopeInformation(StringBuilder stringBuilder) { - var current = ConsoleLogScope.Current; - string scopeLog = string.Empty; - var length = builder.Length; - - while (current != null) + var scopeProvider = ScopeProvider; + if (scopeProvider != null) { - if (length == builder.Length) - { - scopeLog = $"=> {current}"; - } - else - { - scopeLog = $"=> {current} "; - } + var initialLength = stringBuilder.Length; - builder.Insert(length, scopeLog); - current = current.Parent; - } - if (builder.Length > length) - { - builder.Insert(length, _messagePadding); - builder.AppendLine(); + scopeProvider.ForEachScope((scope, state) => + { + var (builder, length) = state; + var first = length == builder.Length; + builder.Append(first ? "=> " : " => ").Append(scope); + }, (stringBuilder, initialLength)); + + if (stringBuilder.Length > initialLength) + { + stringBuilder.Insert(initialLength, _messagePadding); + stringBuilder.AppendLine(); + } } } @@ -333,9 +356,9 @@ namespace BTCPayServer.Logging // Start Console message queue processor _outputTask = Task.Factory.StartNew( ProcessLogQueue, - this, - default(CancellationToken), - TaskCreationOptions.LongRunning, TaskScheduler.Default); + state: this, + cancellationToken: default(CancellationToken), + creationOptions: TaskCreationOptions.LongRunning, scheduler: TaskScheduler.Default); } public virtual void EnqueueMessage(LogMessageEntry message) diff --git a/BTCPayServer/Migrations/20170913143004_Init.cs b/BTCPayServer/Migrations/20170913143004_Init.cs index 7d58bd4b1..149900b1f 100644 --- a/BTCPayServer/Migrations/20170913143004_Init.cs +++ b/BTCPayServer/Migrations/20170913143004_Init.cs @@ -12,10 +12,10 @@ namespace BTCPayServer.Migrations name: "AspNetRoles", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), - Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), - NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true) + Id = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true) }, constraints: table => { @@ -26,21 +26,21 @@ namespace BTCPayServer.Migrations name: "AspNetUsers", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - AccessFailedCount = table.Column(type: "INTEGER", nullable: false), - ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), - Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Id = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Email = table.Column(maxLength: 256, nullable: true), EmailConfirmed = table.Column(nullable: false), LockoutEnabled = table.Column(nullable: false), LockoutEnd = table.Column(nullable: true), - NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), - NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), - PasswordHash = table.Column(type: "TEXT", nullable: true), - PhoneNumber = table.Column(type: "TEXT", nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + PasswordHash = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), PhoneNumberConfirmed = table.Column(nullable: false), - SecurityStamp = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(nullable: true), TwoFactorEnabled = table.Column(nullable: false), - UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true) + UserName = table.Column(maxLength: 256, nullable: true) }, constraints: table => { @@ -51,12 +51,12 @@ namespace BTCPayServer.Migrations name: "Stores", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - DerivationStrategy = table.Column(type: "TEXT", nullable: true), - SpeedPolicy = table.Column(type: "INTEGER", nullable: false), + Id = table.Column(nullable: false), + DerivationStrategy = table.Column(nullable: true), + SpeedPolicy = table.Column(nullable: false), StoreCertificate = table.Column(nullable: true), - StoreName = table.Column(type: "TEXT", nullable: true), - StoreWebsite = table.Column(type: "TEXT", nullable: true) + StoreName = table.Column(nullable: true), + StoreWebsite = table.Column(nullable: true) }, constraints: table => { @@ -67,11 +67,11 @@ namespace BTCPayServer.Migrations name: "AspNetRoleClaims", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(nullable: false) .Annotation("Sqlite:Autoincrement", true), - ClaimType = table.Column(type: "TEXT", nullable: true), - ClaimValue = table.Column(type: "TEXT", nullable: true), - RoleId = table.Column(type: "TEXT", nullable: false) + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + RoleId = table.Column(nullable: false) }, constraints: table => { @@ -88,11 +88,11 @@ namespace BTCPayServer.Migrations name: "AspNetUserClaims", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(nullable: false) .Annotation("Sqlite:Autoincrement", true), - ClaimType = table.Column(type: "TEXT", nullable: true), - ClaimValue = table.Column(type: "TEXT", nullable: true), - UserId = table.Column(type: "TEXT", nullable: false) + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + UserId = table.Column(nullable: false) }, constraints: table => { @@ -109,10 +109,10 @@ namespace BTCPayServer.Migrations name: "AspNetUserLogins", columns: table => new { - LoginProvider = table.Column(type: "TEXT", nullable: false), - ProviderKey = table.Column(type: "TEXT", nullable: false), - ProviderDisplayName = table.Column(type: "TEXT", nullable: true), - UserId = table.Column(type: "TEXT", nullable: false) + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) }, constraints: table => { @@ -129,8 +129,8 @@ namespace BTCPayServer.Migrations name: "AspNetUserRoles", columns: table => new { - UserId = table.Column(type: "TEXT", nullable: false), - RoleId = table.Column(type: "TEXT", nullable: false) + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) }, constraints: table => { @@ -153,10 +153,10 @@ namespace BTCPayServer.Migrations name: "AspNetUserTokens", columns: table => new { - UserId = table.Column(type: "TEXT", nullable: false), - LoginProvider = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", nullable: false), - Value = table.Column(type: "TEXT", nullable: true) + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) }, constraints: table => { @@ -173,15 +173,15 @@ namespace BTCPayServer.Migrations name: "Invoices", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), + Id = table.Column(nullable: false), Blob = table.Column(nullable: true), Created = table.Column(nullable: false), - CustomerEmail = table.Column(type: "TEXT", nullable: true), - ExceptionStatus = table.Column(type: "TEXT", nullable: true), - ItemCode = table.Column(type: "TEXT", nullable: true), - OrderId = table.Column(type: "TEXT", nullable: true), - Status = table.Column(type: "TEXT", nullable: true), - StoreDataId = table.Column(type: "TEXT", nullable: true) + CustomerEmail = table.Column(nullable: true), + ExceptionStatus = table.Column(nullable: true), + ItemCode = table.Column(nullable: true), + OrderId = table.Column(nullable: true), + Status = table.Column(nullable: true), + StoreDataId = table.Column(nullable: true) }, constraints: table => { @@ -198,9 +198,9 @@ namespace BTCPayServer.Migrations name: "UserStore", columns: table => new { - ApplicationUserId = table.Column(type: "TEXT", nullable: false), - StoreDataId = table.Column(type: "TEXT", nullable: false), - Role = table.Column(type: "TEXT", nullable: true) + ApplicationUserId = table.Column(nullable: false), + StoreDataId = table.Column(nullable: false), + Role = table.Column(nullable: true) }, constraints: table => { @@ -223,9 +223,9 @@ namespace BTCPayServer.Migrations name: "Payments", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), + Id = table.Column(nullable: false), Blob = table.Column(nullable: true), - InvoiceDataId = table.Column(type: "TEXT", nullable: true) + InvoiceDataId = table.Column(nullable: true) }, constraints: table => { @@ -242,9 +242,9 @@ namespace BTCPayServer.Migrations name: "RefundAddresses", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), + Id = table.Column(nullable: false), Blob = table.Column(nullable: true), - InvoiceDataId = table.Column(type: "TEXT", nullable: true) + InvoiceDataId = table.Column(nullable: true) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20170926073744_Settings.cs b/BTCPayServer/Migrations/20170926073744_Settings.cs index 60d8f3fde..efca94c61 100644 --- a/BTCPayServer/Migrations/20170926073744_Settings.cs +++ b/BTCPayServer/Migrations/20170926073744_Settings.cs @@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations name: "Settings", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - Value = table.Column(type: "TEXT", nullable: true) + Id = table.Column(nullable: false), + Value = table.Column(nullable: true) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20171006013443_AddressMapping.cs b/BTCPayServer/Migrations/20171006013443_AddressMapping.cs index aba4aa2ed..18a312349 100644 --- a/BTCPayServer/Migrations/20171006013443_AddressMapping.cs +++ b/BTCPayServer/Migrations/20171006013443_AddressMapping.cs @@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations name: "AddressInvoices", columns: table => new { - Address = table.Column(type: "TEXT", nullable: false), - InvoiceDataId = table.Column(type: "TEXT", nullable: true) + Address = table.Column(nullable: false), + InvoiceDataId = table.Column(nullable: true) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20171010082424_Tokens.cs b/BTCPayServer/Migrations/20171010082424_Tokens.cs index dc2669da3..9847f8a15 100644 --- a/BTCPayServer/Migrations/20171010082424_Tokens.cs +++ b/BTCPayServer/Migrations/20171010082424_Tokens.cs @@ -12,13 +12,13 @@ namespace BTCPayServer.Migrations name: "PairedSINData", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - Facade = table.Column(type: "TEXT", nullable: true), - Label = table.Column(type: "TEXT", nullable: true), - Name = table.Column(type: "TEXT", nullable: true), + Id = table.Column(nullable: false), + Facade = table.Column(nullable: true), + Label = table.Column(nullable: true), + Name = table.Column(nullable: true), PairingTime = table.Column(nullable: false), - SIN = table.Column(type: "TEXT", nullable: true), - StoreDataId = table.Column(type: "TEXT", nullable: true) + SIN = table.Column(nullable: true), + StoreDataId = table.Column(nullable: true) }, constraints: table => { @@ -29,15 +29,15 @@ namespace BTCPayServer.Migrations name: "PairingCodes", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), + Id = table.Column(nullable: false), DateCreated = table.Column(nullable: false), Expiration = table.Column(nullable: false), - Facade = table.Column(type: "TEXT", nullable: true), - Label = table.Column(type: "TEXT", nullable: true), - Name = table.Column(type: "TEXT", nullable: true), - SIN = table.Column(type: "TEXT", nullable: true), - StoreDataId = table.Column(type: "TEXT", nullable: true), - TokenValue = table.Column(type: "TEXT", nullable: true) + Facade = table.Column(nullable: true), + Label = table.Column(nullable: true), + Name = table.Column(nullable: true), + SIN = table.Column(nullable: true), + StoreDataId = table.Column(nullable: true), + TokenValue = table.Column(nullable: true) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs b/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs index 5ac527ac3..1711c54e6 100644 --- a/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs +++ b/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs @@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations name: "PendingInvoices", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false) + Id = table.Column(nullable: false) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20171024163354_RenewUsedAddresses.cs b/BTCPayServer/Migrations/20171024163354_RenewUsedAddresses.cs index d852c5224..83b785dc4 100644 --- a/BTCPayServer/Migrations/20171024163354_RenewUsedAddresses.cs +++ b/BTCPayServer/Migrations/20171024163354_RenewUsedAddresses.cs @@ -17,8 +17,8 @@ namespace BTCPayServer.Migrations name: "HistoricalAddressInvoices", columns: table => new { - InvoiceDataId = table.Column(type: "TEXT", nullable: false), - Address = table.Column(type: "TEXT", nullable: false), + InvoiceDataId = table.Column(nullable: false), + Address = table.Column(nullable: false), Assigned = table.Column(nullable: false), UnAssigned = table.Column(nullable: true) }, diff --git a/BTCPayServer/Models/ErrorViewModel.cs b/BTCPayServer/Models/ErrorViewModel.cs deleted file mode 100644 index b32ee4e43..000000000 --- a/BTCPayServer/Models/ErrorViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 4ff6f21ed..fd3bab95c 100644 --- a/BTCPayServer/Models/InvoiceResponse.cs +++ b/BTCPayServer/Models/InvoiceResponse.cs @@ -79,7 +79,7 @@ namespace BTCPayServer.Models //"price":5 [JsonProperty("price")] - public double Price + public decimal Price { get; set; } @@ -94,7 +94,7 @@ namespace BTCPayServer.Models //"exRates":{"USD":4320.02} [JsonProperty("exRates")] [Obsolete("Use CryptoInfo.ExRates instead")] - public Dictionary ExRates + public Dictionary ExRates { get; set; } diff --git a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs index 39b77c8dc..9ac99d667 100644 --- a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs +++ b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs @@ -14,7 +14,7 @@ namespace BTCPayServer.Models.InvoicingModels Currency = "USD"; } [Required] - public double? Amount + public decimal? Amount { get; set; } diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index 594e28c40..63385532d 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -49,6 +49,8 @@ namespace BTCPayServer.Models.InvoicingModels { get; set; } + public bool ShowCheckout { get; set; } + public string ExceptionStatus { get; set; } public string AmountCurrency { get; set; diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index ee4fbf318..c2fadcdd3 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -85,5 +85,13 @@ namespace BTCPayServer.Models.StoreViewModels { get; set; } = new List(); + + [Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")] + [Range(0, 100)] + public double PaymentTolerance + { + get; + set; + } } } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs index c0d29584a..9026aaf13 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs @@ -68,6 +68,10 @@ namespace BTCPayServer.Payments.Bitcoin { return ConfirmationCount >= 1; } + else if (speedPolicy == SpeedPolicy.LowMediumSpeed) + { + return ConfirmationCount >= 2; + } else if (speedPolicy == SpeedPolicy.LowSpeed) { return ConfirmationCount >= 6; diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index d3f2856d5..4b0e6129d 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -28,7 +28,7 @@ namespace BTCPayServer.Payments.Bitcoin { EventAggregator _Aggregator; ExplorerClientProvider _ExplorerClients; - IApplicationLifetime _Lifetime; + Microsoft.Extensions.Hosting.IApplicationLifetime _Lifetime; InvoiceRepository _InvoiceRepository; private TaskCompletionSource _RunningTask; private CancellationTokenSource _Cts; @@ -39,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin BTCPayWalletProvider wallets, InvoiceRepository invoiceRepository, BTCPayNetworkProvider networkProvider, - EventAggregator aggregator, IApplicationLifetime lifetime) + EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime) { PollInterval = TimeSpan.FromMinutes(1.0); _Wallets = wallets; diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index e3e3a6361..76fc5cb18 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -36,17 +36,25 @@ namespace BTCPayServer.Payments.Lightning expiry = TimeSpan.FromSeconds(1); LightningInvoice lightningInvoice = null; - try + + string description = storeBlob.LightningDescriptionTemplate; + description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase); + using (var cts = new CancellationTokenSource(5000)) { - string description = storeBlob.LightningDescriptionTemplate; - description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase); - lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry); - } - catch (Exception ex) - { - throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex); + try + { + lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry, cts.Token); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner"); + } + catch (Exception ex) + { + throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex); + } } var nodeInfo = await test; return new LightningLikePaymentMethodDetails() @@ -62,34 +70,36 @@ namespace BTCPayServer.Payments.Lightning if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) throw new PaymentMethodUnavailableException($"Full node not available"); - var cts = new CancellationTokenSource(5000); - var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network); - LightningNodeInformation info = null; - try + using (var cts = new CancellationTokenSource(5000)) { - info = await client.GetInfo(cts.Token); - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner"); - } - catch (Exception ex) - { - throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})"); - } + var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network); + LightningNodeInformation info = null; + try + { + info = await client.GetInfo(cts.Token); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner"); + } + catch (Exception ex) + { + throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})"); + } - if (info.Address == null) - { - throw new PaymentMethodUnavailableException($"No lightning node public address has been configured"); - } + if (info.Address == null) + { + throw new PaymentMethodUnavailableException($"No lightning node public address has been configured"); + } - var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight); - if (blocksGap > 10) - { - throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)"); - } + var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight); + if (blocksGap > 10) + { + throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)"); + } - return new NodeInfo(info.NodeId, info.Address, info.P2PPort); + return new NodeInfo(info.NodeId, info.Address, info.P2PPort); + } } public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation) diff --git a/BTCPayServer/Program.cs b/BTCPayServer/Program.cs index c284c41a0..aed3bf2d9 100644 --- a/BTCPayServer/Program.cs +++ b/BTCPayServer/Program.cs @@ -40,7 +40,6 @@ namespace BTCPayServer .UseIISIntegration() .UseContentRoot(Directory.GetCurrentDirectory()) .UseConfiguration(conf) - .UseApplicationInsights() .ConfigureLogging(l => { l.AddFilter("Microsoft", LogLevel.Error); diff --git a/BTCPayServer/Rating/CurrencyPair.cs b/BTCPayServer/Rating/CurrencyPair.cs index 7ba9cfe5a..adeacd603 100644 --- a/BTCPayServer/Rating/CurrencyPair.cs +++ b/BTCPayServer/Rating/CurrencyPair.cs @@ -45,6 +45,11 @@ namespace BTCPayServer.Rating var currencyPair = splitted[0]; if (currencyPair.Length < 6 || currencyPair.Length > 10) return false; + if (currencyPair.Length == 6) + { + value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3)); + return true; + } for (int i = 3; i < 5; i++) { var potentialCryptoName = currencyPair.Substring(0, i); @@ -90,5 +95,10 @@ namespace BTCPayServer.Rating { return $"{Left}_{Right}"; } + + public CurrencyPair Inverse() + { + return new CurrencyPair(Right, Left); + } } } diff --git a/BTCPayServer/Rating/RateRules.cs b/BTCPayServer/Rating/RateRules.cs index 61772a14f..d4f72618f 100644 --- a/BTCPayServer/Rating/RateRules.cs +++ b/BTCPayServer/Rating/RateRules.cs @@ -133,28 +133,31 @@ namespace BTCPayServer.Rating if (currencyPair.Left == "X" || currencyPair.Right == "X") throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency"); var candidate = FindBestCandidate(currencyPair); - if (GlobalMultiplier != decimal.One) { candidate = CreateExpression($"({candidate}) * {GlobalMultiplier.ToString(CultureInfo.InvariantCulture)}"); } return new RateRule(this, currencyPair, candidate); } - + public ExpressionSyntax FindBestCandidate(CurrencyPair p) { - var candidates = new List<(CurrencyPair Pair, int Prioriy, ExpressionSyntax Expression)>(); + var invP = p.Inverse(); + var candidates = new List<(CurrencyPair Pair, int Prioriy, ExpressionSyntax Expression, bool Inverse)>(); foreach (var pair in new[] { - (Pair: p, Priority: 0), - (Pair: new CurrencyPair(p.Left, "X"), Priority: 1), - (Pair: new CurrencyPair("X", p.Right), Priority: 1), - (Pair: new CurrencyPair("X", "X"), Priority: 2) + (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: 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) }) { if (ruleList.ExpressionsByPair.TryGetValue(pair.Pair, out var expression)) { - candidates.Add((pair.Pair, pair.Priority, expression.Expression)); + candidates.Add((pair.Pair, pair.Priority, expression.Expression, pair.Inverse)); } } if (candidates.Count == 0) @@ -163,8 +166,9 @@ namespace BTCPayServer.Rating .OrderBy(c => c.Prioriy) .ThenBy(c => c.Expression.Span.Start) .First(); - - return best.Expression; + return best.Inverse + ? CreateExpression($"1 / {invP}") + : best.Expression; } internal static ExpressionSyntax CreateExpression(string str) @@ -364,7 +368,7 @@ namespace BTCPayServer.Rating string _ExchangeName = null; public List Errors = new List(); - const int MaxNestedCount = 6; + const int MaxNestedCount = 8; public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) { if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair)) @@ -413,10 +417,21 @@ namespace BTCPayServer.Rating public RateRule(RateRules parent, CurrencyPair currencyPair, SyntaxNode candidate) { + _CurrencyPair = currencyPair; flatten = new FlattenExpressionRewriter(parent, currencyPair); this.expression = flatten.Visit(candidate); } + + private readonly CurrencyPair _CurrencyPair; + public CurrencyPair CurrencyPair + { + get + { + return _CurrencyPair; + } + } + public ExchangeRates ExchangeRates { get diff --git a/BTCPayServer/Security/BitpayClaimsFilter.cs b/BTCPayServer/Security/BitpayClaimsFilter.cs index c9c4ea489..463ab57cf 100644 --- a/BTCPayServer/Security/BitpayClaimsFilter.cs +++ b/BTCPayServer/Security/BitpayClaimsFilter.cs @@ -79,13 +79,13 @@ namespace BTCPayServer.Security if (storeId != null) { var identity = ((ClaimsIdentity)context.HttpContext.User.Identity); - identity.AddClaim(new Claim(Claims.OwnStore, storeId)); + identity.AddClaim(new Claim(Policies.CanUseStore.Key, storeId)); var store = await _StoreRepository.FindStore(storeId); context.HttpContext.SetStoreData(store); } else if (failedAuth) { - throw new BitpayHttpException(401, "Can't access to store"); + throw new BitpayHttpException(401, "Invalid credentials"); } } } diff --git a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs index f5f1f8d3b..7a55d51d5 100644 --- a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs +++ b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs @@ -39,6 +39,8 @@ namespace BTCPayServer.Services.Fees ExplorerClient _ExplorerClient; public async Task GetFeeRateAsync() { + if (!_ExplorerClient.Network.SupportEstimatesSmartFee) + return _Factory.Fallback; try { return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate; diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index a44f07de7..c7695a365 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -118,18 +118,7 @@ namespace BTCPayServer.Services } } - public async Task SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy) - { - if (network == null) - throw new ArgumentNullException(nameof(network)); - if (strategy == null) - throw new ArgumentNullException(nameof(strategy)); - if (!strategy.Segwit) - return false; - return await GetKeyPath(_Ledger, network, strategy) != null; - } - - private static async Task GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy) + public async Task GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy) { List derivations = new List(); if(network.NBitcoinNetwork.Consensus.SupportSegwit) @@ -143,7 +132,7 @@ namespace BTCPayServer.Services { try { - var extpubkey = await GetExtPubKey(ledger, network, account, true); + var extpubkey = await GetExtPubKey(_Ledger, network, account, true); if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey) { foundKeyPath = account; @@ -159,79 +148,12 @@ namespace BTCPayServer.Services return foundKeyPath; } - public async Task SendToAddress(DirectDerivationStrategy strategy, - ReceivedCoin[] coins, BTCPayNetwork network, - (IDestination destination, Money amount, bool substractFees)[] send, - FeeRate feeRate, - IDestination changeAddress, - KeyPath changeKeyPath, - FeeRate minTxRelayFee) + public async Task SignTransactionAsync(SignatureRequest[] signatureRequests, + Transaction unsigned, + KeyPath changeKeyPath) { - if (strategy == null) - throw new ArgumentNullException(nameof(strategy)); - if (network == null) - throw new ArgumentNullException(nameof(network)); - if (feeRate == null) - throw new ArgumentNullException(nameof(feeRate)); - if (changeAddress == null) - throw new ArgumentNullException(nameof(changeAddress)); - if (feeRate.FeePerK <= Money.Zero) - { - throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero"); - } - - foreach (var element in send) - { - if (element.destination == null) - throw new ArgumentNullException(nameof(element.destination)); - if (element.amount == null) - throw new ArgumentNullException(nameof(element.amount)); - if (element.amount <= Money.Zero) - throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero"); - } - - var foundKeyPath = await GetKeyPath(Ledger, network, strategy); - - if (foundKeyPath == null) - { - throw new HardwareWalletException($"This store is not configured to use this ledger"); - } - - TransactionBuilder builder = new TransactionBuilder(); - builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee; - builder.SetConsensusFactory(network.NBitcoinNetwork); - builder.AddCoins(coins.Select(c=>c.Coin).ToArray()); - - foreach (var element in send) - { - builder.Send(element.destination, element.amount); - if (element.substractFees) - builder.SubtractFees(); - } - builder.SetChange(changeAddress); - builder.SendEstimatedFees(feeRate); - builder.Shuffle(); - var unsigned = builder.BuildTransaction(false); - - var keypaths = new Dictionary(); - foreach(var c in coins) - { - keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); - } - - var hasChange = unsigned.Outputs.Count == 2; - var usedCoins = builder.FindSpentCoins(unsigned); _Transport.Timeout = TimeSpan.FromMinutes(5); - var fullySigned = await Ledger.SignTransactionAsync( - usedCoins.Select(c => new SignatureRequest - { - InputCoin = c, - KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]), - PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey - }).ToArray(), - unsigned, - hasChange ? foundKeyPath.Derive(changeKeyPath) : null); - return fullySigned; + return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath); } } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 0468de9f8..1142e792d 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -100,7 +100,8 @@ namespace BTCPayServer.Services.Invoices { HighSpeed = 0, MediumSpeed = 1, - LowSpeed = 2 + LowSpeed = 2, + LowMediumSpeed = 3 } public class InvoiceEntity { @@ -314,6 +315,7 @@ namespace BTCPayServer.Services.Invoices } public bool ExtendedNotifications { get; set; } public List Events { get; internal set; } + public double PaymentTolerance { get; set; } public bool IsExpired() { @@ -356,22 +358,22 @@ namespace BTCPayServer.Services.Invoices cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(); cryptoInfo.Address = info.GetPaymentMethodDetails()?.GetPaymentDestination(); - cryptoInfo.ExRates = new Dictionary + cryptoInfo.ExRates = new Dictionary { - { ProductInformation.Currency, (double)cryptoInfo.Rate } + { ProductInformation.Currency, cryptoInfo.Rate } }; var paymentId = info.GetId(); var scheme = info.Network.UriScheme; cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}"; - if (paymentId.PaymentType == PaymentTypes.BTCLike) { + var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode; cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() { - BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={cryptoInfo.Url}", - BIP72b = $"{scheme}:?r={cryptoInfo.Url}", - BIP73 = cryptoInfo.Url, + BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", + BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", + BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"), BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}", }; } @@ -523,6 +525,10 @@ namespace BTCPayServer.Services.Invoices /// Total amount of network fee to pay to the invoice /// public Money NetworkFee { get; set; } + /// + /// Minimum required to be paid in order to accept invocie as paid + /// + public Money MinimumTotalDue { get; set; } } public class PaymentMethod @@ -671,6 +677,7 @@ namespace BTCPayServer.Services.Invoices accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero); accounting.DueUncapped = accounting.TotalDue - accounting.Paid; accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost; + accounting.MinimumTotalDue = Money.Max(Money.Satoshis(1), Money.Satoshis(accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m)))); return accounting; } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index d4c90b718..33253d0ec 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -436,6 +436,18 @@ namespace BTCPayServer.Services.Invoices query = query.Where(i => statusSet.Contains(i.Status)); } + if(queryObject.Unusual != null) + { + var unused = queryObject.Unusual.Value; + query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null)); + } + + if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0) + { + var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet(); + query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus)); + } + query = query.OrderByDescending(q => q.Created); if (queryObject.Skip != null) @@ -451,6 +463,29 @@ namespace BTCPayServer.Services.Invoices } + private string NormalizeExceptionStatus(string status) + { + status = status.ToLowerInvariant(); + switch (status) + { + case "paidover": + case "over": + case "overpaid": + status = "paidOver"; + break; + case "paidlate": + case "late": + status = "paidLate"; + break; + case "paidpartial": + case "underpaid": + case "partial": + status = "paidPartial"; + break; + } + return status; + } + public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network) { if (outputs.Length == 0) @@ -614,10 +649,18 @@ namespace BTCPayServer.Services.Invoices get; set; } + public bool? Unusual { get; set; } + public string[] Status { get; set; } + + public string[] ExceptionStatus + { + get; set; + } + public string InvoiceId { get; diff --git a/BTCPayServer/Services/Mails/EmailSender.cs b/BTCPayServer/Services/Mails/EmailSender.cs index c97c2cbbd..2e51db317 100644 --- a/BTCPayServer/Services/Mails/EmailSender.cs +++ b/BTCPayServer/Services/Mails/EmailSender.cs @@ -24,8 +24,8 @@ namespace BTCPayServer.Services.Mails } public async Task SendEmailAsync(string email, string subject, string message) { - var settings = await _Repository.GetSettingAsync(); - if (settings == null) + var settings = await _Repository.GetSettingAsync() ?? new EmailSettings(); + if (!settings.IsComplete()) { Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); return; @@ -36,8 +36,8 @@ namespace BTCPayServer.Services.Mails public async Task SendMailCore(string email, string subject, string message) { - var settings = await _Repository.GetSettingAsync(); - if (settings == null) + var settings = await _Repository.GetSettingAsync() ?? new EmailSettings(); + if (!settings.IsComplete()) throw new InvalidOperationException("Email settings not configured"); var smtp = settings.CreateSmtpClient(); MailMessage mail = new MailMessage(settings.From, email, subject, message); diff --git a/BTCPayServer/Services/Mails/EmailSettings.cs b/BTCPayServer/Services/Mails/EmailSettings.cs index 22840cc36..93b062eec 100644 --- a/BTCPayServer/Services/Mails/EmailSettings.cs +++ b/BTCPayServer/Services/Mails/EmailSettings.cs @@ -40,6 +40,18 @@ namespace BTCPayServer.Services.Mails get; set; } + public bool IsComplete() + { + SmtpClient smtp = null; + try + { + smtp = CreateSmtpClient(); + return true; + } + catch { } + return false; + } + public SmtpClient CreateSmtpClient() { SmtpClient client = new SmtpClient(Server, Port.Value); diff --git a/BTCPayServer/Services/PoliciesSettings.cs b/BTCPayServer/Services/PoliciesSettings.cs index 431e902eb..b1ef0a423 100644 --- a/BTCPayServer/Services/PoliciesSettings.cs +++ b/BTCPayServer/Services/PoliciesSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; @@ -8,12 +9,14 @@ namespace BTCPayServer.Services { public class PoliciesSettings { + [Display(Name = "Requires a confirmation mail for registering")] public bool RequiresConfirmedEmail { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + [Display(Name = "Disable registration")] public bool LockSubscription { get; set; } } } diff --git a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs index 91104b3ba..b115d1d30 100644 --- a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs @@ -35,7 +35,7 @@ namespace BTCPayServer.Services.Rates } IMemoryCache _Cache; private IOptions _CacheOptions; - + CurrencyNameTable _CurrencyTable; public IMemoryCache Cache { get @@ -46,10 +46,12 @@ namespace BTCPayServer.Services.Rates CoinAverageSettings _CoinAverageSettings; public BTCPayRateProviderFactory(IOptions cacheOptions, BTCPayNetworkProvider btcpayNetworkProvider, + CurrencyNameTable currencyTable, CoinAverageSettings coinAverageSettings) { if (cacheOptions == null) throw new ArgumentNullException(nameof(cacheOptions)); + _CurrencyTable = currencyTable; _CoinAverageSettings = coinAverageSettings; _Cache = new MemoryCache(cacheOptions); _CacheOptions = cacheOptions; @@ -161,6 +163,13 @@ namespace BTCPayServer.Services.Rates } rateRule.Reevaluate(); result.Value = rateRule.Value; + + var currencyData = _CurrencyTable?.GetCurrencyData(rateRule.CurrencyPair.Right); + if(currencyData != null && result.Value.HasValue) + { + result.Value = decimal.Round(result.Value.Value, currencyData.Divisibility, MidpointRounding.AwayFromZero); + } + result.Errors = rateRule.Errors; result.EvaluatedRule = rateRule.ToString(true); result.Rule = rateRule.ToString(false); diff --git a/BTCPayServer/Views/Account/ConfirmEmail.cshtml b/BTCPayServer/Views/Account/ConfirmEmail.cshtml index 55ceaee89..2322bae22 100644 --- a/BTCPayServer/Views/Account/ConfirmEmail.cshtml +++ b/BTCPayServer/Views/Account/ConfirmEmail.cshtml @@ -7,7 +7,7 @@
- @Html.Partial("_StatusMessage", "Thank you for confirming your email.") +
diff --git a/BTCPayServer/Views/Account/ForgotPassword.cshtml b/BTCPayServer/Views/Account/ForgotPassword.cshtml index fcc7538a9..0747d28b1 100644 --- a/BTCPayServer/Views/Account/ForgotPassword.cshtml +++ b/BTCPayServer/Views/Account/ForgotPassword.cshtml @@ -7,7 +7,7 @@
- @Html.Partial("_StatusMessage", TempData["StatusMessage"]) +
diff --git a/BTCPayServer/Views/Account/Register.cshtml b/BTCPayServer/Views/Account/Register.cshtml index 1c9175918..129ac0d7d 100644 --- a/BTCPayServer/Views/Account/Register.cshtml +++ b/BTCPayServer/Views/Account/Register.cshtml @@ -7,7 +7,7 @@
- @Html.Partial("_StatusMessage", TempData["StatusMessage"]) +
diff --git a/BTCPayServer/Views/Apps/ListApps.cshtml b/BTCPayServer/Views/Apps/ListApps.cshtml index f61af878b..df549a520 100644 --- a/BTCPayServer/Views/Apps/ListApps.cshtml +++ b/BTCPayServer/Views/Apps/ListApps.cshtml @@ -8,7 +8,7 @@
- @Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"]) +
diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index c8099fdf2..1e9304634 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -12,7 +12,7 @@
- @Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"]) +
diff --git a/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml b/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml index 18d8fe9d9..a4445ca83 100644 --- a/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml @@ -1,4 +1,6 @@ -@model ViewPointOfSaleViewModel +@inject BTCPayServer.HostedServices.CssThemeManager themeManager + +@model ViewPointOfSaleViewModel @{ ViewData["Title"] = Model.Title; Layout = null; @@ -11,13 +13,13 @@ - +

@Model.Title

-
+
@for(int i = 0; i < Model.Items.Length; i++) { @@ -34,7 +36,7 @@ {
- +
diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index 05d2bf70b..b50e3a707 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -16,7 +16,7 @@
- @Html.Partial("Checkout-Spinner") +
@@ -51,7 +51,7 @@ }
- @Html.Partial("Checkout-Spinner") +
@@ -151,7 +151,7 @@ @@ -323,7 +323,7 @@ @@ -430,7 +430,7 @@ diff --git a/BTCPayServer/Views/Invoice/Checkout.cshtml b/BTCPayServer/Views/Invoice/Checkout.cshtml index 6faf5668a..26268045f 100644 --- a/BTCPayServer/Views/Invoice/Checkout.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout.cshtml @@ -57,7 +57,7 @@ diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index 66f4674e4..343d3c613 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -28,7 +28,7 @@
- @Html.Partial("_StatusMessage", Model.StatusMessage) +
diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index e6699eda6..d6e6915f8 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -8,7 +8,7 @@
- @Html.Partial("_StatusMessage", Model.StatusMessage) +
@@ -20,14 +20,16 @@

You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.
- You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters + You can also apply filters to your search by searching for filtername:value, here is a list of supported filters

    -
  • storeid:id for filtering a specific store
  • -
  • status:(expired|invalid|complete|confirmed|paid|new) for filtering a specific status
  • +
  • storeid:id for filtering a specific store
  • +
  • status:(expired|invalid|complete|confirmed|paid|new) for filtering a specific status
  • +
  • exceptionstatus:(paidover|paidlate|paidpartial) for filtering a specific exception state
  • +
  • unusual:(true|false) for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)

- If you want two confirmed and complete invoices, duplicate the filter: `status:confirmed status:complete`. + If you want all confirmed and complete invoices, you can duplicate a filter status:confirmed status:complete.

@@ -95,7 +97,7 @@ } @invoice.AmountCurrency - @if(invoice.Status == "new") + @if(invoice.ShowCheckout) { Checkout - }Details diff --git a/BTCPayServer/Views/Manage/ChangePassword.cshtml b/BTCPayServer/Views/Manage/ChangePassword.cshtml index 8c3a4dcef..37b70dd98 100644 --- a/BTCPayServer/Views/Manage/ChangePassword.cshtml +++ b/BTCPayServer/Views/Manage/ChangePassword.cshtml @@ -4,7 +4,7 @@ }

@ViewData["Title"]

-@Html.Partial("_StatusMessage", Model.StatusMessage) +
diff --git a/BTCPayServer/Views/Manage/ExternalLogins.cshtml b/BTCPayServer/Views/Manage/ExternalLogins.cshtml index 488f7542f..c1ec86ddc 100644 --- a/BTCPayServer/Views/Manage/ExternalLogins.cshtml +++ b/BTCPayServer/Views/Manage/ExternalLogins.cshtml @@ -3,7 +3,7 @@ ViewData.SetActivePageAndTitle(ManageNavPages.ExternalLogins, "Manage your external logins"); } -@Html.Partial("_StatusMessage", Model.StatusMessage) + @if (Model.CurrentLogins?.Count > 0) {

Registered Logins

diff --git a/BTCPayServer/Views/Manage/Index.cshtml b/BTCPayServer/Views/Manage/Index.cshtml index c94ed5b22..83ff61360 100644 --- a/BTCPayServer/Views/Manage/Index.cshtml +++ b/BTCPayServer/Views/Manage/Index.cshtml @@ -4,7 +4,7 @@ }

@ViewData["Title"]

-@Html.Partial("_StatusMessage", Model.StatusMessage) +
diff --git a/BTCPayServer/Views/Manage/SetPassword.cshtml b/BTCPayServer/Views/Manage/SetPassword.cshtml index 6cd28ef16..a53bd7961 100644 --- a/BTCPayServer/Views/Manage/SetPassword.cshtml +++ b/BTCPayServer/Views/Manage/SetPassword.cshtml @@ -4,7 +4,7 @@ }

Set your password

-@Html.Partial("_StatusMessage", Model.StatusMessage) +

You do not have a local username/password for this site. Add a local account so you can log in without an external login. diff --git a/BTCPayServer/Views/Server/Emails.cshtml b/BTCPayServer/Views/Server/Emails.cshtml index 5cb41aae5..19d5a81b0 100644 --- a/BTCPayServer/Views/Server/Emails.cshtml +++ b/BTCPayServer/Views/Server/Emails.cshtml @@ -5,7 +5,7 @@

@ViewData["Title"]

-@Html.Partial("_StatusMessage", Model.StatusMessage) +
diff --git a/BTCPayServer/Views/Server/ListUsers.cshtml b/BTCPayServer/Views/Server/ListUsers.cshtml index 5326eefff..3f0a1619e 100644 --- a/BTCPayServer/Views/Server/ListUsers.cshtml +++ b/BTCPayServer/Views/Server/ListUsers.cshtml @@ -5,7 +5,7 @@

@ViewData["Title"]

-@Html.Partial("_StatusMessage", Model.StatusMessage) + diff --git a/BTCPayServer/Views/Server/Policies.cshtml b/BTCPayServer/Views/Server/Policies.cshtml index 69767f659..69f072da2 100644 --- a/BTCPayServer/Views/Server/Policies.cshtml +++ b/BTCPayServer/Views/Server/Policies.cshtml @@ -5,7 +5,7 @@

@ViewData["Title"]

-@Html.Partial("_StatusMessage", TempData["StatusMessage"]) +
diff --git a/BTCPayServer/Views/Server/Rates.cshtml b/BTCPayServer/Views/Server/Rates.cshtml index 63d6e4394..bbf407366 100644 --- a/BTCPayServer/Views/Server/Rates.cshtml +++ b/BTCPayServer/Views/Server/Rates.cshtml @@ -5,7 +5,7 @@

@ViewData["Title"]

-@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"]) +
diff --git a/BTCPayServer/Views/Server/Theme.cshtml b/BTCPayServer/Views/Server/Theme.cshtml index b9f71fae4..2580e0812 100644 --- a/BTCPayServer/Views/Server/Theme.cshtml +++ b/BTCPayServer/Views/Server/Theme.cshtml @@ -5,7 +5,7 @@

@ViewData["Title"]

-@Html.Partial("_StatusMessage", TempData["StatusMessage"]) +
diff --git a/BTCPayServer/Views/Server/User.cshtml b/BTCPayServer/Views/Server/User.cshtml index 47f4225ac..93039fe77 100644 --- a/BTCPayServer/Views/Server/User.cshtml +++ b/BTCPayServer/Views/Server/User.cshtml @@ -5,7 +5,7 @@

Modify User - @Model.Email

-@Html.Partial("_StatusMessage", Model.StatusMessage) +
diff --git a/BTCPayServer/Views/Shared/Error.cshtml b/BTCPayServer/Views/Shared/Error.cshtml deleted file mode 100644 index 086796acf..000000000 --- a/BTCPayServer/Views/Shared/Error.cshtml +++ /dev/null @@ -1,22 +0,0 @@ -@model ErrorViewModel -@{ - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if(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/Shared/_Layout.cshtml b/BTCPayServer/Views/Shared/_Layout.cshtml index 3a75e4c8e..2af9e84cd 100644 --- a/BTCPayServer/Views/Shared/_Layout.cshtml +++ b/BTCPayServer/Views/Shared/_Layout.cshtml @@ -31,7 +31,7 @@ @{ - if (ViewBag.AlwaysShrinkNavBar == null) + if(ViewBag.AlwaysShrinkNavBar == null) { ViewBag.AlwaysShrinkNavBar = true; } @@ -43,7 +43,7 @@
- @if (env.NetworkType != NBitcoin.NetworkType.Mainnet) + @if(env.NetworkType != NBitcoin.NetworkType.Mainnet) { @env.NetworkType.ToString() } @@ -53,9 +53,9 @@