Improve tests of webhooks

This commit is contained in:
nicolas.dorier 2020-11-13 16:28:15 +09:00
parent 94bcbeb604
commit df79c2cf48
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
13 changed files with 269 additions and 30 deletions

View file

@ -9,6 +9,14 @@ namespace BTCPayServer.Client.Models
{ {
public class WebhookEvent public class WebhookEvent
{ {
public readonly static JsonSerializerSettings DefaultSerializerSettings;
static WebhookEvent()
{
DefaultSerializerSettings = new JsonSerializerSettings();
DefaultSerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
DefaultSerializerSettings.Formatting = Formatting.None;
}
public string DeliveryId { get; set; } public string DeliveryId { get; set; }
public string OrignalDeliveryId { get; set; } public string OrignalDeliveryId { get; set; }
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]

View file

@ -8,9 +8,85 @@ namespace BTCPayServer.Client.Models
{ {
public class WebhookInvoiceEvent : WebhookEvent public class WebhookInvoiceEvent : WebhookEvent
{ {
public WebhookInvoiceEvent()
{
}
public WebhookInvoiceEvent(WebhookEventType evtType)
{
this.Type = evtType;
}
[JsonProperty(Order = 1)] [JsonProperty(Order = 1)]
public string StoreId { get; set; } public string StoreId { get; set; }
[JsonProperty(Order = 2)] [JsonProperty(Order = 2)]
public string InvoiceId { get; set; } public string InvoiceId { get; set; }
public T ReadAs<T>()
{
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);
return JsonConvert.DeserializeObject<T>(str, DefaultSerializerSettings);
}
}
public class WebhookInvoiceConfirmedEvent : WebhookInvoiceEvent
{
public WebhookInvoiceConfirmedEvent()
{
}
public WebhookInvoiceConfirmedEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool ManuallyMarked { get; set; }
}
public class WebhookInvoiceInvalidEvent : WebhookInvoiceEvent
{
public WebhookInvoiceInvalidEvent()
{
}
public WebhookInvoiceInvalidEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool ManuallyMarked { get; set; }
}
public class WebhookInvoicePaidEvent : WebhookInvoiceEvent
{
public WebhookInvoicePaidEvent()
{
}
public WebhookInvoicePaidEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool OverPaid { get; set; }
public bool PaidAfterExpiration { get; set; }
}
public class WebhookInvoiceReceivedPaymentEvent : WebhookInvoiceEvent
{
public WebhookInvoiceReceivedPaymentEvent()
{
}
public WebhookInvoiceReceivedPaymentEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool AfterExpiration { get; set; }
}
public class WebhookInvoiceExpiredEvent : WebhookInvoiceEvent
{
public WebhookInvoiceExpiredEvent()
{
}
public WebhookInvoiceExpiredEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool PartiallyPaid { get; set; }
} }
} }

View file

@ -89,7 +89,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click(); s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
//there should be a store already by default in the dropdown //there should be a store already by default in the dropdown
var dropdown = s.Driver.FindElement(By.Name("PermissionValues[3].SpecificStores[0]")); var dropdown = s.Driver.FindElement(By.Name("PermissionValues[4].SpecificStores[0]"));
var option = dropdown.FindElement(By.TagName("option")); var option = dropdown.FindElement(By.TagName("option"));
var storeId = option.GetAttribute("value"); var storeId = option.GetAttribute("value");
option.Click(); option.Click();

View file

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ExchangeSharp;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
@ -62,9 +63,9 @@ namespace BTCPayServer.Tests
semaphore.Dispose(); semaphore.Dispose();
} }
public async Task<HttpContext> GetNextRequest() public async Task<HttpContext> GetNextRequest(CancellationToken cancellationToken = default)
{ {
return await _channel.Reader.ReadAsync(); return await _channel.Reader.ReadAsync(cancellationToken);
} }
} }
} }

View file

