diff --git a/BTCPayServer.Tests/CrowdfundTests.cs b/BTCPayServer.Tests/CrowdfundTests.cs index 23449ff89..3cd78f402 100644 --- a/BTCPayServer.Tests/CrowdfundTests.cs +++ b/BTCPayServer.Tests/CrowdfundTests.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Controllers; +using BTCPayServer.Crowdfund; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Hubs; diff --git a/BTCPayServer.Tests/MockDelay.cs b/BTCPayServer.Tests/MockDelay.cs new file mode 100644 index 000000000..93c61f4ed --- /dev/null +++ b/BTCPayServer.Tests/MockDelay.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BTCPayServer.Tests +{ + public class MockDelay : IDelay + { + class WaitObj + { + public DateTimeOffset Expiration; + public TaskCompletionSource CTS; + } + + List waits = new List(); + DateTimeOffset _Now = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + public async Task Wait(TimeSpan delay, CancellationToken cancellation) + { + WaitObj w = new WaitObj(); + w.Expiration = _Now + delay; + w.CTS = new TaskCompletionSource(); + using (cancellation.Register(() => + { + w.CTS.TrySetCanceled(); + })) + { + lock (waits) + { + waits.Add(w); + } + await w.CTS.Task; + } + } + + public void Advance(TimeSpan time) + { + _Now += time; + lock (waits) + { + foreach (var wait in waits.ToArray()) + { + if (_Now >= wait.Expiration) + { + wait.CTS.TrySetResult(true); + waits.Remove(wait); + } + } + } + } + + public void AdvanceMilliseconds(long milli) + { + Advance(TimeSpan.FromMilliseconds(milli)); + } + + public override string ToString() + { + return _Now.Millisecond.ToString(CultureInfo.InvariantCulture); + } + } +} diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 04f724d29..4853ea32c 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1549,6 +1549,81 @@ donation: } } + + [Fact] + [Trait("Fast", "Fast")] + public void CanScheduleBackgroundTasks() + { + BackgroundJobClient client = new BackgroundJobClient(); + MockDelay mockDelay = new MockDelay(); + client.Delay = mockDelay; + bool[] jobs = new bool[4]; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + Logs.Tester.LogInformation("Start Job[0] in 5 sec"); + client.Schedule(async () => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; }, TimeSpan.FromSeconds(5.0)); + Logs.Tester.LogInformation("Start Job[1] in 2 sec"); + client.Schedule(async () => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; }, TimeSpan.FromSeconds(2.0)); + Logs.Tester.LogInformation("Start Job[2] fails in 6 sec"); + client.Schedule(async () => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0)); + Logs.Tester.LogInformation("Start Job[3] starts in in 7 sec"); + client.Schedule(async () => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; }, TimeSpan.FromSeconds(7.0)); + + Assert.True(new[] { false, false, false, false }.SequenceEqual(jobs)); + CancellationTokenSource cts = new CancellationTokenSource(); + var processing = client.ProcessJobs(cts.Token); + + Assert.Equal(4, client.GetExecutingCount()); + + var waitJobsFinish = client.WaitAllRunning(default); + + mockDelay.Advance(TimeSpan.FromSeconds(2.0)); + Assert.True(new[] { false, true, false, false }.SequenceEqual(jobs)); + + mockDelay.Advance(TimeSpan.FromSeconds(3.0)); + Assert.True(new[] { true, true, false, false }.SequenceEqual(jobs)); + + mockDelay.Advance(TimeSpan.FromSeconds(1.0)); + Assert.True(new[] { true, true, true, false }.SequenceEqual(jobs)); + Assert.Equal(1, client.GetExecutingCount()); + + Assert.False(waitJobsFinish.Wait(100)); + Assert.False(waitJobsFinish.IsCompletedSuccessfully); + + mockDelay.Advance(TimeSpan.FromSeconds(1.0)); + Assert.True(new[] { true, true, true, true }.SequenceEqual(jobs)); + + Assert.True(waitJobsFinish.Wait(100)); + Assert.True(waitJobsFinish.IsCompletedSuccessfully); + Assert.True(!waitJobsFinish.IsFaulted); + Assert.Equal(0, client.GetExecutingCount()); + + bool jobExecuted = false; + Logs.Tester.LogInformation("This job will be cancelled"); + client.Schedule(async () => { jobExecuted = true; }, TimeSpan.FromSeconds(1.0)); + mockDelay.Advance(TimeSpan.FromSeconds(0.5)); + Assert.False(jobExecuted); + Thread.Sleep(100); + Assert.Equal(1, client.GetExecutingCount()); + + + waitJobsFinish = client.WaitAllRunning(default); + Assert.False(waitJobsFinish.Wait(100)); + cts.Cancel(); + Assert.True(waitJobsFinish.Wait(1000)); + Assert.True(waitJobsFinish.IsCompletedSuccessfully); + Assert.True(!waitJobsFinish.IsFaulted); + Assert.False(jobExecuted); + + mockDelay.Advance(TimeSpan.FromSeconds(1.0)); + Thread.Sleep(100); // Make sure it get cancelled + + Assert.False(jobExecuted); + Assert.Equal(0, client.GetExecutingCount()); + Assert.True(processing.IsCanceled); + Assert.True(client.WaitAllRunning(default).Wait(100)); +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + } + [Fact] [Trait("Fast", "Fast")] public void PosDataParser_ParsesCorrectly() @@ -1856,6 +1931,50 @@ donation: } } + [Fact] + [Trait("Integration", "Integration")] + public void CanCreateStrangeInvoice() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + var invoice1 = user.BitPay.CreateInvoice(new Invoice() + { + Price = 0.000000012m, + Currency = "BTC", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + var invoice2 = user.BitPay.CreateInvoice(new Invoice() + { + Price = 0.000000019m, + Currency = "BTC", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + Assert.Equal(0.00000001m, invoice1.Price); + Assert.Equal(0.00000002m, invoice2.Price); + + var invoice = user.BitPay.CreateInvoice(new Invoice() + { + Price = -0.1m, + Currency = "BTC", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + Assert.Equal(0.0m, invoice.Price); + } + } + [Fact] [Trait("Integration", "Integration")] public void InvoiceFlowThroughDifferentStatesCorrectly() @@ -1869,6 +1988,7 @@ donation: var invoice = user.BitPay.CreateInvoice(new Invoice() { Price = 5000.0m, + TaxIncluded = 1000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1898,6 +2018,8 @@ donation: }); invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal(1000.0m, invoice.TaxIncluded); + Assert.Equal(5000.0m, invoice.Price); Assert.Equal(Money.Coins(0), invoice.BtcPaid); Assert.Equal("new", invoice.Status); Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index d56c1a0c0..778127f62 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -69,7 +69,7 @@ services: nbxplorer: - image: nicolasdorier/nbxplorer:2.0.0.2 + image: nicolasdorier/nbxplorer:2.0.0.8 restart: unless-stopped ports: - "32838:32838" diff --git a/BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs b/BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs new file mode 100644 index 000000000..d7b472993 --- /dev/null +++ b/BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services.Rates; +using NBitcoin; +using NBXplorer; + +namespace BTCPayServer +{ + public partial class BTCPayNetworkProvider + { + public void InitBitcoinplus() + { + var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("XBC"); + Add(new BTCPayNetwork() + { + CryptoCode = nbxplorerNetwork.CryptoCode, + DisplayName = "Bitcoinplus", + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}", + NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, + NBXplorerNetwork = nbxplorerNetwork, + UriScheme = "bitcoinplus", + DefaultRateRules = new[] + { + "XBC_X = XBC_BTC * BTC_X", + "XBC_BTC = cryptopia(XBC_BTC)" + }, + CryptoImagePath = "imlegacy/bitcoinplus.png", + DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("65'") : new KeyPath("1'") + }); + } + } +} diff --git a/BTCPayServer/BTCPayNetworkProvider.Bitcore.cs b/BTCPayServer/BTCPayNetworkProvider.Bitcore.cs index 62090b39a..fad2bb54f 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Bitcore.cs +++ b/BTCPayServer/BTCPayNetworkProvider.Bitcore.cs @@ -24,7 +24,7 @@ namespace BTCPayServer DefaultRateRules = new[] { "BTX_X = BTX_BTC * BTC_X", - "BTX_BTC = cryptopia(BTX_BTC)" + "BTX_BTC = hitbtc(BTX_BTC)" }, CryptoImagePath = "imlegacy/bitcore.svg", LightningImagePath = "imlegacy/bitcore-lightning.svg", diff --git a/BTCPayServer/BTCPayNetworkProvider.cs b/BTCPayServer/BTCPayNetworkProvider.cs index b7ad6fa50..be0c4970d 100644 --- a/BTCPayServer/BTCPayNetworkProvider.cs +++ b/BTCPayServer/BTCPayNetworkProvider.cs @@ -52,10 +52,13 @@ namespace BTCPayServer InitBitcoinGold(); InitMonacoin(); InitDash(); - InitPolis(); InitFeathercoin(); InitGroestlcoin(); InitViacoin(); + + // Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586 + //InitPolis(); + //InitBitcoinplus(); //InitUfo(); } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 31a104bf6..739ffaff9 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.1 - 1.0.3.42 + 1.0.3.45 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -33,11 +33,9 @@ - + - - @@ -47,10 +45,10 @@ all runtime; build; native; contentfiles; analyzers - - + + - + diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index b40470f40..906546275 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -110,7 +110,7 @@ namespace BTCPayServer.Configuration if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error)) { throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine + - $"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine + + $"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine + $"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine + $"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine + $" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine + @@ -118,7 +118,7 @@ namespace BTCPayServer.Configuration } if (connectionString.IsLegacy) { - Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'"); + Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'"); } InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString); } @@ -177,10 +177,11 @@ namespace BTCPayServer.Configuration var services = conf.GetOrDefault("externalservices", null); if (services != null) { - foreach (var service in services.Split(new[] { ';', ',' }) - .Select(p => p.Split(':')) - .Where(p => p.Length == 2) - .Select(p => (Name: p[0], Link: p[1]))) + foreach (var service in services.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => (p, SeparatorIndex: p.IndexOf(':', StringComparison.OrdinalIgnoreCase))) + .Where(p => p.SeparatorIndex != -1) + .Select(p => (Name: p.p.Substring(0, p.SeparatorIndex), + Link: p.p.Substring(p.SeparatorIndex + 1)))) { ExternalServices.AddOrReplace(service.Name, service.Link); } @@ -235,7 +236,7 @@ namespace BTCPayServer.Configuration RootPath = "/" + RootPath; var old = conf.GetOrDefault("internallightningnode", null); if (old != null) - throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead"); + throw new ConfigException($"internallightningnode is deprecated and should not be used anymore, use btclightning instead"); LogFile = GetDebugLog(conf); if (!string.IsNullOrEmpty(LogFile)) diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 39e083e35..94cc11c42 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -28,7 +28,7 @@ namespace BTCPayServer.Controllers { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly IEmailSender _emailSender; + private readonly EmailSenderFactory _EmailSenderFactory; StoreRepository storeRepository; RoleManager _RoleManager; SettingsRepository _SettingsRepository; @@ -40,14 +40,14 @@ namespace BTCPayServer.Controllers RoleManager roleManager, StoreRepository storeRepository, SignInManager signInManager, - IEmailSender emailSender, + EmailSenderFactory emailSenderFactory, SettingsRepository settingsRepository, Configuration.BTCPayServerOptions options) { this.storeRepository = storeRepository; _userManager = userManager; _signInManager = signInManager; - _emailSender = emailSender; + _EmailSenderFactory = emailSenderFactory; _RoleManager = roleManager; _SettingsRepository = settingsRepository; _Options = options; @@ -286,7 +286,8 @@ namespace BTCPayServer.Controllers var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); RegisteredUserId = user.Id; - await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl); + + _EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl); if (!policies.RequiresConfirmedEmail) { if(logon) @@ -446,8 +447,9 @@ namespace BTCPayServer.Controllers // visit https://go.microsoft.com/fwlink/?LinkID=532713 var code = await _userManager.GeneratePasswordResetTokenAsync(user); var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); - await _emailSender.SendEmailAsync(model.Email, "Reset Password", - $"Please reset your password by clicking here: link"); + _EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password", + $"Please reset your password by clicking here: link"); + return RedirectToAction(nameof(ForgotPasswordConfirmation)); } diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 4d97d60fe..10067e021 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using BTCPayServer.Crowdfund; using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Hubs; @@ -19,6 +20,7 @@ using BTCPayServer.Services.Rates; using Ganss.XSS; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -183,7 +185,7 @@ namespace BTCPayServer.Controllers NotificationURL = settings.NotificationUrl, FullNotifications = true, ExtendedNotifications = true, - RedirectURL = request.RedirectUrl, + RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(), }, store, HttpContext.Request.GetAbsoluteRoot()); @@ -254,7 +256,7 @@ namespace BTCPayServer.Controllers BuyerEmail = email, OrderId = orderId, NotificationURL = notificationUrl, - RedirectURL = redirectUrl, + RedirectURL = redirectUrl ?? Request.GetDisplayUrl(), FullNotifications = true, }, store, HttpContext.Request.GetAbsoluteRoot()); return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id }); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 60f036ecd..e94cdabe3 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -447,8 +447,11 @@ namespace BTCPayServer.Controllers Count = count, StatusMessage = StatusMessage }; - - var list = await ListInvoicesProcess(searchTerm, skip, count); + InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm); + var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery); + invoiceQuery.Count = count; + invoiceQuery.Skip = skip; + var list = await _InvoiceRepository.GetInvoices(invoiceQuery); foreach (var invoice in list) { var state = invoice.GetInvoiceState(); @@ -465,17 +468,16 @@ namespace BTCPayServer.Controllers CanMarkComplete = state.CanMarkComplete() }); } + model.Total = await counting; return View(model); } - private async Task ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50) + private InvoiceQuery GetInvoiceQuery(string searchTerm = null) { var filterString = new SearchString(searchTerm); - var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery() + var invoiceQuery = new InvoiceQuery() { TextSearch = filterString.TextSearch, - Count = count, - Skip = skip, UserId = GetUserId(), Unusual = !filterString.Filters.ContainsKey("unusual") ? null : !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null @@ -485,9 +487,8 @@ namespace BTCPayServer.Controllers StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null, ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null, OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null - }); - - return list; + }; + return invoiceQuery; } [HttpGet] @@ -497,7 +498,10 @@ namespace BTCPayServer.Controllers { var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable); - var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue); + InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm); + invoiceQuery.Count = int.MaxValue; + invoiceQuery.Skip = 0; + var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery); var res = model.Process(invoices, format); var cd = new ContentDisposition diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index eda115aa2..6f06573c0 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -71,7 +71,6 @@ namespace BTCPayServer.Controllers { InvoiceTime = DateTimeOffset.UtcNow }; - var storeBlob = store.GetStoreBlob(); Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null; if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ? @@ -95,7 +94,20 @@ namespace BTCPayServer.Controllers throw new BitpayHttpException(400, "Invalid email"); entity.RefundMail = entity.BuyerInformation.BuyerEmail; } + + var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false); + if (currencyInfo != null) + { + invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits); + invoice.TaxIncluded = Math.Round(invoice.TaxIncluded, currencyInfo.CurrencyDecimalDigits); + } + invoice.Price = Math.Max(0.0m, invoice.Price); + invoice.TaxIncluded = Math.Max(0.0m, invoice.TaxIncluded); + invoice.TaxIncluded = Math.Min(invoice.TaxIncluded, invoice.Price); + entity.ProductInformation = Map(invoice); + + entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite; if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute)) entity.RedirectURL = null; @@ -147,7 +159,7 @@ namespace BTCPayServer.Controllers if (supported.Count == 0) { StringBuilder errors = new StringBuilder(); - errors.AppendLine("No payment method available for this store"); + errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet)"); foreach (var error in logs.ToList()) { errors.AppendLine(error.ToString()); diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index bf36f61e1..65341743f 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly IEmailSender _emailSender; + private readonly EmailSenderFactory _EmailSenderFactory; private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; TokenRepository _TokenRepository; @@ -44,7 +44,7 @@ namespace BTCPayServer.Controllers public ManageController( UserManager userManager, SignInManager signInManager, - IEmailSender emailSender, + EmailSenderFactory emailSenderFactory, ILogger logger, UrlEncoder urlEncoder, TokenRepository tokenRepository, @@ -54,7 +54,7 @@ namespace BTCPayServer.Controllers { _userManager = userManager; _signInManager = signInManager; - _emailSender = emailSender; + _EmailSenderFactory = emailSenderFactory; _logger = logger; _urlEncoder = urlEncoder; _TokenRepository = tokenRepository; @@ -156,8 +156,7 @@ namespace BTCPayServer.Controllers var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); var email = user.Email; - await _emailSender.SendEmailConfirmationAsync(email, callbackUrl); - + _EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl); StatusMessage = "Verification email sent. Please check your email."; return RedirectToAction(nameof(Index)); } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 3c5e404c0..7de937d5c 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -465,7 +465,7 @@ namespace BTCPayServer.Controllers result.ExternalServices.Add(new ServicesViewModel.ExternalService() { Name = externalService.Key, - Link = this.Request.GetRelativePath(externalService.Value) + Link = this.Request.GetRelativePathOrAbsolute(externalService.Value) }); } if(_Options.SSHSettings != null) diff --git a/BTCPayServer/Controllers/StoresController.Email.cs b/BTCPayServer/Controllers/StoresController.Email.cs new file mode 100644 index 000000000..0ced7427b --- /dev/null +++ b/BTCPayServer/Controllers/StoresController.Email.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Models.ServerViewModels; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments.Changelly; +using BTCPayServer.Services.Mails; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class StoresController + { + + [Route("{storeId}/emails")] + public IActionResult Emails() + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings(); + return View(new EmailsViewModel() { Settings = data }); + } + + [Route("{storeId}/emails")] + [HttpPost] + public async Task Emails(string storeId, EmailsViewModel model, string command) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + if (command == "Test") + { + try + { + if (!model.Settings.IsComplete()) + { + model.StatusMessage = "Error: Required fields missing"; + return View(model); + } + var client = model.Settings.CreateSmtpClient(); + await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test"); + model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it"; + } + catch (Exception ex) + { + model.StatusMessage = "Error: " + ex.Message; + } + return View(model); + } + else // if(command == "Save") + { + + var storeBlob = store.GetStoreBlob(); + storeBlob.EmailSettings = model.Settings; + store.SetStoreBlob(storeBlob); + await _Repo.UpdateStore(store); + StatusMessage = "Email settings modified"; + return RedirectToAction(nameof(UpdateStore), new { + storeId}); + + } + } + } +} diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index f0251dc72..9604d439f 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -96,6 +96,11 @@ namespace BTCPayServer.Controllers { get; set; } + [TempData] + public bool StoreNotConfigured + { + get; set; + } [HttpGet] [Route("{storeId}/users")] @@ -167,7 +172,7 @@ namespace BTCPayServer.Controllers return View("Confirm", new ConfirmModel() { Title = $"Remove store user", - Description = $"Are you sure to remove access to remove access to {user.Email}?", + Description = $"Are you sure you want to remove store access for {user.Email}?", Action = "Delete" }); } @@ -567,6 +572,7 @@ namespace BTCPayServer.Controllers var model = new TokensViewModel(); var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id); model.StatusMessage = StatusMessage; + model.StoreNotConfigured = StoreNotConfigured; model.Tokens = tokens.Select(t => new TokenViewModel() { Facade = t.Facade, @@ -794,6 +800,10 @@ namespace BTCPayServer.Controllers var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id); if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) { + var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); + StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider) + .Where(p => !excludeFilter.Match(p.PaymentId)) + .Count() == 0; StatusMessage = "Pairing is successful"; if (pairingResult == PairingResult.Partial) StatusMessage = "Server initiated pairing code: " + pairingCode; diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index bda65045b..56490da92 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers [Route("{walletId}/send")] public async Task WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, string defaultDestination = null, string defaultAmount = null) + WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false) { if (walletId?.StoreId == null) return NotFound(); @@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers } catch (Exception ex) { model.RateError = ex.Message; } } + model.AdvancedMode = advancedMode; return View(model); } @@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers [Route("{walletId}/send")] public async Task WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, WalletSendModel vm) + WalletId walletId, WalletSendModel vm, string command = null) { if (walletId?.StoreId == null) return NotFound(); @@ -212,6 +213,14 @@ namespace BTCPayServer.Controllers var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); if (network == null) return NotFound(); + + if (command == "noob" || command == "expert") + { + ModelState.Clear(); + vm.AdvancedMode = command == "expert"; + return View(vm); + } + var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork); if (destination == null) ModelState.AddModelError(nameof(vm.Destination), "Invalid address"); @@ -231,7 +240,8 @@ namespace BTCPayServer.Controllers Destination = vm.Destination, Amount = vm.Amount.Value, SubstractFees = vm.SubstractFees, - FeeSatoshiPerByte = vm.FeeSatoshiPerByte + FeeSatoshiPerByte = vm.FeeSatoshiPerByte, + NoChange = vm.NoChange }); } @@ -403,6 +413,7 @@ namespace BTCPayServer.Controllers // getxpub int account = 0, // sendtoaddress + bool noChange = false, string destination = null, string amount = null, string feeRate = null, string substractFees = null ) { @@ -436,7 +447,7 @@ namespace BTCPayServer.Controllers { try { - destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork); + destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork); } catch { } if (destinationAddress == null) @@ -487,24 +498,16 @@ namespace BTCPayServer.Controllers var strategy = GetDirectDerivationStrategy(derivationScheme); var wallet = _walletProvider.GetWallet(network); var change = wallet.GetChangeAddressAsync(derivationScheme); - - var unspentCoins = await wallet.GetUnspentCoins(derivationScheme); - var changeAddress = await change; - var send = new[] { ( - destination: destinationAddress as IDestination, - amount: amountBTC, - substractFees: subsctractFeesValue) }; - - foreach (var element in send) + var keypaths = new Dictionary(); + List availableCoins = new List(); + foreach (var c in await wallet.GetUnspentCoins(derivationScheme)) { - 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"); + keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); + availableCoins.Add(c.Coin); } + var changeAddress = await change; + var storeBlob = storeData.GetStoreBlob(); var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike); var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId); @@ -520,10 +523,25 @@ namespace BTCPayServer.Controllers storeData.SetStoreBlob(storeBlob); await Repository.UpdateStore(storeData); } +retry: + 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"); + } TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder(); builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee; - builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray()); + builder.AddCoins(availableCoins); foreach (var element in send) { @@ -531,6 +549,7 @@ namespace BTCPayServer.Controllers if (element.substractFees) builder.SubtractFees(); } + builder.SetChange(changeAddress.Item1); if (network.MinFee == null) @@ -547,13 +566,15 @@ namespace BTCPayServer.Controllers } var unsigned = builder.BuildTransaction(false); - var keypaths = new Dictionary(); - foreach (var c in unspentCoins) + var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey); + if (noChange && hasChange) { - keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); + availableCoins = builder.FindSpentCoins(unsigned).Cast().ToList(); + amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum(); + subsctractFeesValue = true; + goto retry; } - var hasChange = unsigned.Outputs.Count == 2; var usedCoins = builder.FindSpentCoins(unsigned); Dictionary parentTransactions = new Dictionary(); diff --git a/BTCPayServer/Crowdfund/CrowdfundHub.cs b/BTCPayServer/Crowdfund/CrowdfundHub.cs index a2e00ebec..a030ed361 100644 --- a/BTCPayServer/Crowdfund/CrowdfundHub.cs +++ b/BTCPayServer/Crowdfund/CrowdfundHub.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using BTCPayServer.Controllers; using BTCPayServer.Models.AppViewModels; @@ -35,20 +36,30 @@ namespace BTCPayServer.Hubs { model.RedirectToCheckout = false; _AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext(); - var result = await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model); - switch (result) + try { - case OkObjectResult okObjectResult: - await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()}); - break; - case ObjectResult objectResult: - await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value}); - break; - default: - await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty()); - break; + + var result = + await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model); + switch (result) + { + case OkObjectResult okObjectResult: + await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()}); + break; + case ObjectResult objectResult: + await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value}); + break; + default: + await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty()); + break; + } } - + catch (Exception) + { + await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty()); + + } + } } diff --git a/BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs b/BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs index 7e1f69105..9e7741488 100644 --- a/BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs +++ b/BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs @@ -2,11 +2,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Events; +using BTCPayServer.Hubs; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Payments; using BTCPayServer.Rating; @@ -16,14 +16,11 @@ using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; using NBitcoin; -using YamlDotNet.Core; -namespace BTCPayServer.Hubs +namespace BTCPayServer.Crowdfund { - public class - CrowdfundHubStreamer + public class CrowdfundHubStreamer: IDisposable { public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app_"; private readonly EventAggregator _EventAggregator; @@ -37,7 +34,9 @@ namespace BTCPayServer.Hubs private readonly ConcurrentDictionary _QuickAppInvoiceLookup = new ConcurrentDictionary(); - + + private List _Subscriptions; + public CrowdfundHubStreamer(EventAggregator eventAggregator, IHubContext hubContext, IMemoryCache memoryCache, @@ -116,13 +115,15 @@ namespace BTCPayServer.Hubs private void SubscribeToEvents() { - - _EventAggregator.Subscribe(OnInvoiceEvent); - _EventAggregator.Subscribe(updated => + _Subscriptions = new List() { - UpdateLookup(updated.AppId, updated.StoreId, updated.Settings); - InvalidateCacheForApp(updated.AppId); - }); + _EventAggregator.Subscribe(OnInvoiceEvent), + _EventAggregator.Subscribe(updated => + { + UpdateLookup(updated.AppId, updated.StoreId, updated.Settings); + InvalidateCacheForApp(updated.AppId); + }) + }; } private string GetCacheKey(string appId) @@ -152,7 +153,7 @@ namespace BTCPayServer.Hubs Enum.GetName(typeof(PaymentTypes), invoiceEvent.Payment.GetPaymentMethodId().PaymentType) } ); - + _Logger.LogInformation($"App {quickLookup.appId}: Received Payment"); InvalidateCacheForApp(quickLookup.appId); break; case InvoiceEvent.Created: @@ -361,5 +362,10 @@ namespace BTCPayServer.Hubs StartDate = startDate }); } + + public void Dispose() + { + _Subscriptions.ForEach(subscription => subscription.Dispose()); + } } } diff --git a/BTCPayServer/Data/ApplicationDbContextFactory.cs b/BTCPayServer/Data/ApplicationDbContextFactory.cs index 4a21e99f2..05611e87e 100644 --- a/BTCPayServer/Data/ApplicationDbContextFactory.cs +++ b/BTCPayServer/Data/ApplicationDbContextFactory.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Hangfire; -using Hangfire.MemoryStorage; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; using JetBrains.Annotations; @@ -98,15 +96,5 @@ namespace BTCPayServer.Data else if (_Type == DatabaseType.MySQL) builder.UseMySql(_ConnectionString); } - - public void ConfigureHangfireBuilder(IGlobalConfiguration builder) - { - 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 953f0ecf6..18f8c2fd2 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.CoinSwitch; using BTCPayServer.Security; using BTCPayServer.Rating; +using BTCPayServer.Services.Mails; namespace BTCPayServer.Data { @@ -403,6 +404,8 @@ namespace BTCPayServer.Data [Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")] public Dictionary WalletKeyPathRoots { get; set; } = new Dictionary(); + public EmailSettings EmailSettings { get; set; } + public IPaymentFilter GetExcludedPaymentMethods() { #pragma warning disable CS0618 // Type or member is obsolete diff --git a/BTCPayServer/Extensions/EmailSenderExtensions.cs b/BTCPayServer/Extensions/EmailSenderExtensions.cs index ac07f0505..b1010b57b 100644 --- a/BTCPayServer/Extensions/EmailSenderExtensions.cs +++ b/BTCPayServer/Extensions/EmailSenderExtensions.cs @@ -10,9 +10,9 @@ namespace BTCPayServer.Services { public static class EmailSenderExtensions { - public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) + public static void SendEmailConfirmation(this IEmailSender emailSender, string email, string link) { - return emailSender.SendEmailAsync(email, "Confirm your email", + emailSender.SendEmail(email, "Confirm your email", $"Please confirm your account by clicking this link: link"); } } diff --git a/BTCPayServer/HostedServices/BackgroundJobSchedulerHostedService.cs b/BTCPayServer/HostedServices/BackgroundJobSchedulerHostedService.cs new file mode 100644 index 000000000..1689e74af --- /dev/null +++ b/BTCPayServer/HostedServices/BackgroundJobSchedulerHostedService.cs @@ -0,0 +1,133 @@ +using System; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using BTCPayServer.Logging; +using BTCPayServer.Services; +using Microsoft.Extensions.Hosting; +using NicolasDorier.RateLimits; + +namespace BTCPayServer.HostedServices +{ + public class BackgroundJobSchedulerHostedService : IHostedService + { + public BackgroundJobSchedulerHostedService(IBackgroundJobClient backgroundJobClient) + { + BackgroundJobClient = (BackgroundJobClient)backgroundJobClient; + } + + public BackgroundJobClient BackgroundJobClient { get; } + + Task _Loop; + + public Task StartAsync(CancellationToken cancellationToken) + { + _Stop = new CancellationTokenSource(); + _Loop = BackgroundJobClient.ProcessJobs(_Stop.Token); + return Task.CompletedTask; + } + + CancellationTokenSource _Stop; + + public async Task StopAsync(CancellationToken cancellationToken) + { + _Stop.Cancel(); + try + { + await _Loop; + } + catch (OperationCanceledException) + { + + } + await BackgroundJobClient.WaitAllRunning(cancellationToken); + } + } + + public class BackgroundJobClient : IBackgroundJobClient + { + class BackgroundJob + { + public Func Action; + public TimeSpan Delay; + public IDelay DelayImplementation; + public BackgroundJob(Func action, TimeSpan delay, IDelay delayImplementation) + { + this.Action = action; + this.Delay = delay; + this.DelayImplementation = delayImplementation; + } + + public async Task Run(CancellationToken cancellationToken) + { + await DelayImplementation.Wait(Delay, cancellationToken); + await Action(); + } + } + + public IDelay Delay { get; set; } = TaskDelay.Instance; + public int GetExecutingCount() + { + lock (_Processing) + { + return _Processing.Count(); + } + } + + private Channel _Jobs = Channel.CreateUnbounded(); + HashSet _Processing = new HashSet(); + public void Schedule(Func action, TimeSpan delay) + { + _Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay)); + } + + public async Task WaitAllRunning(CancellationToken cancellationToken) + { + Task[] processing = null; + lock (_Processing) + { + processing = _Processing.ToArray(); + } + + try + { + await Task.WhenAll(processing).WithCancellation(cancellationToken); + } + catch (Exception) when (cancellationToken.IsCancellationRequested) + { + throw; + } + } + + public async Task ProcessJobs(CancellationToken cancellationToken) + { + while (await _Jobs.Reader.WaitToReadAsync(cancellationToken)) + { + if (_Jobs.Reader.TryRead(out var job)) + { + var processing = job.Run(cancellationToken); + lock (_Processing) + { + _Processing.Add(processing); + } +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + processing.ContinueWith(t => + { + if (t.IsFaulted) + { + Logs.PayServer.LogWarning(t.Exception, "Unhandled exception while job running"); + } + lock (_Processing) + { + _Processing.Remove(processing); + } + }, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + } + } + } + } +} diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index b63d5d5d8..daad3e11f 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -1,10 +1,7 @@ -using Hangfire; -using Hangfire.Common; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Hangfire.Annotations; using System.Reflection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -21,6 +18,7 @@ using NBXplorer; using BTCPayServer.Services.Invoices; using BTCPayServer.Payments; using BTCPayServer.Services.Mails; +using BTCPayServer.Services; namespace BTCPayServer.HostedServices { @@ -44,16 +42,11 @@ namespace BTCPayServer.HostedServices public string Message { get; set; } } - public ILogger Logger - { - get; set; - } - IBackgroundJobClient _JobClient; EventAggregator _EventAggregator; InvoiceRepository _InvoiceRepository; BTCPayNetworkProvider _NetworkProvider; - IEmailSender _EmailSender; + private readonly EmailSenderFactory _EmailSenderFactory; public InvoiceNotificationManager( IBackgroundJobClient jobClient, @@ -61,17 +54,16 @@ namespace BTCPayServer.HostedServices InvoiceRepository invoiceRepository, BTCPayNetworkProvider networkProvider, ILogger logger, - IEmailSender emailSender) + EmailSenderFactory emailSenderFactory) { - Logger = logger as ILogger ?? NullLogger.Instance; _JobClient = jobClient; _EventAggregator = eventAggregator; _InvoiceRepository = invoiceRepository; _NetworkProvider = networkProvider; - _EmailSender = emailSender; + _EmailSenderFactory = emailSenderFactory; } - async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null) + void Notify(InvoiceEntity invoice, int? eventCode = null, string name = null) { CancellationTokenSource cts = new CancellationTokenSource(10000); @@ -85,58 +77,31 @@ namespace BTCPayServer.HostedServices invoice.StoreId }; // TODO: Consider adding info on ItemDesc and payment info (amount) - + var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn); - await _EmailSender.SendEmailAsync( - invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody); - } - try - { - if (string.IsNullOrEmpty(invoice.NotificationURL)) - return; - _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id, eventCode, name)); - var response = await SendNotification(invoice, eventCode, name, cts.Token); - response.EnsureSuccessStatusCode(); + _EmailSenderFactory.GetEmailSender(invoice.StoreId).SendEmail( + invoice.NotificationEmail, + $"BtcPayServer Invoice Notification - ${invoice.StoreId}", + emailBody); + + } + if (string.IsNullOrEmpty(invoice.NotificationURL)) return; - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id, eventCode, name) - { - Error = "Timeout" - }); - } - catch (Exception ex) // It fails, it is OK because we try with hangfire after - { - _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id, eventCode, name) - { - Error = ex.Message - }); - } var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name }); if (!string.IsNullOrEmpty(invoice.NotificationURL)) _JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero); } - ConcurrentDictionary _Executing = new ConcurrentDictionary(); public async Task NotifyHttp(string invoiceData) { var job = NBitcoin.JsonConverters.Serializer.ToObject(invoiceData); - var jobId = GetHttpJobId(job.Invoice); - - if (!_Executing.TryAdd(jobId, jobId)) - return; //For some reason, Hangfire fire the job several time - - Logger.LogInformation("Running " + jobId); bool reschedule = false; CancellationTokenSource cts = new CancellationTokenSource(10000); try { HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token); reschedule = !response.IsSuccessStatusCode; - Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode); - _EventAggregator.Publish(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message) { Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null @@ -149,9 +114,8 @@ namespace BTCPayServer.HostedServices Error = "Timeout" }); reschedule = true; - Logger.LogInformation("Job " + jobId + " timed out"); } - catch (Exception ex) // It fails, it is OK because we try with hangfire after + catch (Exception ex) { _EventAggregator.Publish(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message) { @@ -166,21 +130,18 @@ namespace BTCPayServer.HostedServices ex = ex.InnerException; } string message = String.Join(',', messages.ToArray()); - Logger.LogInformation("Job " + jobId + " threw exception " + message); _EventAggregator.Publish(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message) { Error = $"Unexpected error: {message}" }); } - finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); } + finally { cts?.Dispose(); } job.TryCount++; if (job.TryCount < MaxTry && reschedule) { - Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount)); - invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job); _JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0)); } @@ -320,11 +281,6 @@ namespace BTCPayServer.HostedServices int MaxTry = 6; - private static string GetHttpJobId(InvoiceEntity invoice) - { - return $"{invoice.Id}-{invoice.Status}-HTTP"; - } - CompositeDisposable leases = new CompositeDisposable(); public Task StartAsync(CancellationToken cancellationToken) { @@ -350,19 +306,18 @@ namespace BTCPayServer.HostedServices e.Name == InvoiceEvent.Completed || e.Name == InvoiceEvent.ExpiredPaidPartial ) - tasks.Add(Notify(invoice)); + Notify(invoice); } if (e.Name == "invoice_confirmed") { - tasks.Add(Notify(invoice)); + Notify(invoice); } if (invoice.ExtendedNotifications) { - tasks.Add(Notify(invoice, e.EventCode, e.Name)); + Notify(invoice, e.EventCode, e.Name); } - await Task.WhenAll(tasks.ToArray()); })); diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 7c9687bfc..52e62d0b6 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -11,7 +11,6 @@ using BTCPayServer.Logging; using System.Threading; using Microsoft.Extensions.Hosting; using System.Collections.Concurrent; -using Hangfire; using BTCPayServer.Services.Wallets; using BTCPayServer.Controllers; using BTCPayServer.Events; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index e2f05e477..403b18ef2 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -38,6 +38,7 @@ using BTCPayServer.Logging; using BTCPayServer.HostedServices; using Meziantou.AspNetCore.BundleTagHelpers; using System.Security.Claims; +using BTCPayServer.Crowdfund; using BTCPayServer.Hubs; using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.Lightning; @@ -116,7 +117,7 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(o => new NBXplorerFeeProviderFactory(o.GetRequiredService()) { - Fallback = new FeeRate(100, 1), + Fallback = new FeeRate(100L, 1), BlockTarget = 20 }); @@ -144,6 +145,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddTransient, BTCPayClaimsFilter>(); services.TryAddSingleton(); @@ -162,7 +165,7 @@ namespace BTCPayServer.Hosting services.AddTransient(); services.AddTransient(); // Add application services. - services.AddTransient(); + services.AddSingleton(); // bundling services.AddAuthorization(o => Policies.AddBTCPayPolicies(o)); diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index c656040a4..bd7301f68 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -19,7 +19,6 @@ using BTCPayServer.Models; using Microsoft.AspNetCore.Identity; using BTCPayServer.Data; using Microsoft.Extensions.Logging; -using Hangfire; using BTCPayServer.Logging; using Microsoft.AspNetCore.Authorization; using System.Threading.Tasks; @@ -27,11 +26,8 @@ using BTCPayServer.Controllers; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Mails; using Microsoft.Extensions.Configuration; -using Hangfire.AspNetCore; using BTCPayServer.Configuration; using System.IO; -using Hangfire.Dashboard; -using Hangfire.Annotations; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Threading; using Microsoft.Extensions.Options; @@ -46,18 +42,6 @@ namespace BTCPayServer.Hosting { public class Startup { - class NeedRole : IDashboardAuthorizationFilter - { - string _Role; - public NeedRole(string role) - { - _Role = role; - } - public bool Authorize([NotNull] DashboardContext context) - { - return context.GetHttpContext().User.IsInRole(_Role); - } - } public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory) { Configuration = conf; @@ -108,13 +92,6 @@ namespace BTCPayServer.Hosting options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; }); - - services.AddHangfire((o) => - { - var scope = AspNetCoreJobActivator.Current.BeginScope(null); - var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory)); - options.ConfigureHangfireBuilder(o); - }); services.AddCors(o => { o.AddPolicy("BitpayAPI", b => @@ -193,12 +170,6 @@ namespace BTCPayServer.Hosting app.UsePayServer(); app.UseStaticFiles(); app.UseAuthentication(); - app.UseHangfireServer(); - app.UseHangfireDashboard("/hangfire", new DashboardOptions() - { - AppPath = options.GetRootUri(), - Authorization = new[] { new NeedRole(Roles.ServerAdmin) } - }); app.UseSignalR(route => { route.MapHub("/apps/crowdfund/hub"); diff --git a/BTCPayServer/IDelay.cs b/BTCPayServer/IDelay.cs new file mode 100644 index 000000000..e5aca5425 --- /dev/null +++ b/BTCPayServer/IDelay.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BTCPayServer +{ + public interface IDelay + { + Task Wait(TimeSpan delay, CancellationToken cancellationToken); + } + + public class TaskDelay : IDelay + { + TaskDelay() + { + + } + private static readonly TaskDelay _Instance = new TaskDelay(); + public static TaskDelay Instance + { + get + { + return _Instance; + } + } + public Task Wait(TimeSpan delay, CancellationToken cancellationToken) + { + return Task.Delay(delay, cancellationToken); + } + } +} diff --git a/BTCPayServer/Models/InvoiceResponse.cs b/BTCPayServer/Models/InvoiceResponse.cs index 486a3b906..d0c5c5dfe 100644 --- a/BTCPayServer/Models/InvoiceResponse.cs +++ b/BTCPayServer/Models/InvoiceResponse.cs @@ -90,6 +90,12 @@ namespace BTCPayServer.Models get; set; } + [JsonProperty("taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] + public decimal TaxIncluded + { + get; set; + } + //"currency":"USD" [JsonProperty("currency")] public string Currency diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index 7a4e3fb9c..5f359df22 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -15,6 +15,10 @@ namespace BTCPayServer.Models.InvoicingModels { get; set; } + public int Total + { + get; set; + } public string SearchTerm { get; set; diff --git a/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs b/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs index 502ffc3bf..464cf8d0d 100644 --- a/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs @@ -72,5 +72,6 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "API Key")] public string ApiKey { get; set; } public string EncodedApiKey { get; set; } + public bool StoreNotConfigured { get; set; } } } diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs index a3aec83bc..948903714 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs @@ -11,5 +11,6 @@ namespace BTCPayServer.Models.WalletViewModels public bool SubstractFees { get; set; } public decimal Amount { get; set; } public string Destination { get; set; } + public bool NoChange { get; set; } } } diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs index 4b006b619..7faa04a13 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs @@ -28,6 +28,10 @@ namespace BTCPayServer.Models.WalletViewModels [Display(Name = "Fee rate (satoshi per byte)")] [Required] public int FeeSatoshiPerByte { get; set; } + + [Display(Name = "Make sure no change UTXO is created")] + public bool NoChange { get; set; } + public bool AdvancedMode { get; set; } public decimal? Rate { get; set; } public int Divisibility { get; set; } public string Fiat { get; set; } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index e11be11cc..e8a59c3e5 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning } catch (OperationCanceledException) when (cts.IsCancellationRequested) { - throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner"); + throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely maner"); } catch (Exception ex) { @@ -78,7 +78,7 @@ namespace BTCPayServer.Payments.Lightning } catch (OperationCanceledException) when (cts.IsCancellationRequested) { - throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely manner"); + throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely manner"); } catch (Exception ex) { @@ -115,7 +115,7 @@ namespace BTCPayServer.Payments.Lightning } if (address == null) - throw new PaymentMethodUnavailableException($"DNS did not resolved {nodeInfo.Host}"); + throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}"); using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)) { diff --git a/BTCPayServer/Security/BitpayAuthentication.cs b/BTCPayServer/Security/BitpayAuthentication.cs index e71c9f2db..9ea5c6297 100644 --- a/BTCPayServer/Security/BitpayAuthentication.cs +++ b/BTCPayServer/Security/BitpayAuthentication.cs @@ -198,44 +198,6 @@ namespace BTCPayServer.Security } yield return token; } - - private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth) - { - 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" || path == "/invoices/") && - httpContext.Request.Method == "POST" && - isJson) - return true; - - if ( - bitpayAuth && - (path == "/invoices" || path == "/invoices/") && - httpContext.Request.Method == "GET") - return true; - - if ( - path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) && - httpContext.Request.Method == "GET" && - (isJson || httpContext.Request.Query.ContainsKey("token"))) - return true; - - if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) && - httpContext.Request.Method == "GET") - return true; - - if ( - path.Equals("/tokens", StringComparison.Ordinal) && - (httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST")) - return true; - - return false; - } } internal static void AddAuthentication(IServiceCollection services, Action bitpayAuth = null) { diff --git a/BTCPayServer/Services/IBackgroundJobClient.cs b/BTCPayServer/Services/IBackgroundJobClient.cs new file mode 100644 index 000000000..36debc6ca --- /dev/null +++ b/BTCPayServer/Services/IBackgroundJobClient.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Services +{ + public interface IBackgroundJobClient + { + void Schedule(Func act, TimeSpan zero); + } +} diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 0bc8fa914..cb54640f1 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -91,6 +91,12 @@ namespace BTCPayServer.Services.Invoices get; set; } + [JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] + public decimal TaxIncluded + { + get; set; + } + [JsonProperty(PropertyName = "currency")] public string Currency { diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 314a90bfa..207fcdaa5 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -420,91 +420,108 @@ retry: return entity; } + private IQueryable GetInvoiceQuery(ApplicationDbContext context, InvoiceQuery queryObject) + { + IQueryable query = context.Invoices; + + if (!string.IsNullOrEmpty(queryObject.InvoiceId)) + { + query = query.Where(i => i.Id == queryObject.InvoiceId); + } + + if (queryObject.StoreId != null && queryObject.StoreId.Length > 0) + { + var stores = queryObject.StoreId.ToHashSet(); + query = query.Where(i => stores.Contains(i.StoreDataId)); + } + + if (queryObject.UserId != null) + { + query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId)); + } + + if (!string.IsNullOrEmpty(queryObject.TextSearch)) + { + var ids = new HashSet(SearchInvoice(queryObject.TextSearch)); + if (ids.Count == 0) + { + // Hacky way to return an empty query object. The nice way is much too elaborate: + // https://stackoverflow.com/questions/33305495/how-to-return-empty-iqueryable-in-an-async-repository-method + return query.Where(x => false); + } + query = query.Where(i => ids.Contains(i.Id)); + } + + if (queryObject.StartDate != null) + query = query.Where(i => queryObject.StartDate.Value <= i.Created); + + if (queryObject.EndDate != null) + query = query.Where(i => i.Created <= queryObject.EndDate.Value); + + if (queryObject.OrderId != null && queryObject.OrderId.Length > 0) + { + var statusSet = queryObject.OrderId.ToHashSet(); + query = query.Where(i => statusSet.Contains(i.OrderId)); + } + if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0) + { + var statusSet = queryObject.ItemCode.ToHashSet(); + query = query.Where(i => statusSet.Contains(i.ItemCode)); + } + + if (queryObject.Status != null && queryObject.Status.Length > 0) + { + var statusSet = queryObject.Status.ToHashSet(); + 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) + query = query.Skip(queryObject.Skip.Value); + + if (queryObject.Count != null) + query = query.Take(queryObject.Count.Value); + + return query; + } + + public async Task GetInvoicesTotal(InvoiceQuery queryObject) + { + using (var context = _ContextFactory.CreateContext()) + { + var query = GetInvoiceQuery(context, queryObject); + return await query.CountAsync(); + } + } public async Task GetInvoices(InvoiceQuery queryObject) { using (var context = _ContextFactory.CreateContext()) { - IQueryable query = context - .Invoices - .Include(o => o.Payments) + var query = GetInvoiceQuery(context, queryObject); + query = query.Include(o => o.Payments) .Include(o => o.RefundAddresses); if (queryObject.IncludeAddresses) query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices); if (queryObject.IncludeEvents) query = query.Include(o => o.Events); - if (!string.IsNullOrEmpty(queryObject.InvoiceId)) - { - query = query.Where(i => i.Id == queryObject.InvoiceId); - } - - if (queryObject.StoreId != null && queryObject.StoreId.Length > 0) - { - var stores = queryObject.StoreId.ToHashSet(); - query = query.Where(i => stores.Contains(i.StoreDataId)); - } - - if (queryObject.UserId != null) - { - query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId)); - } - - if (!string.IsNullOrEmpty(queryObject.TextSearch)) - { - var ids = new HashSet(SearchInvoice(queryObject.TextSearch)); - if (ids.Count == 0) - return Array.Empty(); - query = query.Where(i => ids.Contains(i.Id)); - } - - if (queryObject.StartDate != null) - query = query.Where(i => queryObject.StartDate.Value <= i.Created); - - if (queryObject.EndDate != null) - query = query.Where(i => i.Created <= queryObject.EndDate.Value); - - if (queryObject.OrderId != null && queryObject.OrderId.Length > 0) - { - var statusSet = queryObject.OrderId.ToHashSet(); - query = query.Where(i => statusSet.Contains(i.OrderId)); - } - if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0) - { - var statusSet = queryObject.ItemCode.ToHashSet(); - query = query.Where(i => statusSet.Contains(i.ItemCode)); - } - - if (queryObject.Status != null && queryObject.Status.Length > 0) - { - var statusSet = queryObject.Status.ToHashSet(); - 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) - query = query.Skip(queryObject.Skip.Value); - - if (queryObject.Count != null) - query = query.Take(queryObject.Count.Value); var data = await query.ToArrayAsync().ConfigureAwait(false); - return data.Select(ToEntity).ToArray(); } - } private string NormalizeExceptionStatus(string status) diff --git a/BTCPayServer/Services/Mails/EmailSender.cs b/BTCPayServer/Services/Mails/EmailSender.cs index 2e51db317..801f525c8 100644 --- a/BTCPayServer/Services/Mails/EmailSender.cs +++ b/BTCPayServer/Services/Mails/EmailSender.cs @@ -1,48 +1,40 @@ using BTCPayServer.Logging; using Microsoft.Extensions.Logging; -using Hangfire; using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Mail; using System.Threading.Tasks; namespace BTCPayServer.Services.Mails { - // This class is used by the application to send email for account confirmation and password reset. - // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 - public class EmailSender : IEmailSender + public abstract class EmailSender : IEmailSender { IBackgroundJobClient _JobClient; - SettingsRepository _Repository; - public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository) + + public EmailSender(IBackgroundJobClient jobClient) { - if (jobClient == null) - throw new ArgumentNullException(nameof(jobClient)); - _JobClient = jobClient; - _Repository = repository; - } - public async Task SendEmailAsync(string email, string subject, string message) - { - var settings = await _Repository.GetSettingAsync() ?? new EmailSettings(); - if (!settings.IsComplete()) - { - Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); - return; - } - _JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero); - return; + _JobClient = jobClient ?? throw new ArgumentNullException(nameof(jobClient)); } - public async Task SendMailCore(string email, string subject, string message) + public void SendEmail(string email, string subject, string message) { - 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); - mail.IsBodyHtml = true; - await smtp.SendMailAsync(mail); + _JobClient.Schedule(async () => + { + var emailSettings = await GetEmailSettings(); + if (emailSettings?.IsComplete() != true) + { + Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); + return; + } + var smtp = emailSettings.CreateSmtpClient(); + var mail = new MailMessage(emailSettings.From, email, subject, message) + { + IsBodyHtml = true + }; + await smtp.SendMailAsync(mail); + + }, TimeSpan.Zero); } + + public abstract Task GetEmailSettings(); } } diff --git a/BTCPayServer/Services/Mails/EmailSenderFactory.cs b/BTCPayServer/Services/Mails/EmailSenderFactory.cs new file mode 100644 index 000000000..12c288350 --- /dev/null +++ b/BTCPayServer/Services/Mails/EmailSenderFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using BTCPayServer.Services.Stores; + +namespace BTCPayServer.Services.Mails +{ + public class EmailSenderFactory + { + private readonly IBackgroundJobClient _JobClient; + private readonly SettingsRepository _Repository; + private readonly StoreRepository _StoreRepository; + + public EmailSenderFactory(IBackgroundJobClient jobClient, + SettingsRepository repository, + StoreRepository storeRepository) + { + _JobClient = jobClient; + _Repository = repository; + _StoreRepository = storeRepository; + } + + public IEmailSender GetEmailSender(string storeId = null) + { + var serverSender = new ServerEmailSender(_Repository, _JobClient); + if (string.IsNullOrEmpty(storeId)) + return serverSender; + return new StoreEmailSender(_StoreRepository, serverSender, _JobClient, storeId); + } + } +} diff --git a/BTCPayServer/Services/Mails/IEmailSender.cs b/BTCPayServer/Services/Mails/IEmailSender.cs index a48025573..625886566 100644 --- a/BTCPayServer/Services/Mails/IEmailSender.cs +++ b/BTCPayServer/Services/Mails/IEmailSender.cs @@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Mails { public interface IEmailSender { - Task SendEmailAsync(string email, string subject, string message); + void SendEmail(string email, string subject, string message); } } diff --git a/BTCPayServer/Services/Mails/ServerEmailSender.cs b/BTCPayServer/Services/Mails/ServerEmailSender.cs new file mode 100644 index 000000000..66d2cc0c0 --- /dev/null +++ b/BTCPayServer/Services/Mails/ServerEmailSender.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Services.Mails +{ + class ServerEmailSender : EmailSender + { + public ServerEmailSender(SettingsRepository settingsRepository, + IBackgroundJobClient backgroundJobClient) : base(backgroundJobClient) + { + if (settingsRepository == null) + throw new ArgumentNullException(nameof(settingsRepository)); + SettingsRepository = settingsRepository; + } + + public SettingsRepository SettingsRepository { get; } + + public override Task GetEmailSettings() + { + return SettingsRepository.GetSettingAsync(); + } + } +} diff --git a/BTCPayServer/Services/Mails/StoreEmailSender.cs b/BTCPayServer/Services/Mails/StoreEmailSender.cs new file mode 100644 index 000000000..8d9751dd5 --- /dev/null +++ b/BTCPayServer/Services/Mails/StoreEmailSender.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services.Stores; + +namespace BTCPayServer.Services.Mails +{ + class StoreEmailSender : EmailSender + { + public StoreEmailSender(StoreRepository storeRepository, + EmailSender fallback, + IBackgroundJobClient backgroundJobClient, + string storeId) : base(backgroundJobClient) + { + if (storeId == null) + throw new ArgumentNullException(nameof(storeId)); + StoreRepository = storeRepository; + FallbackSender = fallback; + StoreId = storeId; + } + + public StoreRepository StoreRepository { get; } + public EmailSender FallbackSender { get; } + public string StoreId { get; } + + public override async Task GetEmailSettings() + { + var store = await StoreRepository.FindStore(StoreId); + var emailSettings = store.GetStoreBlob().EmailSettings; + if (emailSettings?.IsComplete() == true) + { + return emailSettings; + } + return await FallbackSender.GetEmailSettings(); + } + } +} diff --git a/BTCPayServer/Services/Rates/RateProviderFactory.cs b/BTCPayServer/Services/Rates/RateProviderFactory.cs index 76395f138..e31f2291c 100644 --- a/BTCPayServer/Services/Rates/RateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/RateProviderFactory.cs @@ -104,7 +104,8 @@ namespace BTCPayServer.Services.Rates Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false)); // Cryptopia is often not available - Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false)); + // Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586 + // Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false)); // Handmade providers Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider()); diff --git a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml index 895ec8f3d..1af73a633 100644 --- a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml @@ -1,4 +1,5 @@ -@using BTCPayServer.Hubs +@using BTCPayServer.Crowdfund +@using BTCPayServer.Hubs @addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers @model UpdateCrowdfundViewModel @{ diff --git a/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml b/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml index 38d206846..3c2142371 100644 --- a/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml +++ b/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml @@ -1,41 +1,48 @@
- -
+ +

- {{srvModel.title}} - + {{srvModel.title}} + Starts {{startDateRelativeTime}} - + Ends {{endDateRelativeTime}} - + Currently Active!

- - {{srvModel.targetAmount}} {{targetCurrency}} - + {{srvModel.targetAmount}} {{targetCurrency}} + Dynamic - Hardcap Goal - Softcap Goal + Hardcap Goal + Softcap Goal
-
+
- -
-
+
{{ raisedAmount }} {{targetCurrency}}
Raised
-
+
{{ percentageRaisedAmount }}%
Of Goal
-
+
{{srvModel.info.totalContributors}}
Contributors
-
+
{{endDiff}}
Left
- +
  • {{started? "Started" : "Starts"}} {{startDate}} @@ -78,13 +83,13 @@
-
+
{{startDiff}}
Left to start
- +
  • {{started? "Started" : "Starts"}} {{startDate}} @@ -95,13 +100,13 @@
-
+
Campaign
not active
- +
  • {{started? "Started" : "Starts"}} {{startDate}} @@ -111,14 +116,10 @@
- - - -
- +
  • @@ -126,37 +127,34 @@
- + Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}} -
+
-
- - -

{{srvModel.tagline}}

+
+

{{srvModel.tagline}}

-
- - - +
+