diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index cf7486d4f..f219074aa 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -280,7 +280,8 @@ namespace BTCPayServer.Tests OrderId = "orderId", NotificationURL = callbackServer.GetUri().AbsoluteUri, ItemDesc = "Some description", - FullNotifications = true + FullNotifications = true, + ExtendedNotifications = true }); BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21); tester.ExplorerNode.SendToAddress(url.Address, url.Amount); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index a21b977b5..b4623e5f3 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.1.7 + 1.0.1.8 diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index e690d9565..bf504f5a7 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -237,8 +237,7 @@ namespace BTCPayServer.Controllers try { leases.Add(_EventAggregator.Subscribe(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId))); - leases.Add(_EventAggregator.Subscribe(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId))); - leases.Add(_EventAggregator.Subscribe(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId))); + leases.Add(_EventAggregator.Subscribe(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId))); while (true) { var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken)); @@ -414,6 +413,7 @@ namespace BTCPayServer.Controllers public async Task InvalidatePaidInvoice(string invoiceId) { await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId); + _EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid")); return RedirectToAction(nameof(ListInvoices)); } diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index e1e470734..e6de51232 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -178,7 +178,7 @@ namespace BTCPayServer.Controllers entity.SetCryptoData(cryptoDatas); entity.PosData = invoice.PosData; entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider); - _EventAggregator.Publish(new Events.InvoiceCreatedEvent(entity.Id)); + _EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created")); var resp = entity.EntityToDTO(_NetworkProvider); return new DataWrapper(resp) { Facade = "pos/invoice" }; } diff --git a/BTCPayServer/Events/InvoiceCreatedEvent.cs b/BTCPayServer/Events/InvoiceCreatedEvent.cs deleted file mode 100644 index af0462f85..000000000 --- a/BTCPayServer/Events/InvoiceCreatedEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace BTCPayServer.Events -{ - public class InvoiceCreatedEvent - { - public InvoiceCreatedEvent(string id) - { - InvoiceId = id; - } - - public string InvoiceId { get; set; } - - public override string ToString() - { - return $"Invoice {InvoiceId} created"; - } - } -} diff --git a/BTCPayServer/Events/InvoiceEvent.cs b/BTCPayServer/Events/InvoiceEvent.cs new file mode 100644 index 000000000..b21e15cc9 --- /dev/null +++ b/BTCPayServer/Events/InvoiceEvent.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services.Invoices; + +namespace BTCPayServer.Events +{ + public class InvoiceEvent + { + public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name) + { + + } + public InvoiceEvent(string invoiceId, int code, string name) + { + InvoiceId = invoiceId; + EventCode = code; + Name = name; + } + + public string InvoiceId { get; set; } + public int EventCode { get; set; } + public string Name { get; set; } + + public override string ToString() + { + return $"Invoice {InvoiceId} new event: {Name} ({EventCode})"; + } + } +} diff --git a/BTCPayServer/Events/InvoiceIPNEvent.cs b/BTCPayServer/Events/InvoiceIPNEvent.cs index 98c3a4b15..c2d97de7d 100644 --- a/BTCPayServer/Events/InvoiceIPNEvent.cs +++ b/BTCPayServer/Events/InvoiceIPNEvent.cs @@ -7,19 +7,29 @@ namespace BTCPayServer.Events { public class InvoiceIPNEvent { - public InvoiceIPNEvent(string invoiceId) + public InvoiceIPNEvent(string invoiceId, int? eventCode, string name) { InvoiceId = invoiceId; + EventCode = eventCode; + Name = name; } + public int? EventCode { get; set; } + public string Name { get; set; } + public string InvoiceId { get; set; } public string Error { get; set; } public override string ToString() { + string ipnType = "IPN"; + if(EventCode.HasValue) + { + ipnType = $"IPN ({EventCode.Value} {Name})"; + } if (Error == null) - return $"IPN sent for invoice {InvoiceId}"; - return $"Error while sending IPN: {Error}"; + return $"{ipnType} sent for invoice {InvoiceId}"; + return $"Error while sending {ipnType}: {Error}"; } } } diff --git a/BTCPayServer/Events/InvoicePaymentEvent.cs b/BTCPayServer/Events/InvoicePaymentEvent.cs deleted file mode 100644 index 6280efaa6..000000000 --- a/BTCPayServer/Events/InvoicePaymentEvent.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Services.Invoices; - -namespace BTCPayServer.Events -{ - public class InvoicePaymentEvent - { - - public InvoicePaymentEvent(string invoiceId, string cryptoCode, string address) - { - InvoiceId = invoiceId; - Address = address; - CryptoCode = cryptoCode; - } - - public string Address { get; set; } - public string CryptoCode { get; private set; } - public string InvoiceId { get; set; } - - public override string ToString() - { - return $"{CryptoCode}: Invoice {InvoiceId} received a payment on {Address}"; - } - } -} diff --git a/BTCPayServer/Events/InvoiceStatusChangedEvent.cs b/BTCPayServer/Events/InvoiceStatusChangedEvent.cs deleted file mode 100644 index 925f5fc6b..000000000 --- a/BTCPayServer/Events/InvoiceStatusChangedEvent.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Services.Invoices; - -namespace BTCPayServer.Events -{ - public class InvoiceStatusChangedEvent - { - public InvoiceStatusChangedEvent() - { - - } - public InvoiceStatusChangedEvent(InvoiceEntity invoice, string newState) - { - OldState = invoice.Status; - InvoiceId = invoice.Id; - NewState = newState; - } - public string InvoiceId { get; set; } - public string OldState { get; set; } - public string NewState { get; set; } - - public override string ToString() - { - return $"Invoice {InvoiceId} changed status: {OldState} => {NewState}"; - } - } -} diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 5c27326b2..cb92fc8a1 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -37,6 +37,9 @@ namespace BTCPayServer.HostedServices { get; set; } + + public int? EventCode { get; set; } + public string Message { get; set; } } public ILogger Logger @@ -63,32 +66,32 @@ namespace BTCPayServer.HostedServices _NetworkProvider = networkProvider; } - async Task Notify(InvoiceEntity invoice) + async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null) { CancellationTokenSource cts = new CancellationTokenSource(10000); try { if (string.IsNullOrEmpty(invoice.NotificationURL)) return; - _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id)); - await SendNotification(invoice, cts.Token); + _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id, eventCode, name)); + await SendNotification(invoice, eventCode, name, cts.Token); return; } - catch(OperationCanceledException) when(cts.IsCancellationRequested) + catch (OperationCanceledException) when (cts.IsCancellationRequested) { - _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id) + _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id, eventCode, name) { Error = "Timeout" }); } - catch(Exception ex) // It fails, it is OK because we try with hangfire after + catch (Exception ex) // It fails, it is OK because we try with hangfire after { - _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id) + _EventAggregator.Publish(new InvoiceIPNEvent(invoice.Id, eventCode, name) { Error = ex.Message }); } - var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice }); + 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); } @@ -107,14 +110,18 @@ namespace BTCPayServer.HostedServices CancellationTokenSource cts = new CancellationTokenSource(10000); try { - _EventAggregator.Publish(new InvoiceIPNEvent(job.Invoice.Id)); - HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token); + HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token); reschedule = response.StatusCode != System.Net.HttpStatusCode.OK; 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 + }); } catch (OperationCanceledException) when (cts.IsCancellationRequested) { - _EventAggregator.Publish(new InvoiceIPNEvent(job.Invoice.Id) + _EventAggregator.Publish(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message) { Error = "Timeout" }); @@ -123,12 +130,25 @@ namespace BTCPayServer.HostedServices } catch (Exception ex) // It fails, it is OK because we try with hangfire after { - _EventAggregator.Publish(new InvoiceIPNEvent(job.Invoice.Id) + _EventAggregator.Publish(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message) { Error = ex.Message }); reschedule = true; - Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message); + + List messages = new List(); + while(ex != null) + { + messages.Add(ex.Message); + 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); } @@ -143,8 +163,23 @@ namespace BTCPayServer.HostedServices } } + public class InvoicePaymentNotificationEvent + { + [JsonProperty("code")] + public int Code { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } + public class InvoicePaymentNotificationEventWrapper + { + [JsonProperty("event")] + public InvoicePaymentNotificationEvent Event { get; set; } + [JsonProperty("data")] + public InvoicePaymentNotification Data { get; set; } + } + Encoding UTF8 = new UTF8Encoding(false); - private async Task SendNotification(InvoiceEntity invoice, CancellationToken cancellation) + private async Task SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation) { var request = new HttpRequestMessage(); request.Method = HttpMethod.Post; @@ -167,7 +202,7 @@ namespace BTCPayServer.HostedServices // We keep backward compatibility with bitpay by passing BTC info to the notification // we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked) var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC"); - if(btcCryptoInfo != null) + if (btcCryptoInfo != null) { #pragma warning disable CS0618 notification.Rate = (double)dto.Rate; @@ -177,8 +212,22 @@ namespace BTCPayServer.HostedServices notification.BTCPrice = dto.BTCPrice; #pragma warning restore CS0618 } + + string notificationString = null; + if (eventCode.HasValue) + { + var wrapper = new InvoicePaymentNotificationEventWrapper(); + wrapper.Data = notification; + wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name }; + notificationString = JsonConvert.SerializeObject(wrapper); + } + else + { + notificationString = JsonConvert.SerializeObject(notification); + } + request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute); - request.Content = new StringContent(JsonConvert.SerializeObject(notification), UTF8, "application/json"); + request.Content = new StringContent(notificationString, UTF8, "application/json"); var response = await _Client.SendAsync(request, cancellation); return response; } @@ -193,42 +242,41 @@ namespace BTCPayServer.HostedServices CompositeDisposable leases = new CompositeDisposable(); public Task StartAsync(CancellationToken cancellationToken) { - leases.Add(_EventAggregator.Subscribe(async e => + leases.Add(_EventAggregator.Subscribe(async e => { var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId); + await 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) { - if (e.NewState == "expired" || - e.NewState == "paid" || - e.NewState == "invalid" || - e.NewState == "complete" + if (e.Name == "invoice_expired" || + e.Name == "invoice_paidInFull" || + e.Name == "invoice_failedToConfirm" || + e.Name == "invoice_markedInvalid" || + e.Name == "invoice_failedToConfirm" || + e.Name == "invoice_completed" ) await Notify(invoice); } - - if(e.NewState == "confirmed") + + if (e.Name == "invoice_confirmed") { await Notify(invoice); } - await SaveEvent(invoice.Id, e); + + if (invoice.ExtendedNotifications) + { + await Notify(invoice, e.EventCode, e.Name); + } })); - leases.Add(_EventAggregator.Subscribe(async e => + + leases.Add(_EventAggregator.Subscribe(async e => { await SaveEvent(e.InvoiceId, e); })); - leases.Add(_EventAggregator.Subscribe(async e => - { - await SaveEvent(e.InvoiceId, e); - })); - - leases.Add(_EventAggregator.Subscribe(async e => - { - await SaveEvent(e.InvoiceId, e); - })); leases.Add(_EventAggregator.Subscribe(async e => { diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index c938136c1..48247133c 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -146,7 +146,7 @@ namespace BTCPayServer.HostedServices context.MarkDirty(); await _InvoiceRepository.UnaffectAddress(invoice.Id); - context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired")); + context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired")); invoice.Status = "expired"; } @@ -169,7 +169,7 @@ namespace BTCPayServer.HostedServices invoice.Payments.Add(payment); #pragma warning restore CS0618 alreadyAccounted.Add(coin.Coin.Outpoint); - context.Events.Add(new InvoicePaymentEvent(invoice.Id, coins.Wallet.Network.CryptoCode, coin.Coin.ScriptPubKey.GetDestinationAddress(coins.Wallet.Network.NBitcoinNetwork).ToString())); + context.Events.Add(new InvoiceEvent(invoice, 1002, "invoice_receivedPayment")); dirtyAddress = true; } if (dirtyAddress) @@ -188,7 +188,7 @@ namespace BTCPayServer.HostedServices { if (invoice.Status == "new") { - context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid")); + context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull")); invoice.Status = "paid"; invoice.ExceptionStatus = null; await _InvoiceRepository.UnaffectAddress(invoice.Id); @@ -197,6 +197,7 @@ namespace BTCPayServer.HostedServices else if (invoice.Status == "expired") { invoice.ExceptionStatus = "paidLate"; + context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration")); context.MarkDirty(); } } @@ -246,14 +247,14 @@ namespace BTCPayServer.HostedServices (totalConfirmed < accounting.TotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); - context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid")); + context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm")); invoice.Status = "invalid"; context.MarkDirty(); } else if (totalConfirmed >= accounting.TotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); - context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed")); + context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed")); invoice.Status = "confirmed"; context.MarkDirty(); } @@ -266,7 +267,7 @@ namespace BTCPayServer.HostedServices var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalConfirmed >= accounting.TotalDue) { - context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete")); + context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed")); invoice.Status = "complete"; context.MarkDirty(); } @@ -442,7 +443,13 @@ namespace BTCPayServer.HostedServices leases.Add(_EventAggregator.Subscribe(async b => { await NotifyBlock(); })); leases.Add(_EventAggregator.Subscribe(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); })); - leases.Add(_EventAggregator.Subscribe(async b => { await Watch(b.InvoiceId); })); + leases.Add(_EventAggregator.Subscribe(async b => + { + if(b.Name == "invoice_created") + { + await Watch(b.InvoiceId); + } + })); return Task.CompletedTask; } diff --git a/BTCPayServer/HostedServices/NBXplorerListener.cs b/BTCPayServer/HostedServices/NBXplorerListener.cs index 84bb81333..c0b0bb7db 100644 --- a/BTCPayServer/HostedServices/NBXplorerListener.cs +++ b/BTCPayServer/HostedServices/NBXplorerListener.cs @@ -87,19 +87,22 @@ namespace BTCPayServer.HostedServices }, null, 0, (int)PollInterval.TotalMilliseconds); leases.Add(_ListenPoller); - leases.Add(_Aggregator.Subscribe(async inv => + leases.Add(_Aggregator.Subscribe(async inv => { - var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId); - List listeningDerivations = new List(); - foreach (var notificationSessions in _Sessions) + if (inv.Name == "invoice_created") { - var derivationStrategy = GetStrategy(notificationSessions.Key, invoice); - if (derivationStrategy != null) + var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId); + List listeningDerivations = new List(); + foreach (var notificationSessions in _Sessions) { - listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token)); + var derivationStrategy = GetStrategy(notificationSessions.Key, invoice); + if (derivationStrategy != null) + { + listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token)); + } } + await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false); } - await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false); })); return Task.CompletedTask; }