@ -619,7 +619,7 @@ namespace BTCPayServer.Tests
{ {
Assert.True(hook.Enabled); Assert.True(hook.Enabled);
Assert.True(hook.AuthorizedEvents.Everything); Assert.True(hook.AuthorizedEvents.Everything);
Assert.True(hook.AutomaticRedelivery); Assert.False(hook.AutomaticRedelivery);
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url); Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
} }
using var tester = ServerTester.Create(); using var tester = ServerTester.Create();
@ -913,6 +913,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
await user.GrantAccessAsync(); await user.GrantAccessAsync();
await user.MakeAdmin(); await user.MakeAdmin();
await user.SetupWebhook();
var client = await user.CreateClient(Policies.Unrestricted); var client = await user.CreateClient(Policies.Unrestricted);
var viewOnly = await user.CreateClient(Policies.CanViewInvoices); var viewOnly = await user.CreateClient(Policies.CanViewInvoices);
@ -970,6 +971,38 @@ namespace BTCPayServer.Tests
await client.UnarchiveInvoice(user.StoreId, invoice.Id); await client.UnarchiveInvoice(user.StoreId, invoice.Id);
Assert.NotNull(await client.GetInvoice(user.StoreId, invoice.Id)); Assert.NotNull(await client.GetInvoice(user.StoreId, invoice.Id));
foreach (var marked in new[] { InvoiceStatus.Complete, InvoiceStatus.Invalid })
{
var inv = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 100 });
await user.PayInvoice(inv.Id);
await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest()
{
Status = marked
});
var result = await client.GetInvoice(user.StoreId, inv.Id);
if (marked == InvoiceStatus.Complete)
{
Assert.Equal(InvoiceStatus.Complete, result.Status);
user.AssertHasWebhookEvent<WebhookInvoiceConfirmedEvent>(WebhookEventType.InvoiceConfirmed,
o =>
{
Assert.Equal(inv.Id, o.InvoiceId);
Assert.True(o.ManuallyMarked);
});
}
if (marked == InvoiceStatus.Invalid)
{
Assert.Equal(InvoiceStatus.Invalid, result.Status);
user.AssertHasWebhookEvent<WebhookInvoiceInvalidEvent>(WebhookEventType.InvoiceInvalid,
o =>
{
Assert.Equal(inv.Id, o.InvoiceId);
Assert.True(o.ManuallyMarked);
});
}
}
} }
} }

View file

