diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 0011dac21..88a666fd4 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -108,7 +108,7 @@ namespace BTCPayServer.Tests _Host.Start(); Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime)); var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher)); - watcher.PollInterval = TimeSpan.FromMilliseconds(50); + watcher.PollInterval = TimeSpan.FromMilliseconds(500); } public BTCPayServerRuntime Runtime diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index e5d07ab78..be6029bbe 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.0.8 + 1.0.0.9 diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index cf69251b5..165fc1228 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -17,8 +17,79 @@ using System.Threading.Tasks; namespace BTCPayServer.Controllers { - public partial class InvoiceController - { + public partial class InvoiceController + { + + [HttpPost] + [Route("invoices/{invoiceId}")] + public async Task Invoice(string invoiceId, string command) + { + if(command == "refresh") + { + await _Watcher.WatchAsync(invoiceId, true); + } + StatusMessage = "Invoice is state is being refreshed, please refresh the page soon..."; + return RedirectToAction(nameof(Invoice), new + { + invoiceId = invoiceId + }); + } + + [HttpGet] + [Route("invoices/{invoiceId}")] + public async Task Invoice(string invoiceId) + { + var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() + { + UserId = GetUserId(), + InvoiceId = invoiceId + })).FirstOrDefault(); + if(invoice == null) + return NotFound(); + + var dto = invoice.EntityToDTO(); + var store = await _StoreRepository.FindStore(invoice.StoreId); + InvoiceDetailsModel model = new InvoiceDetailsModel() + { + StoreName = store.StoreName, + StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }), + Id = invoice.Id, + Status = invoice.Status, + RefundEmail = invoice.RefundMail, + CreatedDate = invoice.InvoiceTime, + ExpirationDate = invoice.ExpirationTime, + OrderId = invoice.OrderId, + BuyerInformation = invoice.BuyerInformation, + Rate = invoice.Rate, + Fiat = dto.Price + " " + dto.Currency, + BTC = invoice.GetTotalCryptoDue().ToString() + " BTC", + BTCDue = invoice.GetCryptoDue().ToString() + " BTC", + BTCPaid = invoice.GetTotalPaid().ToString() + " BTC", + NetworkFee = invoice.GetNetworkFee().ToString() + " BTC", + NotificationUrl = invoice.NotificationURL, + ProductInformation = invoice.ProductInformation, + BitcoinAddress = invoice.DepositAddress, + PaymentUrl = dto.PaymentUrls.BIP72 + }; + + var payments = invoice + .Payments + .Select(async payment => + { + var m = new InvoiceDetailsModel.Payment(); + m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network); + m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0; + m.TransactionId = payment.Outpoint.Hash.ToString(); + m.ReceivedTime = payment.ReceivedTime; + m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/{m.TransactionId}"; + return m; + }) + .ToArray(); + await Task.WhenAll(payments); + model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList(); + model.StatusMessage = StatusMessage; + return View(model); + } [HttpGet] [Route("i/{invoiceId}")] @@ -168,14 +239,14 @@ namespace BTCPayServer.Controllers FullNotifications = true, BuyerEmail = model.BuyerEmail, }, store); - + StatusMessage = $"Invoice {result.Data.Id} just created!"; return RedirectToAction(nameof(ListInvoices)); } private async Task GetStores(string userId, string storeId = null) { - return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); + return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); } [HttpPost] diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 64e9fab47..097c3d134 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -37,6 +37,7 @@ using BTCPayServer.Validations; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc.Routing; using NBXplorer.DerivationStrategy; +using NBXplorer; namespace BTCPayServer.Controllers { @@ -51,6 +52,7 @@ namespace BTCPayServer.Controllers Network _Network; UserManager _UserManager; IFeeProvider _FeeProvider; + ExplorerClient _Explorer; public InvoiceController( Network network, @@ -61,8 +63,10 @@ namespace BTCPayServer.Controllers IRateProvider rateProvider, StoreRepository storeRepository, InvoiceWatcher watcher, + ExplorerClient explorerClient, IFeeProvider feeProvider) { + _Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _Network = network ?? throw new ArgumentNullException(nameof(network)); _TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository)); diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs new file mode 100644 index 000000000..4c0e47790 --- /dev/null +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Servcices.Invoices; +using NBitcoin; + +namespace BTCPayServer.Models.InvoicingModels +{ + public class InvoiceDetailsModel + { + public class Payment + { + public int Confirmations + { + get; set; + } + public BitcoinAddress DepositAddress + { + get; set; + } + public string Amount + { + get; set; + } + public string TransactionId + { + get; set; + } + public DateTimeOffset ReceivedTime + { + get; + internal set; + } + public string TransactionLink + { + get; + set; + } + } + + public string StatusMessage + { + get; set; + } + public String Id + { + get; set; + } + + public List Payments + { + get; set; + } = new List(); + + public string Status + { + get; set; + } + + public DateTimeOffset CreatedDate + { + get; set; + } + + public DateTimeOffset ExpirationDate + { + get; set; + } + + public string OrderId + { + get; set; + } + public string RefundEmail + { + get; + set; + } + public BuyerInformation BuyerInformation + { + get; + set; + } + public object StoreName + { + get; + internal set; + } + public string StoreLink + { + get; + set; + } + public double Rate + { + get; + internal set; + } + public string NotificationUrl + { + get; + internal set; + } + public string Fiat + { + get; + set; + } + public string BTC + { + get; + set; + } + public string BTCDue + { + get; + set; + } + public string BTCPaid + { + get; + internal set; + } + public String NetworkFee + { + get; + internal set; + } + public ProductInformation ProductInformation + { + get; + internal set; + } + public BitcoinAddress BitcoinAddress + { + get; + internal set; + } + public string PaymentUrl + { + get; + set; + } + } +} diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 4756db01e..1b4daae16 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -302,6 +302,12 @@ namespace BTCPayServer.Servcices.Invoices var str = JsonConvert.SerializeObject(from); JsonConvert.PopulateObject(str, dest); } + + public Money GetNetworkFee() + { + var item = Calculate(); + return TxFee * item.TxCount; + } } public class PaymentEntity diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 49f280458..b313d5ea7 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -65,12 +65,17 @@ namespace BTCPayServer.Servcices.Invoices } } - public async Task RemovePendingInvoice(string invoiceId) + public async Task RemovePendingInvoice(string invoiceId) { using(var ctx = _ContextFactory.CreateContext()) { ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId }); - await ctx.SaveChangesAsync(); + try + { + await ctx.SaveChangesAsync(); + return true; + } + catch(DbUpdateException) { return false; } } } @@ -207,12 +212,16 @@ namespace BTCPayServer.Servcices.Invoices { using(var context = _ContextFactory.CreateContext()) { - IQueryable query = context .Invoices .Include(o => o.Payments) .Include(o => o.RefundAddresses); + if(!string.IsNullOrEmpty(queryObject.InvoiceId)) + { + query = query.Where(i => i.Id == queryObject.InvoiceId); + } + if(!string.IsNullOrEmpty(queryObject.StoreId)) { query = query.Where(i => i.StoreDataId == queryObject.StoreId); @@ -380,5 +389,10 @@ namespace BTCPayServer.Servcices.Invoices { get; set; } + public string InvoiceId + { + get; + set; + } } } diff --git a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs b/BTCPayServer/Services/Invoices/InvoiceWatcher.cs index 8492476eb..62c58e4e2 100644 --- a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs +++ b/BTCPayServer/Services/Invoices/InvoiceWatcher.cs @@ -24,11 +24,13 @@ namespace BTCPayServer.Servcices.Invoices InvoiceNotificationManager _NotificationManager; BTCPayWallet _Wallet; - public InvoiceWatcher(ExplorerClient explorerClient, + public InvoiceWatcher(ExplorerClient explorerClient, InvoiceRepository invoiceRepository, BTCPayWallet wallet, InvoiceNotificationManager notificationManager) { + LongPollingMode = explorerClient.Network == Network.RegTest; + PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0); _Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); _ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient)); _DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network); @@ -36,6 +38,11 @@ namespace BTCPayServer.Servcices.Invoices _NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager)); } + public bool LongPollingMode + { + get; set; + } + public async Task NotifyReceived(Script scriptPubKey) { var invoice = await _Wallet.GetInvoiceId(scriptPubKey); @@ -52,7 +59,6 @@ namespace BTCPayServer.Servcices.Invoices private async Task UpdateInvoice(string invoiceId) { - Logs.PayServer.LogInformation("Updating invoice " + invoiceId); UTXOChanges changes = null; while(true) { @@ -75,8 +81,8 @@ namespace BTCPayServer.Servcices.Invoices if(invoice.Status == "complete" || invoice.Status == "invalid") { - await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false); - Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); + if(await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false)) + Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); break; } @@ -97,34 +103,27 @@ namespace BTCPayServer.Servcices.Invoices private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice) - { - if(invoice.Status == "invalid") - { - return (false, changes); - } + { bool needSave = false; - bool shouldWait = true; - if(invoice.ExpirationTime < DateTimeOffset.UtcNow && (invoice.Status == "new" || invoice.Status == "paidPartial")) + if(invoice.Status != "invalid" && invoice.ExpirationTime < DateTimeOffset.UtcNow && (invoice.Status == "new" || invoice.Status == "paidPartial")) { needSave = true; invoice.Status = "invalid"; } - if(invoice.Status == "new" || invoice.Status == "paidPartial") + if(invoice.Status == "invalid" || invoice.Status == "new" || invoice.Status == "paidPartial") { var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); - changes = await _ExplorerClient.SyncAsync(strategy, changes, true, _Cts.Token).ConfigureAwait(false); + changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false); var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray(); utxos = utxos - .Where((u,i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id) + .Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id) .ToArray(); - shouldWait = false; //should not wait, Sync is blocking call - List receivedCoins = new List(); foreach(var received in utxos) if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey) @@ -218,24 +217,31 @@ namespace BTCPayServer.Servcices.Invoices } } - shouldWait = shouldWait && !needSave; - - if(shouldWait) - { - await Task.Delay(PollInterval, _Cts.Token).ConfigureAwait(false); - } - return (needSave, changes); } + + TimeSpan _PollInterval; public TimeSpan PollInterval { - get; set; - } = TimeSpan.FromSeconds(10); + get + { + return _PollInterval; + } + set + { + _PollInterval = value; + if(_UpdatePendingInvoices != null) + { + _UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds); + } + } + } - public async Task WatchAsync(string invoiceId) + public async Task WatchAsync(string invoiceId, bool singleShot = false) { - await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false); + if(!singleShot) + await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false); _WatchRequests.Add(invoiceId); } @@ -264,7 +270,7 @@ namespace BTCPayServer.Servcices.Invoices { _WatchRequests.Add(pending); } - }, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds); + }, null, 0, (int)PollInterval.TotalMilliseconds); return Task.CompletedTask; } @@ -279,7 +285,7 @@ namespace BTCPayServer.Servcices.Invoices var localItem = item; // If the invoice is already updating, ignore - Lazy updateInvoice =new Lazy(() => UpdateInvoice(localItem), false); + Lazy updateInvoice = new Lazy(() => UpdateInvoice(localItem), false); if(updating.TryAdd(item, updateInvoice)) { updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice)); diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml new file mode 100644 index 000000000..3bb55b594 --- /dev/null +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -0,0 +1,187 @@ +@model InvoiceDetailsModel +@{ + ViewData["Title"] = "Invoice " + Model.Id; +} + +
+
+ +
+
+ @Html.Partial("_StatusMessage", Model.StatusMessage) +
+
+ +
+
+

@ViewData["Title"]

+
+

Invoice details

+
+
+ +
+
+

Information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Store@Model.StoreName
Id@Model.Id
Created date@Model.CreatedDate
Expiration date@Model.CreatedDate
Status@Model.Status
Refund email@Model.RefundEmail
Order Id@Model.OrderId
Rate@Model.Rate
Total fiat due@Model.Fiat
Network Fee@Model.NetworkFee
Total crypto due@Model.BTC
Crypto due@Model.BTCDue
Crypto paid@Model.BTCPaid
Notification Url@Model.NotificationUrl
Payment address@Model.BitcoinAddress
Payment Url@Model.PaymentUrl
+
+ +
+

Buyer information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + + @Model.BuyerInformation.BuyerName
Email@Model.BuyerInformation.BuyerEmail
Phone@Model.BuyerInformation.BuyerPhone
Address 1@Model.BuyerInformation.BuyerAddress1
Address 2@Model.BuyerInformation.BuyerAddress2
City@Model.BuyerInformation.BuyerCity
State@Model.BuyerInformation.BuyerState
Country@Model.BuyerInformation.BuyerCountry
Zip@Model.BuyerInformation.BuyerZip
+ +

Product information

+ + + + + + + + + + + + + +
Item code@Model.ProductInformation.ItemCode
Item Description@Model.ProductInformation.ItemDesc
Price@Model.ProductInformation.Price @Model.ProductInformation.Currency
+
+
+
+
+

Payments

+
+
+ +
+
+ + + + + + + + + + + @foreach(var payment in Model.Payments) + { + + + + + + + } + +
DateDeposit addressTransaction IdConfirmations
@payment.ReceivedTime@payment.DepositAddress@payment.TransactionId@payment.Confirmations
+
+
+
+
\ No newline at end of file diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index c718946de..dadc97cb3 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -50,7 +50,7 @@ @invoice.InvoiceId @invoice.Status @invoice.AmountCurrency - Checkout + Checkout - Details }