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;
}