@ -23,6 +23,7 @@ namespace BTCPayServer.Tests
return new ServerTester(scope, newDb); return new ServerTester(scope, newDb);
} }
public List<IDisposable> Resources = new List<IDisposable>();
readonly string _Directory; readonly string _Directory;
public ServerTester(string scope, bool newDb) public ServerTester(string scope, bool newDb)
{ {
@ -145,7 +146,7 @@ namespace BTCPayServer.Tests
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
var sub = PayTester.GetService<EventAggregator>().Subscribe<T>(evt => var sub = PayTester.GetService<EventAggregator>().Subscribe<T>(evt =>
{ {
if(correctEvent is null) if (correctEvent is null)
tcs.TrySetResult(evt); tcs.TrySetResult(evt);
else if (correctEvent(evt)) else if (correctEvent(evt))
{ {
@ -207,6 +208,8 @@ namespace BTCPayServer.Tests
public void Dispose() public void Dispose()
{ {
foreach (var r in this.Resources)
r.Dispose();
Logs.Tester.LogInformation("Disposing the BTCPayTester..."); Logs.Tester.LogInformation("Disposing the BTCPayTester...");
foreach (var store in Stores) foreach (var store in Stores)
{ {

View file

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
@ -23,8 +25,10 @@ using NBitcoin.Payment;
using NBitpayClient; using NBitpayClient;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Xunit; using Xunit;
using Xunit.Sdk;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@ -427,5 +431,86 @@ namespace BTCPayServer.Tests
return null; return null;
return parsedBip21; return parsedBip21;
} }
class WebhookListener : IDisposable
{
private Client.Models.StoreWebhookData _wh;
private FakeServer _server;
private readonly List<WebhookInvoiceEvent> _webhookEvents;
private CancellationTokenSource _cts;
public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List<WebhookInvoiceEvent> webhookEvents)
{
_wh = wh;
_server = server;
_webhookEvents = webhookEvents;
_cts = new CancellationTokenSource();
_ = Listen(_cts.Token);
}
async Task Listen(CancellationToken cancellation)
{
while (!cancellation.IsCancellationRequested)
{
var req = await _server.GetNextRequest(cancellation);
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
var callback = Encoding.UTF8.GetString(bytes);
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
req.Response.StatusCode = 200;
_server.Done();
}
}
public void Dispose()
{
_cts.Cancel();
_server.Dispose();
}
}
public List<WebhookInvoiceEvent> WebhookEvents { get; set; } = new List<WebhookInvoiceEvent>();
public TEvent AssertHasWebhookEvent<TEvent>(WebhookEventType eventType, Action<TEvent> assert) where TEvent : class
{
foreach (var evt in WebhookEvents)
{
if (evt.Type == eventType)
{
var typedEvt = evt.ReadAs<TEvent>();
try
{
assert(typedEvt);
return typedEvt;
}
catch (XunitException ex)
{
}
}
}
Assert.True(false, "No webhook event match the assertion");
return null;
}
public async Task SetupWebhook()
{
FakeServer server = new FakeServer();
await server.Start();
var client = await CreateClient(Policies.CanModifyStoreWebhooks);
var wh = await client.CreateWebhook(StoreId, new CreateStoreWebhookRequest()
{
AutomaticRedelivery = false,
Url = server.ServerUri.AbsoluteUri
});
parent.Resources.Add(new WebhookListener(wh, server, WebhookEvents));
}
public async Task PayInvoice(string invoiceId)
{
var inv = await BitPay.GetInvoiceAsync(invoiceId);
var net = parent.ExplorerNode.Network;
this.parent.ExplorerNode.SendToAddress(BitcoinAddress.Create(inv.BitcoinAddress, net), inv.BtcDue);
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = await BitPay.GetInvoiceAsync(invoiceId, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
});
}
} }
} }

View file

@ -2526,6 +2526,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
await user.SetupWebhook();
var invoice = user.BitPay.CreateInvoice( var invoice = user.BitPay.CreateInvoice(
new Invoice() new Invoice()
{ {
@ -2582,7 +2583,6 @@ namespace BTCPayServer.Tests
var cashCow = tester.ExplorerNode; var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var iii = ctx.AddressInvoices.ToArray();
Assert.True(IsMapped(invoice, ctx)); Assert.True(IsMapped(invoice, ctx));
cashCow.SendToAddress(invoiceAddress, firstPayment); cashCow.SendToAddress(invoiceAddress, firstPayment);
@ -2686,6 +2686,23 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Zero, localInvoice.BtcDue); Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
}); });
// Test on the webhooks
user.AssertHasWebhookEvent<WebhookInvoiceConfirmedEvent>(WebhookEventType.InvoiceConfirmed,
c =>
{
Assert.False(c.ManuallyMarked);
});
user.AssertHasWebhookEvent<WebhookInvoicePaidEvent>(WebhookEventType.InvoicePaidInFull,
c =>
{
Assert.True(c.OverPaid);
});
user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(WebhookEventType.InvoiceReceivedPayment,
c =>
{
Assert.False(c.AfterExpiration);
});
} }
} }

View file

@ -51,7 +51,7 @@ namespace BTCPayServer.Controllers
} }
[HttpGet] [HttpGet]
[Route("invoices")] [Route("invoices")]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices( public async Task<IActionResult> GetInvoices(
string token, string token,
DateTimeOffset? dateStart = null, DateTimeOffset? dateStart = null,
DateTimeOffset? dateEnd = null, DateTimeOffset? dateEnd = null,
@ -61,6 +61,8 @@ namespace BTCPayServer.Controllers
int? limit = null, int? limit = null,
int? offset = null) int? offset = null)
{ {
if (User.Identity.AuthenticationType == Security.Bitpay.BitpayAuthenticationTypes.Anonymous)
return Forbid(Security.Bitpay.BitpayAuthenticationTypes.Anonymous);
if (dateEnd != null) if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
@ -79,7 +81,7 @@ namespace BTCPayServer.Controllers
var entities = (await _InvoiceRepository.GetInvoices(query)) var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray(); .Select((o) => o.EntityToDTO()).ToArray();
return DataWrapper.Create(entities); return Json(DataWrapper.Create(entities));
} }
} }
} }

View file

@ -38,7 +38,7 @@ namespace BTCPayServer.Events
{ReceivedPayment, InvoiceEventCode.ReceivedPayment}, {ReceivedPayment, InvoiceEventCode.ReceivedPayment},
{PaidInFull, InvoiceEventCode.PaidInFull}, {PaidInFull, InvoiceEventCode.PaidInFull},
{Expired, InvoiceEventCode.Expired}, {Expired, InvoiceEventCode.Expired},
{Confirmed, InvoiceEventCode.Completed}, {Confirmed, InvoiceEventCode.Confirmed},
{Completed, InvoiceEventCode.Completed}, {Completed, InvoiceEventCode.Completed},
{MarkedInvalid, InvoiceEventCode.MarkedInvalid}, {MarkedInvalid, InvoiceEventCode.MarkedInvalid},
{FailedToConfirm, InvoiceEventCode.FailedToConfirm}, {FailedToConfirm, InvoiceEventCode.FailedToConfirm},

View file

@ -43,10 +43,7 @@ namespace BTCPayServer.HostedServices
public readonly static JsonSerializerSettings DefaultSerializerSettings; public readonly static JsonSerializerSettings DefaultSerializerSettings;
static WebhookNotificationManager() static WebhookNotificationManager()
{ {
DefaultSerializerSettings = new JsonSerializerSettings(); DefaultSerializerSettings = WebhookEvent.DefaultSerializerSettings;
DefaultSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
DefaultSerializerSettings.Formatting = Formatting.None;
} }
public const string OnionNamedClient = "greenfield-webhook.onion"; public const string OnionNamedClient = "greenfield-webhook.onion";
public const string ClearnetNamedClient = "greenfield-webhook.clearnet"; public const string ClearnetNamedClient = "greenfield-webhook.clearnet";
@ -113,7 +110,6 @@ namespace BTCPayServer.HostedServices
var webhookEvent = newDeliveryBlob.ReadRequestAs<WebhookEvent>(); var webhookEvent = newDeliveryBlob.ReadRequestAs<WebhookEvent>();
webhookEvent.DeliveryId = newDelivery.Id; webhookEvent.DeliveryId = newDelivery.Id;
webhookEvent.OrignalDeliveryId ??= deliveryId; webhookEvent.OrignalDeliveryId ??= deliveryId;
webhookEvent.Timestamp = newDelivery.Timestamp;
newDeliveryBlob.Request = ToBytes(webhookEvent); newDeliveryBlob.Request = ToBytes(webhookEvent);
newDelivery.SetBlob(newDeliveryBlob); newDelivery.SetBlob(newDeliveryBlob);
return new WebhookDeliveryRequest(webhookDelivery.Webhook.Id, webhookEvent, newDelivery, webhookDelivery.Webhook.GetBlob()); return new WebhookDeliveryRequest(webhookDelivery.Webhook.Id, webhookEvent, newDelivery, webhookDelivery.Webhook.GetBlob());
@ -126,16 +122,14 @@ namespace BTCPayServer.HostedServices
foreach (var webhook in webhooks) foreach (var webhook in webhooks)
{ {
var webhookBlob = webhook.GetBlob(); var webhookBlob = webhook.GetBlob();
if (!(GetWebhookEvent(invoiceEvent.EventCode) is WebhookEventType webhookEventType)) if (!(GetWebhookEvent(invoiceEvent) is WebhookInvoiceEvent webhookEvent))
continue; continue;
if (!ShouldDeliver(webhookEventType, webhookBlob)) if (!ShouldDeliver(webhookEvent.Type, webhookBlob))
continue; continue;
Data.WebhookDeliveryData delivery = NewDelivery(); Data.WebhookDeliveryData delivery = NewDelivery();
delivery.WebhookId = webhook.Id; delivery.WebhookId = webhook.Id;
var webhookEvent = new WebhookInvoiceEvent();
webhookEvent.InvoiceId = invoiceEvent.InvoiceId; webhookEvent.InvoiceId = invoiceEvent.InvoiceId;
webhookEvent.StoreId = invoiceEvent.Invoice.StoreId; webhookEvent.StoreId = invoiceEvent.Invoice.StoreId;
webhookEvent.Type = webhookEventType;
webhookEvent.DeliveryId = delivery.Id; webhookEvent.DeliveryId = delivery.Id;
webhookEvent.OrignalDeliveryId = delivery.Id; webhookEvent.OrignalDeliveryId = delivery.Id;
webhookEvent.Timestamp = delivery.Timestamp; webhookEvent.Timestamp = delivery.Timestamp;
@ -158,28 +152,45 @@ namespace BTCPayServer.HostedServices
_ = Process(context.WebhookId, channel); _ = Process(context.WebhookId, channel);
} }
private WebhookEventType? GetWebhookEvent(InvoiceEventCode eventCode) private WebhookInvoiceEvent GetWebhookEvent(InvoiceEvent invoiceEvent)
{ {
var eventCode = invoiceEvent.EventCode;
switch (eventCode) switch (eventCode)
{ {
case InvoiceEventCode.Completed: case InvoiceEventCode.Completed:
return null; return null;
case InvoiceEventCode.Confirmed: case InvoiceEventCode.Confirmed:
case InvoiceEventCode.MarkedCompleted: case InvoiceEventCode.MarkedCompleted:
return WebhookEventType.InvoiceConfirmed; return new WebhookInvoiceConfirmedEvent(WebhookEventType.InvoiceConfirmed)
{
ManuallyMarked = eventCode == InvoiceEventCode.MarkedCompleted
};
case InvoiceEventCode.Created: case InvoiceEventCode.Created:
return WebhookEventType.InvoiceCreated; return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated);
case InvoiceEventCode.Expired: case InvoiceEventCode.Expired:
case InvoiceEventCode.ExpiredPaidPartial: case InvoiceEventCode.ExpiredPaidPartial:
return WebhookEventType.InvoiceExpired; return new WebhookInvoiceExpiredEvent(WebhookEventType.InvoiceExpired)
{
PartiallyPaid = eventCode == InvoiceEventCode.ExpiredPaidPartial
};
case InvoiceEventCode.FailedToConfirm: case InvoiceEventCode.FailedToConfirm:
case InvoiceEventCode.MarkedInvalid: case InvoiceEventCode.MarkedInvalid:
return WebhookEventType.InvoiceInvalid; return new WebhookInvoiceInvalidEvent(WebhookEventType.InvoiceInvalid)
{
ManuallyMarked = eventCode == InvoiceEventCode.MarkedInvalid
};
case InvoiceEventCode.PaidInFull: case InvoiceEventCode.PaidInFull:
return WebhookEventType.InvoicePaidInFull;
case InvoiceEventCode.ReceivedPayment:
case InvoiceEventCode.PaidAfterExpiration: case InvoiceEventCode.PaidAfterExpiration:
return WebhookEventType.InvoiceReceivedPayment; return new WebhookInvoicePaidEvent(WebhookEventType.InvoicePaidInFull)
{
OverPaid = invoiceEvent.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver,
PaidAfterExpiration = eventCode == InvoiceEventCode.PaidAfterExpiration
};
case InvoiceEventCode.ReceivedPayment:
return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment)
{
AfterExpiration = invoiceEvent.Invoice.Status == InvoiceStatus.Expired || invoiceEvent.Invoice.Status == InvoiceStatus.Invalid
};
default: default:
return null; return null;
} }

View file

@ -553,12 +553,14 @@
"enabled": { "enabled": {
"type": "boolean", "type": "boolean",
"description": "Whether this webhook is enabled or not", "description": "Whether this webhook is enabled or not",
"nullable": false "nullable": false,
"default": true
}, },
"automaticRedelivery": { "automaticRedelivery": {
"type": "boolean", "type": "boolean",
"description": "If true, BTCPay Server will retry to redeliver any failed delivery after 10 seconds, 1 minutes and up to 6 times after 10 minutes.", "description": "If true, BTCPay Server will retry to redeliver any failed delivery after 10 seconds, 1 minutes and up to 6 times after 10 minutes.",
"nullable": false "nullable": false,
"default": true
}, },
"url": { "url": {
"type": "string", "type": "string",
@ -572,7 +574,8 @@
"everything": { "everything": {
"type": "string", "type": "string",
"description": "If true, the endpoint will receive all events related to the store.", "description": "If true, the endpoint will receive all events related to the store.",
"nullable": false "nullable": false,
"default": true
}, },
"specificEvents": { "specificEvents": {
"type": "string", "type": "string",