diff --git a/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj b/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj index 24011917a..5c1226f22 100644 --- a/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj +++ b/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj @@ -30,4 +30,10 @@ + + + + + + diff --git a/BTCPayServer.Abstractions/Constants/AuthenticationSchemes.cs b/BTCPayServer.Abstractions/Constants/AuthenticationSchemes.cs index 652d06997..007acc6a8 100644 --- a/BTCPayServer.Abstractions/Constants/AuthenticationSchemes.cs +++ b/BTCPayServer.Abstractions/Constants/AuthenticationSchemes.cs @@ -1,4 +1,4 @@ -namespace BTCPayServer.Security +namespace BTCPayServer.Abstractions.Constants { public class AuthenticationSchemes { diff --git a/BTCPayServer.Abstractions/Contracts/BaseDbContextFactory.cs b/BTCPayServer.Abstractions/Contracts/BaseDbContextFactory.cs new file mode 100644 index 000000000..0997c9120 --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/BaseDbContextFactory.cs @@ -0,0 +1,108 @@ +using System; +using BTCPayServer.Abstractions.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; + +namespace BTCPayServer.Abstractions.Contracts +{ + public abstract class BaseDbContextFactory where T: DbContext + { + private readonly DatabaseOptions _options; + private readonly string _schemaPrefix; + + public BaseDbContextFactory(DatabaseOptions options, string schemaPrefix) + { + _options = options; + _schemaPrefix = schemaPrefix; + } + + public abstract T CreateContext(); + + class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator + { + public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies, IMigrationsAnnotationProvider annotations, Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal.INpgsqlOptions opts) : base(dependencies, annotations, opts) + { + } + + protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder) + { + builder + .Append("CREATE DATABASE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)); + + // POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale + builder + .Append(" TEMPLATE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0")); + + builder + .Append(" LC_CTYPE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C")); + + builder + .Append(" LC_COLLATE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C")); + + builder + .Append(" ENCODING ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8")); + + if (operation.Tablespace != null) + { + builder + .Append(" TABLESPACE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace)); + } + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + EndStatement(builder, suppressTransaction: true); + } + } + + public void ConfigureBuilder(DbContextOptionsBuilder builder) + { + switch (_options.DatabaseType) + { + case DatabaseType.Sqlite: + builder.UseSqlite(_options.ConnectionString, o => + { + if (!string.IsNullOrEmpty(_schemaPrefix)) + { + o.MigrationsHistoryTable(_schemaPrefix); + } + }); + break; + case DatabaseType.Postgres: + builder + .UseNpgsql(_options.ConnectionString, o => + { + o.EnableRetryOnFailure(10); + if (!string.IsNullOrEmpty(_schemaPrefix)) + { + o.MigrationsHistoryTable(_schemaPrefix); + } + }) + .ReplaceService(); + break; + case DatabaseType.MySQL: + builder.UseMySql(_options.ConnectionString, o => + { + o.EnableRetryOnFailure(10); + + if (!string.IsNullOrEmpty(_schemaPrefix)) + { + o.MigrationsHistoryTable(_schemaPrefix); + } + }); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + } +} diff --git a/BTCPayServer.Abstractions/Contracts/IBTCPayServerPlugin.cs b/BTCPayServer.Abstractions/Contracts/IBTCPayServerPlugin.cs index a82103ac9..b59b9cd18 100644 --- a/BTCPayServer.Abstractions/Contracts/IBTCPayServerPlugin.cs +++ b/BTCPayServer.Abstractions/Contracts/IBTCPayServerPlugin.cs @@ -4,7 +4,7 @@ using BTCPayServer.Abstractions.Converters; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -namespace BTCPayServer.Contracts +namespace BTCPayServer.Abstractions.Contracts { public interface IBTCPayServerPlugin { diff --git a/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs b/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs index 999d54085..b756feb9b 100644 --- a/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs +++ b/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs @@ -1,6 +1,6 @@ using System; -namespace BTCPayServer.Contracts +namespace BTCPayServer.Abstractions.Contracts { public abstract class BaseNotification { diff --git a/BTCPayServer.Abstractions/Contracts/IPluginHookAction.cs b/BTCPayServer.Abstractions/Contracts/IPluginHookAction.cs new file mode 100644 index 000000000..2307b30aa --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/IPluginHookAction.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace BTCPayServer.Abstractions.Contracts +{ + public interface IPluginHookAction + { + public string Hook { get; } + Task Execute(object args); + } +} diff --git a/BTCPayServer.Abstractions/Contracts/IPluginHookFilter.cs b/BTCPayServer.Abstractions/Contracts/IPluginHookFilter.cs new file mode 100644 index 000000000..6e319783e --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/IPluginHookFilter.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace BTCPayServer.Abstractions.Contracts +{ + public interface IPluginHookFilter + { + public string Hook { get; } + + Task Execute(object args); + } +} diff --git a/BTCPayServer.Abstractions/Contracts/IPluginHookService.cs b/BTCPayServer.Abstractions/Contracts/IPluginHookService.cs new file mode 100644 index 000000000..2c440b0de --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/IPluginHookService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace BTCPayServer.Abstractions.Contracts +{ + public interface IPluginHookService + { + Task ApplyAction(string hook, object args); + Task ApplyFilter(string hook, object args); + } +} diff --git a/BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs b/BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs index add724d6d..182443a3b 100644 --- a/BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs +++ b/BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BTCPayServer.Services +namespace BTCPayServer.Abstractions.Contracts { public interface ISettingsRepository { diff --git a/BTCPayServer.Abstractions/Contracts/IStartupTask.cs b/BTCPayServer.Abstractions/Contracts/IStartupTask.cs index 69ec7b505..bfa873bd2 100644 --- a/BTCPayServer.Abstractions/Contracts/IStartupTask.cs +++ b/BTCPayServer.Abstractions/Contracts/IStartupTask.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BTCPayServer.Hosting +namespace BTCPayServer.Abstractions.Contracts { public interface IStartupTask { diff --git a/BTCPayServer.Abstractions/Contracts/ISyncSummaryProvider.cs b/BTCPayServer.Abstractions/Contracts/ISyncSummaryProvider.cs index 9843da32e..5f1171f32 100644 --- a/BTCPayServer.Abstractions/Contracts/ISyncSummaryProvider.cs +++ b/BTCPayServer.Abstractions/Contracts/ISyncSummaryProvider.cs @@ -1,4 +1,4 @@ -namespace BTCPayServer.Contracts +namespace BTCPayServer.Abstractions.Contracts { public interface ISyncSummaryProvider { diff --git a/BTCPayServer.Abstractions/Contracts/IUIExtension.cs b/BTCPayServer.Abstractions/Contracts/IUIExtension.cs new file mode 100644 index 000000000..1539d8d53 --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/IUIExtension.cs @@ -0,0 +1,9 @@ +namespace BTCPayServer.Abstractions.Contracts +{ + public interface IUIExtension + { + string Partial { get; } + + string Location { get; } + } +} diff --git a/BTCPayServer.Abstractions/Extensions/Extensions.cs b/BTCPayServer.Abstractions/Extensions/Extensions.cs index 98c3f5973..d8c762fb1 100644 --- a/BTCPayServer.Abstractions/Extensions/Extensions.cs +++ b/BTCPayServer.Abstractions/Extensions/Extensions.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using BTCPayServer.Models; +using BTCPayServer.Abstractions.Models; using Microsoft.AspNetCore.Mvc.ViewFeatures; -namespace BTCPayServer +namespace BTCPayServer.Abstractions.Extensions { public static class SetStatusMessageModelExtensions { diff --git a/BTCPayServer.Abstractions/Extensions/ServiceCollectionExtensions.cs b/BTCPayServer.Abstractions/Extensions/ServiceCollectionExtensions.cs index 3a21f649f..5b505b766 100644 --- a/BTCPayServer.Abstractions/Extensions/ServiceCollectionExtensions.cs +++ b/BTCPayServer.Abstractions/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ -using BTCPayServer.Hosting; +using BTCPayServer.Abstractions.Contracts; +using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.Extensions.DependencyInjection +namespace BTCPayServer.Abstractions.Extensions { public static class ServiceCollectionExtensions { diff --git a/BTCPayServer.Abstractions/Models/BaseBTCPayServerPlugin.cs b/BTCPayServer.Abstractions/Models/BaseBTCPayServerPlugin.cs index 29a86f3d8..d5c42b39c 100644 --- a/BTCPayServer.Abstractions/Models/BaseBTCPayServerPlugin.cs +++ b/BTCPayServer.Abstractions/Models/BaseBTCPayServerPlugin.cs @@ -1,10 +1,10 @@ using System; using System.Reflection; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -namespace BTCPayServer.Models +namespace BTCPayServer.Abstractions.Models { public abstract class BaseBTCPayServerPlugin : IBTCPayServerPlugin { diff --git a/BTCPayServer.Abstractions/Models/DatabaseOptions.cs b/BTCPayServer.Abstractions/Models/DatabaseOptions.cs new file mode 100644 index 000000000..2c518aaa9 --- /dev/null +++ b/BTCPayServer.Abstractions/Models/DatabaseOptions.cs @@ -0,0 +1,14 @@ +namespace BTCPayServer.Abstractions.Models +{ + public class DatabaseOptions + { + public DatabaseOptions(DatabaseType type, string connString) + { + DatabaseType = type; + ConnectionString = connString; + } + + public DatabaseType DatabaseType { get; set; } + public string ConnectionString { get; set; } + } +} diff --git a/BTCPayServer.Abstractions/Models/DatabaseType.cs b/BTCPayServer.Abstractions/Models/DatabaseType.cs new file mode 100644 index 000000000..09347b2e1 --- /dev/null +++ b/BTCPayServer.Abstractions/Models/DatabaseType.cs @@ -0,0 +1,9 @@ +namespace BTCPayServer.Abstractions.Models +{ + public enum DatabaseType + { + Sqlite, + Postgres, + MySQL, + } +} diff --git a/BTCPayServer.Abstractions/Models/StatusMessageModel.cs b/BTCPayServer.Abstractions/Models/StatusMessageModel.cs index 4b4f68543..2543cd400 100644 --- a/BTCPayServer.Abstractions/Models/StatusMessageModel.cs +++ b/BTCPayServer.Abstractions/Models/StatusMessageModel.cs @@ -1,6 +1,6 @@ using System; -namespace BTCPayServer.Models +namespace BTCPayServer.Abstractions.Models { public class StatusMessageModel { diff --git a/BTCPayServer.Abstractions/Services/PluginAction.cs b/BTCPayServer.Abstractions/Services/PluginAction.cs new file mode 100644 index 000000000..35aaff142 --- /dev/null +++ b/BTCPayServer.Abstractions/Services/PluginAction.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; + +namespace BTCPayServer.Abstractions.Services +{ + public abstract class PluginAction:IPluginHookAction + { + public string Hook { get; } + public Task Execute(object args) + { + return Execute(args is T args1 ? args1 : default); + } + + public abstract Task Execute(T arg); + } +} diff --git a/BTCPayServer.Abstractions/Services/PluginHookFilter.cs b/BTCPayServer.Abstractions/Services/PluginHookFilter.cs new file mode 100644 index 000000000..02751b980 --- /dev/null +++ b/BTCPayServer.Abstractions/Services/PluginHookFilter.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; + +namespace BTCPayServer.Abstractions.Services +{ + public abstract class PluginHookFilter:IPluginHookFilter + { + public string Hook { get; } + public Task Execute(object args) + { + return Execute(args is T args1 ? args1 : default).ContinueWith(task => task.Result as object); + } + + public abstract Task Execute(T arg); + } +} diff --git a/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs b/BTCPayServer.Abstractions/Services/UIExtension.cs similarity index 64% rename from BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs rename to BTCPayServer.Abstractions/Services/UIExtension.cs index 4bf60b3c2..512f698bb 100644 --- a/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs +++ b/BTCPayServer.Abstractions/Services/UIExtension.cs @@ -1,12 +1,7 @@ -namespace BTCPayServer.Contracts -{ - public interface IUIExtension - { - string Partial { get; } - - string Location { get; } - } +using BTCPayServer.Abstractions.Contracts; +namespace BTCPayServer.Abstractions.Services +{ public class UIExtension: IUIExtension { public UIExtension(string partial, string location) diff --git a/BTCPayServer.Client/BTCPayServerClient.Authorization.cs b/BTCPayServer.Client/BTCPayServerClient.Authorization.cs index c59775935..e5e5ab852 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Authorization.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Authorization.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace BTCPayServer.Client { diff --git a/BTCPayServer.Client/BTCPayServerClient.Webhooks.cs b/BTCPayServer.Client/BTCPayServerClient.Webhooks.cs new file mode 100644 index 000000000..a34309fd7 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.Webhooks.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public async Task CreateWebhook(string storeId, Client.Models.CreateStoreWebhookRequest create, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks", bodyPayload: create, method: HttpMethod.Post), token); + return await HandleResponse(response); + } + public async Task GetWebhook(string storeId, string webhookId, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}"), token); + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + return null; + return await HandleResponse(response); + } + public async Task UpdateWebhook(string storeId, string webhookId, Models.UpdateStoreWebhookRequest update, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}", bodyPayload: update, method: HttpMethod.Put), token); + return await HandleResponse(response); + } + public async Task DeleteWebhook(string storeId, string webhookId, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}", method: HttpMethod.Delete), token); + return response.IsSuccessStatusCode; + } + public async Task GetWebhooks(string storeId, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks"), token); + return await HandleResponse(response); + } + public async Task GetWebhookDeliveries(string storeId, string webhookId, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries"), token); + return await HandleResponse(response); + } + public async Task GetWebhookDelivery(string storeId, string webhookId, string deliveryId, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}"), token); + return await HandleResponse(response); + } + public async Task RedeliverWebhook(string storeId, string webhookId, string deliveryId, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver", null, HttpMethod.Post), token); + return await HandleResponse(response); + } + + public async Task GetWebhookDeliveryRequest(string storeId, string webhookId, string deliveryId, CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request"), token); + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + return null; + return await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index d410fdf5c..651a7dd89 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -65,7 +65,8 @@ namespace BTCPayServer.Client protected async Task HandleResponse(HttpResponseMessage message) { await HandleResponse(message); - return JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); + var str = await message.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(str); } protected virtual HttpRequestMessage CreateHttpRequest(string path, diff --git a/BTCPayServer.Client/Models/InvoiceStatus.cs b/BTCPayServer.Client/Models/InvoiceStatus.cs index a3d866acd..b30e4e73a 100644 --- a/BTCPayServer.Client/Models/InvoiceStatus.cs +++ b/BTCPayServer.Client/Models/InvoiceStatus.cs @@ -9,4 +9,4 @@ namespace BTCPayServer.Client.Models Complete, Confirmed } -} \ No newline at end of file +} diff --git a/BTCPayServer.Client/Models/StoreBaseData.cs b/BTCPayServer.Client/Models/StoreBaseData.cs index 61e8ed747..824756053 100644 --- a/BTCPayServer.Client/Models/StoreBaseData.cs +++ b/BTCPayServer.Client/Models/StoreBaseData.cs @@ -30,14 +30,21 @@ namespace BTCPayServer.Client.Models public double PaymentTolerance { get; set; } = 0; public bool AnyoneCanCreateInvoice { get; set; } + + public bool RequiresRefundEmail { get; set; } + public bool LightningAmountInSatoshi { get; set; } + public bool LightningPrivateRouteHints { get; set; } + public bool OnChainWithLnInvoiceFallback { get; set; } + public bool RedirectAutomatically { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public bool ShowRecommendedFee { get; set; } = true; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int RecommendedFeeBlockTarget { get; set; } = 1; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string DefaultLang { get; set; } = "en"; - public bool LightningAmountInSatoshi { get; set; } public string CustomLogo { get; set; } @@ -45,16 +52,13 @@ namespace BTCPayServer.Client.Models public string HtmlTitle { get; set; } - public bool RedirectAutomatically { get; set; } - public bool RequiresRefundEmail { get; set; } [JsonConverter(typeof(StringEnumConverter))] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never; public bool PayJoinEnabled { get; set; } - public bool LightningPrivateRouteHints { get; set; } [JsonExtensionData] diff --git a/BTCPayServer.Client/Models/StoreWebhookData.cs b/BTCPayServer.Client/Models/StoreWebhookData.cs new file mode 100644 index 000000000..497bd8fd3 --- /dev/null +++ b/BTCPayServer.Client/Models/StoreWebhookData.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models +{ + public class StoreWebhookBaseData + { + public class AuthorizedEventsData + { + public bool Everything { get; set; } = true; + + [JsonProperty(ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public WebhookEventType[] SpecificEvents { get; set; } = Array.Empty(); + } + + public bool Enabled { get; set; } = true; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Secret { get; set; } + public bool AutomaticRedelivery { get; set; } = true; + public string Url { get; set; } + public AuthorizedEventsData AuthorizedEvents { get; set; } = new AuthorizedEventsData(); + } + public class UpdateStoreWebhookRequest : StoreWebhookBaseData + { + } + public class CreateStoreWebhookRequest : StoreWebhookBaseData + { + } + public class StoreWebhookData : StoreWebhookBaseData + { + public string Id { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/WebhookDeliveryData.cs b/BTCPayServer.Client/Models/WebhookDeliveryData.cs new file mode 100644 index 000000000..0c3626253 --- /dev/null +++ b/BTCPayServer.Client/Models/WebhookDeliveryData.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models +{ + public class WebhookDeliveryData + { + public string Id { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset Timestamp { get; set; } + public int? HttpCode { get; set; } + public string ErrorMessage { get; set; } + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public WebhookDeliveryStatus Status { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/WebhookDeliveryStatus.cs b/BTCPayServer.Client/Models/WebhookDeliveryStatus.cs new file mode 100644 index 000000000..dde4b52d4 --- /dev/null +++ b/BTCPayServer.Client/Models/WebhookDeliveryStatus.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BTCPayServer.Client.Models +{ + public enum WebhookDeliveryStatus + { + Failed, + HttpError, + HttpSuccess + } +} diff --git a/BTCPayServer.Client/Models/WebhookEvent.cs b/BTCPayServer.Client/Models/WebhookEvent.cs new file mode 100644 index 000000000..b117919f8 --- /dev/null +++ b/BTCPayServer.Client/Models/WebhookEvent.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Client.Models +{ + 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 WebhookId { get; set; } + public string OrignalDeliveryId { get; set; } + public bool IsRedelivery { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + public WebhookEventType Type { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset Timestamp { get; set; } + [JsonExtensionData] + public IDictionary AdditionalData { get; set; } + public T ReadAs() + { + var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings); + return JsonConvert.DeserializeObject(str, DefaultSerializerSettings); + } + } +} diff --git a/BTCPayServer.Client/Models/WebhookEventType.cs b/BTCPayServer.Client/Models/WebhookEventType.cs new file mode 100644 index 000000000..a0644bf80 --- /dev/null +++ b/BTCPayServer.Client/Models/WebhookEventType.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BTCPayServer.Client.Models +{ + public enum WebhookEventType + { + InvoiceCreated, + InvoiceReceivedPayment, + InvoicePaidInFull, + InvoiceExpired, + InvoiceConfirmed, + InvoiceInvalid + } +} diff --git a/BTCPayServer.Client/Models/WebhookInvoiceEvent.cs b/BTCPayServer.Client/Models/WebhookInvoiceEvent.cs new file mode 100644 index 000000000..027caa056 --- /dev/null +++ b/BTCPayServer.Client/Models/WebhookInvoiceEvent.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BTCPayServer.Client.Models +{ + public class WebhookInvoiceEvent : WebhookEvent + { + public WebhookInvoiceEvent() + { + + } + public WebhookInvoiceEvent(WebhookEventType evtType) + { + this.Type = evtType; + } + [JsonProperty(Order = 1)] + public string StoreId { get; set; } + [JsonProperty(Order = 2)] + public string InvoiceId { get; set; } + } + + 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; } + } +} diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index 9d939cea3..245325723 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -12,6 +12,7 @@ namespace BTCPayServer.Client public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode"; public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings"; public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings"; + public const string CanModifyStoreWebhooks = "btcpay.store.webhooks.canmodifywebhooks"; public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:"; public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings"; public const string CanViewInvoices = "btcpay.store.canviewinvoices"; @@ -29,6 +30,7 @@ namespace BTCPayServer.Client { yield return CanViewInvoices; yield return CanCreateInvoice; + yield return CanModifyStoreWebhooks; yield return CanModifyServerSettings; yield return CanModifyStoreSettings; yield return CanViewStoreSettings; @@ -156,6 +158,7 @@ namespace BTCPayServer.Client switch (subpolicy) { case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings: + case Policies.CanModifyStoreWebhooks when this.Policy == Policies.CanModifyStoreSettings: case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings: case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings: case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings: diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index f44139247..479d9db9d 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -7,12 +7,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + diff --git a/BTCPayServer.Data/Data/ApplicationDbContext.cs b/BTCPayServer.Data/Data/ApplicationDbContext.cs index 0e18da7de..fe857548a 100644 --- a/BTCPayServer.Data/Data/ApplicationDbContext.cs +++ b/BTCPayServer.Data/Data/ApplicationDbContext.cs @@ -63,6 +63,11 @@ namespace BTCPayServer.Data public DbSet U2FDevices { get; set; } public DbSet Notifications { get; set; } + public DbSet StoreWebhooks { get; set; } + public DbSet Webhooks { get; set; } + public DbSet WebhookDeliveries { get; set; } + public DbSet InvoiceWebhookDeliveries { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var isConfigured = optionsBuilder.Options.Extensions.OfType().Any(); @@ -73,6 +78,7 @@ namespace BTCPayServer.Data protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + Data.UserStore.OnModelCreating(builder); NotificationData.OnModelCreating(builder); InvoiceData.OnModelCreating(builder); PaymentData.OnModelCreating(builder); @@ -91,7 +97,11 @@ namespace BTCPayServer.Data PayoutData.OnModelCreating(builder); RefundData.OnModelCreating(builder); U2FDevice.OnModelCreating(builder); - + + Data.WebhookDeliveryData.OnModelCreating(builder); + Data.StoreWebhookData.OnModelCreating(builder); + Data.InvoiceWebhookDeliveryData.OnModelCreating(builder); + if (Database.IsSqlite() && !_designTime) { // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations diff --git a/BTCPayServer.Data/Data/ApplicationDbContextFactory.cs b/BTCPayServer.Data/Data/ApplicationDbContextFactory.cs index cdbcfe3ef..9aefd3e64 100644 --- a/BTCPayServer.Data/Data/ApplicationDbContextFactory.cs +++ b/BTCPayServer.Data/Data/ApplicationDbContextFactory.cs @@ -1,96 +1,20 @@ -using System; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; namespace BTCPayServer.Data { - public enum DatabaseType + public class ApplicationDbContextFactory : BaseDbContextFactory { - Sqlite, - Postgres, - MySQL, - } - public class ApplicationDbContextFactory - { - readonly string _ConnectionString; - readonly DatabaseType _Type; - public ApplicationDbContextFactory(DatabaseType type, string connectionString) + public ApplicationDbContextFactory(DatabaseOptions options) : base(options, "") { - _ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); - _Type = type; } - - public DatabaseType Type - { - get - { - return _Type; - } - } - - public ApplicationDbContext CreateContext() + public override ApplicationDbContext CreateContext() { var builder = new DbContextOptionsBuilder(); ConfigureBuilder(builder); return new ApplicationDbContext(builder.Options); } - - class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator - { - public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies, IMigrationsAnnotationProvider annotations, Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal.INpgsqlOptions opts) : base(dependencies, annotations, opts) - { - } - - protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder) - { - builder - .Append("CREATE DATABASE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)); - - // POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale - builder - .Append(" TEMPLATE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0")); - - builder - .Append(" LC_CTYPE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C")); - - builder - .Append(" LC_COLLATE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C")); - - builder - .Append(" ENCODING ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8")); - - if (operation.Tablespace != null) - { - builder - .Append(" TABLESPACE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace)); - } - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - EndStatement(builder, suppressTransaction: true); - } - } - - public void ConfigureBuilder(DbContextOptionsBuilder builder) - { - if (_Type == DatabaseType.Sqlite) - builder.UseSqlite(_ConnectionString, o => o.MigrationsAssembly("BTCPayServer.Data")); - else if (_Type == DatabaseType.Postgres) - builder - .UseNpgsql(_ConnectionString, o => o.MigrationsAssembly("BTCPayServer.Data").EnableRetryOnFailure(10)) - .ReplaceService(); - else if (_Type == DatabaseType.MySQL) - builder.UseMySql(_ConnectionString, o => o.MigrationsAssembly("BTCPayServer.Data").EnableRetryOnFailure(10)); - } } } diff --git a/BTCPayServer.Data/Data/InvoiceWebhookDeliveryData.cs b/BTCPayServer.Data/Data/InvoiceWebhookDeliveryData.cs new file mode 100644 index 000000000..186f02148 --- /dev/null +++ b/BTCPayServer.Data/Data/InvoiceWebhookDeliveryData.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; + +namespace BTCPayServer.Data +{ + public class InvoiceWebhookDeliveryData + { + public string InvoiceId { get; set; } + public InvoiceData Invoice { get; set; } + public string DeliveryId { get; set; } + public WebhookDeliveryData Delivery { get; set; } + internal static void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasKey(p => new { p.InvoiceId, p.DeliveryId }); + builder.Entity() + .HasOne(o => o.Invoice) + .WithOne().OnDelete(DeleteBehavior.Cascade); + builder.Entity() + .HasOne(o => o.Delivery) + .WithOne().OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/BTCPayServer.Data/Data/StoreWebhookData.cs b/BTCPayServer.Data/Data/StoreWebhookData.cs new file mode 100644 index 000000000..02ebbe2af --- /dev/null +++ b/BTCPayServer.Data/Data/StoreWebhookData.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; +using System.Linq; + +namespace BTCPayServer.Data +{ + public class StoreWebhookData + { + public string StoreId { get; set; } + public string WebhookId { get; set; } + public WebhookData Webhook { get; set; } + public StoreData Store { get; set; } + + internal static void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasKey(p => new { p.StoreId, p.WebhookId }); + + builder.Entity() + .HasOne(o => o.Webhook) + .WithOne().OnDelete(DeleteBehavior.Cascade); + + builder.Entity() + .HasOne(o => o.Store) + .WithOne().OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/BTCPayServer.Data/Data/WebhookData.cs b/BTCPayServer.Data/Data/WebhookData.cs new file mode 100644 index 000000000..ca5b7fd36 --- /dev/null +++ b/BTCPayServer.Data/Data/WebhookData.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; +using Microsoft.EntityFrameworkCore; + +namespace BTCPayServer.Data +{ + public class WebhookData + { + [Key] + [MaxLength(25)] + public string Id + { + get; + set; + } + [Required] + public byte[] Blob { get; set; } + public List Deliveries { get; set; } + } +} diff --git a/BTCPayServer.Data/Data/WebhookDeliveryData.cs b/BTCPayServer.Data/Data/WebhookDeliveryData.cs new file mode 100644 index 000000000..3d74c3763 --- /dev/null +++ b/BTCPayServer.Data/Data/WebhookDeliveryData.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; +using Microsoft.EntityFrameworkCore; + +namespace BTCPayServer.Data +{ + public class WebhookDeliveryData + { + [Key] + [MaxLength(25)] + public string Id { get; set; } + [MaxLength(25)] + [Required] + public string WebhookId { get; set; } + public WebhookData Webhook { get; set; } + + [Required] + public DateTimeOffset Timestamp + { + get; set; + } + + [Required] + public byte[] Blob { get; set; } + internal static void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(o => o.Webhook) + .WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade); + builder.Entity().HasIndex(o => o.WebhookId); + } + } +} diff --git a/BTCPayServer.Data/Migrations/20201108054749_webhooks.cs b/BTCPayServer.Data/Migrations/20201108054749_webhooks.cs new file mode 100644 index 000000000..fcf1d6739 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20201108054749_webhooks.cs @@ -0,0 +1,115 @@ +using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20201108054749_webhooks")] + public partial class webhooks : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Webhooks", + columns: table => new + { + Id = table.Column(maxLength: 25, nullable: false), + Blob = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Webhooks", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "StoreWebhooks", + columns: table => new + { + StoreId = table.Column(nullable: false), + WebhookId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StoreWebhooks", x => new { x.StoreId, x.WebhookId }); + table.ForeignKey( + name: "FK_StoreWebhooks_Stores_StoreId", + column: x => x.StoreId, + principalTable: "Stores", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_StoreWebhooks_Webhooks_WebhookId", + column: x => x.WebhookId, + principalTable: "Webhooks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "WebhookDeliveries", + columns: table => new + { + Id = table.Column(maxLength: 25, nullable: false), + WebhookId = table.Column(maxLength: 25, nullable: false), + Timestamp = table.Column(nullable: false), + Blob = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WebhookDeliveries", x => x.Id); + table.ForeignKey( + name: "FK_WebhookDeliveries_Webhooks_WebhookId", + column: x => x.WebhookId, + principalTable: "Webhooks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InvoiceWebhookDeliveries", + columns: table => new + { + InvoiceId = table.Column(nullable: false), + DeliveryId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InvoiceWebhookDeliveries", x => new { x.InvoiceId, x.DeliveryId }); + table.ForeignKey( + name: "FK_InvoiceWebhookDeliveries_WebhookDeliveries_DeliveryId", + column: x => x.DeliveryId, + principalTable: "WebhookDeliveries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InvoiceWebhookDeliveries_Invoices_InvoiceId", + column: x => x.InvoiceId, + principalTable: "Invoices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_WebhookDeliveries_WebhookId", + table: "WebhookDeliveries", + column: "WebhookId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InvoiceWebhookDeliveries"); + + migrationBuilder.DropTable( + name: "StoreWebhooks"); + + migrationBuilder.DropTable( + name: "WebhookDeliveries"); + + migrationBuilder.DropTable( + name: "Webhooks"); + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index db897fb04..ff64ecef3 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -257,6 +257,25 @@ namespace BTCPayServer.Migrations b.ToTable("InvoiceEvents"); }); + modelBuilder.Entity("BTCPayServer.Data.InvoiceWebhookDeliveryData", b => + { + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("DeliveryId") + .HasColumnType("TEXT"); + + b.HasKey("InvoiceId", "DeliveryId"); + + b.HasIndex("DeliveryId") + .IsUnique(); + + b.HasIndex("InvoiceId") + .IsUnique(); + + b.ToTable("InvoiceWebhookDeliveries"); + }); + modelBuilder.Entity("BTCPayServer.Data.NotificationData", b => { b.Property("Id") @@ -588,6 +607,25 @@ namespace BTCPayServer.Migrations b.ToTable("Stores"); }); + modelBuilder.Entity("BTCPayServer.Data.StoreWebhookData", b => + { + b.Property("StoreId") + .HasColumnType("TEXT"); + + b.Property("WebhookId") + .HasColumnType("TEXT"); + + b.HasKey("StoreId", "WebhookId"); + + b.HasIndex("StoreId") + .IsUnique(); + + b.HasIndex("WebhookId") + .IsUnique(); + + b.ToTable("StoreWebhooks"); + }); + modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => { b.Property("Id") @@ -696,6 +734,46 @@ namespace BTCPayServer.Migrations b.ToTable("WalletTransactions"); }); + modelBuilder.Entity("BTCPayServer.Data.WebhookData", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(25); + + b.Property("Blob") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("Webhooks"); + }); + + modelBuilder.Entity("BTCPayServer.Data.WebhookDeliveryData", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(25); + + b.Property("Blob") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("WebhookId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(25); + + b.HasKey("Id"); + + b.HasIndex("WebhookId"); + + b.ToTable("WebhookDeliveries"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") @@ -883,6 +961,21 @@ namespace BTCPayServer.Migrations .IsRequired(); }); + modelBuilder.Entity("BTCPayServer.Data.InvoiceWebhookDeliveryData", b => + { + b.HasOne("BTCPayServer.Data.WebhookDeliveryData", "Delivery") + .WithOne() + .HasForeignKey("BTCPayServer.Data.InvoiceWebhookDeliveryData", "DeliveryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BTCPayServer.Data.InvoiceData", "Invoice") + .WithOne() + .HasForeignKey("BTCPayServer.Data.InvoiceWebhookDeliveryData", "InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("BTCPayServer.Data.NotificationData", b => { b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") @@ -956,6 +1049,21 @@ namespace BTCPayServer.Migrations .IsRequired(); }); + modelBuilder.Entity("BTCPayServer.Data.StoreWebhookData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "Store") + .WithOne() + .HasForeignKey("BTCPayServer.Data.StoreWebhookData", "StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BTCPayServer.Data.WebhookData", "Webhook") + .WithOne() + .HasForeignKey("BTCPayServer.Data.StoreWebhookData", "WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => { b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") @@ -995,6 +1103,15 @@ namespace BTCPayServer.Migrations .IsRequired(); }); + modelBuilder.Entity("BTCPayServer.Data.WebhookDeliveryData", b => + { + b.HasOne("BTCPayServer.Data.WebhookData", "Webhook") + .WithMany("Deliveries") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) diff --git a/BTCPayServer.PluginPacker/Program.cs b/BTCPayServer.PluginPacker/Program.cs index 3d56af9b4..d09f9e67b 100644 --- a/BTCPayServer.PluginPacker/Program.cs +++ b/BTCPayServer.PluginPacker/Program.cs @@ -4,7 +4,7 @@ using System.IO.Compression; using System.Linq; using System.Reflection; using System.Text.Json; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; namespace BTCPayServer.PluginPacker { diff --git a/BTCPayServer.Plugins.Test/BTCPayServer.Plugins.Test.csproj b/BTCPayServer.Plugins.Test/BTCPayServer.Plugins.Test.csproj index 3a084657c..aa263c9a7 100644 --- a/BTCPayServer.Plugins.Test/BTCPayServer.Plugins.Test.csproj +++ b/BTCPayServer.Plugins.Test/BTCPayServer.Plugins.Test.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/BTCPayServer.Plugins.Test/Controllers/TestExtensionController.cs b/BTCPayServer.Plugins.Test/Controllers/TestExtensionController.cs new file mode 100644 index 000000000..da62a1439 --- /dev/null +++ b/BTCPayServer.Plugins.Test/Controllers/TestExtensionController.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using BTCPayServer.Plugins.Test.Data; +using BTCPayServer.Plugins.Test.Services; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Plugins.Test +{ + [Route("extensions/test")] + public class TestExtensionController : Controller + { + private readonly TestPluginService _testPluginService; + + public TestExtensionController(TestPluginService testPluginService) + { + _testPluginService = testPluginService; + } + + // GET + public async Task Index() + { + return View(new TestPluginPageViewModel() + { + Data = await _testPluginService.Get() + }); + } + + + } + + public class TestPluginPageViewModel + { + public List Data { get; set; } + } +} diff --git a/BTCPayServer.Plugins.Test/Data/TestPluginData.cs b/BTCPayServer.Plugins.Test/Data/TestPluginData.cs new file mode 100644 index 000000000..f60515d95 --- /dev/null +++ b/BTCPayServer.Plugins.Test/Data/TestPluginData.cs @@ -0,0 +1,14 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace BTCPayServer.Plugins.Test.Data +{ + public class TestPluginData + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + public DateTimeOffset Timestamp { get; set; } + + + } +} diff --git a/BTCPayServer.Plugins.Test/Data/TestPluginDbContext.cs b/BTCPayServer.Plugins.Test/Data/TestPluginDbContext.cs new file mode 100644 index 000000000..a8e51b1cc --- /dev/null +++ b/BTCPayServer.Plugins.Test/Data/TestPluginDbContext.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using BTCPayServer.Plugins.Test.Data; +using Microsoft.EntityFrameworkCore; + +namespace BTCPayServer.Plugins.Test +{ + public class TestPluginDbContext : DbContext + { + private readonly bool _designTime; + + public DbSet TestPluginRecords { get; set; } + + public TestPluginDbContext(DbContextOptions options, bool designTime = false) + : base(options) + { + _designTime = designTime; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.HasDefaultSchema("BTCPayServer.Plugins.Test"); + if (Database.IsSqlite() && !_designTime) + { + // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations + // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations + // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset + // use the DateTimeOffsetToBinaryConverter + // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 + // This only supports millisecond precision, but should be sufficient for most use cases. + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)); + foreach (var property in properties) + { + modelBuilder + .Entity(entityType.Name) + .Property(property.Name) + .HasConversion(new Microsoft.EntityFrameworkCore.Storage.ValueConversion.DateTimeOffsetToBinaryConverter()); + } + } + } + } + } +} diff --git a/BTCPayServer.Plugins.Test/Migrations/20201117154419_Init.cs b/BTCPayServer.Plugins.Test/Migrations/20201117154419_Init.cs new file mode 100644 index 000000000..84737650e --- /dev/null +++ b/BTCPayServer.Plugins.Test/Migrations/20201117154419_Init.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Plugins.Test.Migrations +{ + [DbContext(typeof(TestPluginDbContext))] + [Migration("20201117154419_Init")] + public partial class Init : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "BTCPayServer.Plugins.Test"); + + migrationBuilder.CreateTable( + name: "TestPluginRecords", + schema: "BTCPayServer.Plugins.Test", + columns: table => new + { + Id = table.Column(nullable: false), + Timestamp = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TestPluginRecords", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TestPluginRecords", + schema: "BTCPayServer.Plugins.Test"); + } + } +} diff --git a/BTCPayServer.Plugins.Test/Migrations/TestPluginDbContextModelSnapshot.cs b/BTCPayServer.Plugins.Test/Migrations/TestPluginDbContextModelSnapshot.cs new file mode 100644 index 000000000..d96cd7d03 --- /dev/null +++ b/BTCPayServer.Plugins.Test/Migrations/TestPluginDbContextModelSnapshot.cs @@ -0,0 +1,36 @@ +// +using System; +using BTCPayServer.Plugins.Test; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace BTCPayServer.Plugins.Test.Migrations +{ + [DbContext(typeof(TestPluginDbContext))] + partial class TestPluginDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("BTCPayServer.Plugins.Test") + .HasAnnotation("ProductVersion", "3.1.10"); + + modelBuilder.Entity("BTCPayServer.Plugins.Test.Data.TestPluginData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TestPluginRecords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer.Plugins.Test/ApplicationPartsLogger.cs b/BTCPayServer.Plugins.Test/Services/ApplicationPartsLogger.cs similarity index 100% rename from BTCPayServer.Plugins.Test/ApplicationPartsLogger.cs rename to BTCPayServer.Plugins.Test/Services/ApplicationPartsLogger.cs diff --git a/BTCPayServer.Plugins.Test/Services/TestPluginDbContextFactory.cs b/BTCPayServer.Plugins.Test/Services/TestPluginDbContextFactory.cs new file mode 100644 index 000000000..778faa6f3 --- /dev/null +++ b/BTCPayServer.Plugins.Test/Services/TestPluginDbContextFactory.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Plugins.Test.Migrations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace BTCPayServer.Plugins.Test +{ + + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public TestPluginDbContext CreateDbContext(string[] args) + { + + var builder = new DbContextOptionsBuilder(); + + builder.UseSqlite("Data Source=temp.db"); + + return new TestPluginDbContext(builder.Options, true); + } + } + + public class TestPluginDbContextFactory : BaseDbContextFactory + { + public TestPluginDbContextFactory(DatabaseOptions options) : base(options, "BTCPayServer.Plugins.Test") + { + } + + public override TestPluginDbContext CreateContext() + { + var builder = new DbContextOptionsBuilder(); + ConfigureBuilder(builder); + return new TestPluginDbContext(builder.Options); + + } + } +} diff --git a/BTCPayServer.Plugins.Test/Services/TestPluginService.cs b/BTCPayServer.Plugins.Test/Services/TestPluginService.cs new file mode 100644 index 000000000..f162ab826 --- /dev/null +++ b/BTCPayServer.Plugins.Test/Services/TestPluginService.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using BTCPayServer.Plugins.Test.Data; +using Microsoft.EntityFrameworkCore; + +namespace BTCPayServer.Plugins.Test.Services +{ + public class TestPluginService + { + private readonly TestPluginDbContextFactory _testPluginDbContextFactory; + + public TestPluginService(TestPluginDbContextFactory testPluginDbContextFactory) + { + _testPluginDbContextFactory = testPluginDbContextFactory; + } + + public async Task AddTestDataRecord() + { + await using var context = _testPluginDbContextFactory.CreateContext(); + + await context.TestPluginRecords.AddAsync(new TestPluginData() {Timestamp = DateTimeOffset.UtcNow}); + } + + + public async Task> Get() + { + await using var context = _testPluginDbContextFactory.CreateContext(); + + return await context.TestPluginRecords.ToListAsync(); + } + } +} diff --git a/BTCPayServer.Plugins.Test/TestExtension.cs b/BTCPayServer.Plugins.Test/TestExtension.cs index f28fcf402..2d6fe164d 100644 --- a/BTCPayServer.Plugins.Test/TestExtension.cs +++ b/BTCPayServer.Plugins.Test/TestExtension.cs @@ -1,5 +1,10 @@ -using BTCPayServer.Contracts; -using BTCPayServer.Models; +using System; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using BTCPayServer.Plugins.Test.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace BTCPayServer.Plugins.Test @@ -14,6 +19,21 @@ namespace BTCPayServer.Plugins.Test { services.AddSingleton(new UIExtension("TestExtensionNavExtension", "header-nav")); services.AddHostedService(); + services.AddSingleton(); + services.AddSingleton(); + services.AddDbContext((provider, o) => + { + var factory = provider.GetRequiredService(); + factory.ConfigureBuilder(o); + }); + } + + public override void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices) + { + base.Execute(applicationBuilder, applicationBuilderApplicationServices); + applicationBuilderApplicationServices.GetService().CreateContext().Database.Migrate(); + applicationBuilderApplicationServices.GetService().AddTestDataRecord().GetAwaiter().GetResult(); + } } } diff --git a/BTCPayServer.Plugins.Test/TestExtensionController.cs b/BTCPayServer.Plugins.Test/TestExtensionController.cs deleted file mode 100644 index a5bfcc166..000000000 --- a/BTCPayServer.Plugins.Test/TestExtensionController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace BTCPayServer.Plugins.Test -{ - [Route("extensions/test")] - public class TestExtensionController : Controller - { - // GET - public IActionResult Index() - { - return View(); - } - - - } -} diff --git a/BTCPayServer.Plugins.Test/Views/TestExtension/Index.cshtml b/BTCPayServer.Plugins.Test/Views/TestExtension/Index.cshtml index f87d76718..3c761d54a 100644 --- a/BTCPayServer.Plugins.Test/Views/TestExtension/Index.cshtml +++ b/BTCPayServer.Plugins.Test/Views/TestExtension/Index.cshtml @@ -1,3 +1,4 @@ +@model BTCPayServer.Plugins.Test.TestPluginPageViewModel

Challenge Completed!!

@@ -5,5 +6,16 @@ + +
+

Persisted Data

+

The following is data persisted to the configured database but in an isolated DbContext. Every time you start BTCPayw with this plugin enabled, a timestamp is logged.

+
    > + @foreach (var item in Model.Data) + { +
  • @item.Id at @item.Timestamp.ToString("F")
  • + } +
+
diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index a286f6243..99d5536c7 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -89,7 +89,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("AddApiKey")).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 - 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 storeId = option.GetAttribute("value"); option.Click(); diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index d6ab7faa4..ecf82d0fb 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -21,6 +21,7 @@ + diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index c0ea88d9a..2ceeca809 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -26,7 +26,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; -using AuthenticationSchemes = BTCPayServer.Security.AuthenticationSchemes; +using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes; namespace BTCPayServer.Tests { diff --git a/BTCPayServer.Tests/FakeServer.cs b/BTCPayServer.Tests/FakeServer.cs index 44062c07d..a9692a2d7 100644 --- a/BTCPayServer.Tests/FakeServer.cs +++ b/BTCPayServer.Tests/FakeServer.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using ExchangeSharp; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -62,9 +63,9 @@ namespace BTCPayServer.Tests semaphore.Dispose(); } - public async Task GetNextRequest() + public async Task GetNextRequest(CancellationToken cancellationToken = default) { - return await _channel.Reader.ReadAsync(); + return await _channel.Reader.ReadAsync(cancellationToken); } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 62f73fab6..696e16ccc 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -16,6 +16,7 @@ using BTCPayServer.Tests.Logging; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NBitcoin; +using NBitcoin.OpenAsset; using NBitpayClient; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -149,12 +150,12 @@ namespace BTCPayServer.Tests var user1 = await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" }); Assert.Empty(user1.Roles); - + // We have no admin, so it should work var user2 = await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }); Assert.Empty(user2.Roles); - + // Duplicate email await AssertValidationError(new[] { "Email" }, async () => await unauthClient.CreateUser( @@ -170,7 +171,7 @@ namespace BTCPayServer.Tests Assert.Contains("ServerAdmin", admin.Roles); Assert.NotNull(admin.Created); Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10); - + // Creating a new user without proper creds is now impossible (unauthorized) // Because if registration are locked and that an admin exists, we don't accept unauthenticated connection await AssertHttpError(401, @@ -611,6 +612,101 @@ namespace BTCPayServer.Tests } } + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task CanUseWebhooks() + { + void AssertHook(FakeServer fakeServer, Client.Models.StoreWebhookData hook) + { + Assert.True(hook.Enabled); + Assert.True(hook.AuthorizedEvents.Everything); + Assert.False(hook.AutomaticRedelivery); + Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url); + } + using var tester = ServerTester.Create(); + using var fakeServer = new FakeServer(); + await fakeServer.Start(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + var clientProfile = await user.CreateClient(Policies.CanModifyStoreWebhooks, Policies.CanCreateInvoice); + var hook = await clientProfile.CreateWebhook(user.StoreId, new CreateStoreWebhookRequest() + { + Url = fakeServer.ServerUri.AbsoluteUri, + AutomaticRedelivery = false + }); + Assert.NotNull(hook.Secret); + AssertHook(fakeServer, hook); + hook = await clientProfile.GetWebhook(user.StoreId, hook.Id); + AssertHook(fakeServer, hook); + var hooks = await clientProfile.GetWebhooks(user.StoreId); + hook = Assert.Single(hooks); + AssertHook(fakeServer, hook); + await clientProfile.CreateInvoice(user.StoreId, + new CreateInvoiceRequest() { Currency = "USD", Amount = 100 }); + var req = await fakeServer.GetNextRequest(); + req.Response.StatusCode = 200; + fakeServer.Done(); + hook = await clientProfile.UpdateWebhook(user.StoreId, hook.Id, new UpdateStoreWebhookRequest() + { + Url = hook.Url, + Secret = "lol", + AutomaticRedelivery = false + }); + Assert.Null(hook.Secret); + AssertHook(fakeServer, hook); + var deliveries = await clientProfile.GetWebhookDeliveries(user.StoreId, hook.Id); + var delivery = Assert.Single(deliveries); + delivery = await clientProfile.GetWebhookDelivery(user.StoreId, hook.Id, delivery.Id); + Assert.NotNull(delivery); + Assert.Equal(WebhookDeliveryStatus.HttpSuccess, delivery.Status); + + var newDeliveryId = await clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id); + req = await fakeServer.GetNextRequest(); + req.Response.StatusCode = 404; + fakeServer.Done(); + await TestUtils.EventuallyAsync(async () => + { + var newDelivery = await clientProfile.GetWebhookDelivery(user.StoreId, hook.Id, newDeliveryId); + Assert.NotNull(newDelivery); + Assert.Equal(404, newDelivery.HttpCode); + var req = await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); + Assert.Equal(delivery.Id, req.OrignalDeliveryId); + Assert.True(req.IsRedelivery); + Assert.Equal(WebhookDeliveryStatus.HttpError, newDelivery.Status); + }); + deliveries = await clientProfile.GetWebhookDeliveries(user.StoreId, hook.Id); + Assert.Equal(2, deliveries.Length); + Assert.Equal(newDeliveryId, deliveries[0].Id); + var jObj = await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); + Assert.NotNull(jObj); + + Logs.Tester.LogInformation("Should not be able to access webhook without proper auth"); + var unauthorized = await user.CreateClient(Policies.CanCreateInvoice); + await AssertHttpError(403, async () => + { + await unauthorized.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); + }); + + Logs.Tester.LogInformation("Can use btcpay.store.canmodifystoresettings to query webhooks"); + clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice); + await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); + + Logs.Tester.LogInformation("Testing corner cases"); + Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId)); + Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol")); + Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", "lol")); + Assert.Null(await clientProfile.GetWebhook(user.StoreId, "lol")); + await AssertHttpError(404, async () => + { + await clientProfile.UpdateWebhook(user.StoreId, "lol", new UpdateStoreWebhookRequest() { Url = hook.Url }); + }); + + Assert.True(await clientProfile.DeleteWebhook(user.StoreId, hook.Id)); + Assert.False(await clientProfile.DeleteWebhook(user.StoreId, hook.Id)); + } + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task HealthControllerTests() @@ -821,6 +917,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); await user.GrantAccessAsync(); await user.MakeAdmin(); + await user.SetupWebhook(); var client = await user.CreateClient(Policies.Unrestricted); var viewOnly = await user.CreateClient(Policies.CanViewInvoices); @@ -878,10 +975,43 @@ namespace BTCPayServer.Tests await client.UnarchiveInvoice(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(WebhookEventType.InvoiceConfirmed, + o => + { + Assert.Equal(inv.Id, o.InvoiceId); + Assert.True(o.ManuallyMarked); + }); + } + if (marked == InvoiceStatus.Invalid) + { + Assert.Equal(InvoiceStatus.Invalid, result.Status); + var evt = user.AssertHasWebhookEvent(WebhookEventType.InvoiceInvalid, + o => + { + Assert.Equal(inv.Id, o.InvoiceId); + Assert.True(o.ManuallyMarked); + }); + Assert.NotNull(await client.GetWebhookDelivery(evt.StoreId, evt.WebhookId, evt.DeliveryId)); + } + } } } - - [Fact(Timeout = 60 * 2 * 1000)] + + [Fact(Timeout = 60 * 2 * 1000)] [Trait("Integration", "Integration")] [Trait("Lightning", "Lightning")] public async Task CanUseLightningAPI() @@ -907,7 +1037,7 @@ namespace BTCPayServer.Tests var info = await client.GetLightningNodeInfo("BTC"); Assert.Single(info.NodeURIs); Assert.NotEqual(0, info.BlockHeight); - + var err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); Assert.Contains("503", err.Message); // Not permission for the store! diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index 41825f703..97040cdc0 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 12dd261ba..d9f87f405 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using BTCPayServer; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Lightning; using BTCPayServer.Lightning.CLightning; using BTCPayServer.Models; @@ -317,10 +318,9 @@ namespace BTCPayServer.Tests Driver.FindElement(By.Name("StoreId")).SendKeys(storeName); Driver.FindElement(By.Id("Create")).Click(); - Assert.True(Driver.PageSource.Contains("just created!"), "Unable to create Invoice"); + AssertHappyMessage(); var statusElement = Driver.FindElement(By.ClassName("alert-success")); var id = statusElement.Text.Split(" ")[1]; - return id; } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 7fb42e173..3f22bc605 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1,9 +1,12 @@ using System; using System.Globalization; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Models; using BTCPayServer.Services.Wallets; @@ -12,11 +15,18 @@ using BTCPayServer.Views.Server; using BTCPayServer.Views.Wallets; using Microsoft.EntityFrameworkCore; using NBitcoin; +using NBitcoin.DataEncoders; using NBitcoin.Payment; using NBitpayClient; +using Newtonsoft.Json.Linq; using OpenQA.Selenium; +using OpenQA.Selenium.Support.Extensions; +using OpenQA.Selenium.Support.UI; +using Org.BouncyCastle.Ocsp; +using Renci.SshNet.Security.Cryptography; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; namespace BTCPayServer.Tests { @@ -133,19 +143,20 @@ namespace BTCPayServer.Tests //let's test invite link s.Logout(); s.GoToRegister(); - var newAdminUser = s.RegisterNewUser(true); + var newAdminUser = s.RegisterNewUser(true); s.GoToServer(ServerNavPages.Users); s.Driver.FindElement(By.Id("CreateUser")).Click(); - + var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com"; s.Driver.FindElement(By.Id("Email")).SendKeys(usr); s.Driver.FindElement(By.Id("Save")).Click(); - var url = s.AssertHappyMessage().FindElement(By.TagName("a")).Text;; + var url = s.AssertHappyMessage().FindElement(By.TagName("a")).Text; + ; s.Logout(); s.Driver.Navigate().GoToUrl(url); - Assert.Equal("hidden",s.Driver.FindElement(By.Id("Email")).GetAttribute("type")); - Assert.Equal(usr,s.Driver.FindElement(By.Id("Email")).GetAttribute("value")); - + Assert.Equal("hidden", s.Driver.FindElement(By.Id("Email")).GetAttribute("type")); + Assert.Equal(usr, s.Driver.FindElement(By.Id("Email")).GetAttribute("value")); + s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456"); s.Driver.FindElement(By.Id("SetPassword")).Click(); @@ -301,11 +312,12 @@ namespace BTCPayServer.Tests await s.StartAsync(); var alice = s.RegisterNewUser(); var storeData = s.CreateNewStore(); + var onchainHint = "A store requires a wallet to receive payments. Set up your wallet."; + var offchainHint = "A connection to a Lightning node is required to receive Lightning payments."; + // verify that hints are displayed on the store page - Assert.True(s.Driver.PageSource.Contains("Wallet not setup for the store, please provide Derviation Scheme"), - "Wallet hint not present"); - Assert.True(s.Driver.PageSource.Contains("Review settings if you want to receive Lightning payments"), - "Lightning hint not present"); + Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not present"); + Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint not present"); s.GoToStores(); Assert.True(s.Driver.PageSource.Contains("warninghint_" + storeData.storeId), @@ -314,7 +326,7 @@ namespace BTCPayServer.Tests s.GoToStore(storeData.storeId); s.AddDerivationScheme(); // wallet hint should be dismissed s.Driver.AssertNoError(); - Assert.False(s.Driver.PageSource.Contains("Wallet not setup for the store, please provide Derviation Scheme"), + Assert.False(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not dismissed on derivation scheme add"); s.Driver.FindElement(By.Id("dismissLightningHint")).Click(); // dismiss lightning hint @@ -380,8 +392,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Stores")).Click(); // there shouldn't be any hints now - Assert.False(s.Driver.PageSource.Contains("Review settings if you want to receive Lightning payments"), - "Lightning hint should be dismissed at this point"); + Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point"); s.Driver.FindElement(By.LinkText("Remove")).Click(); s.Driver.FindElement(By.Id("continue")).Click(); @@ -595,6 +606,132 @@ namespace BTCPayServer.Tests } } + [Fact(Timeout = TestTimeout)] + public async Task CanUseWebhooks() + { + using (var s = SeleniumTester.Create()) + { + await s.StartAsync(); + s.RegisterNewUser(true); + var store = s.CreateNewStore(); + s.GoToStore(store.storeId, Views.Stores.StoreNavPages.Webhooks); + + Logs.Tester.LogInformation("Let's create two webhooks"); + for (int i = 0; i < 2; i++) + { + s.Driver.FindElement(By.Id("CreateWebhook")).Click(); + s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys($"http://127.0.0.1/callback{i}"); + new SelectElement(s.Driver.FindElement(By.Name("Everything"))) + .SelectByValue("false"); + s.Driver.FindElement(By.Id("InvoiceCreated")).Click(); + s.Driver.FindElement(By.Id("InvoicePaidInFull")).Click(); + s.Driver.FindElement(By.Name("add")).Click(); + } + + Logs.Tester.LogInformation("Let's delete one of them"); + var deletes = s.Driver.FindElements(By.LinkText("Delete")); + Assert.Equal(2, deletes.Count); + deletes[0].Click(); + s.Driver.FindElement(By.Id("continue")).Click(); + deletes = s.Driver.FindElements(By.LinkText("Delete")); + Assert.Single(deletes); + s.AssertHappyMessage(); + + Logs.Tester.LogInformation("Let's try to update one of them"); + s.Driver.FindElement(By.LinkText("Modify")).Click(); + + using FakeServer server = new FakeServer(); + await server.Start(); + s.Driver.FindElement(By.Name("PayloadUrl")).Clear(); + s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys(server.ServerUri.AbsoluteUri); + s.Driver.FindElement(By.Name("Secret")).Clear(); + s.Driver.FindElement(By.Name("Secret")).SendKeys("HelloWorld"); + s.Driver.FindElement(By.Name("update")).Click(); + s.AssertHappyMessage(); + s.Driver.FindElement(By.LinkText("Modify")).Click(); + foreach (var value in Enum.GetValues(typeof(WebhookEventType))) + { + // Here we make sure we did not forget an event type in the list + // However, maybe some event should not appear here because not at the store level. + // Fix as needed. + Assert.Contains($"value=\"{value}\"", s.Driver.PageSource); + } + // This one should be checked + Assert.Contains($"value=\"InvoicePaidInFull\" checked", s.Driver.PageSource); + Assert.Contains($"value=\"InvoiceCreated\" checked", s.Driver.PageSource); + // This one never been checked + Assert.DoesNotContain($"value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource); + + s.Driver.FindElement(By.Name("update")).Click(); + s.AssertHappyMessage(); + Assert.Contains(server.ServerUri.AbsoluteUri, s.Driver.PageSource); + + Logs.Tester.LogInformation("Let's see if we can generate an event"); + s.GoToStore(store.storeId); + s.AddDerivationScheme(); + s.CreateInvoice(store.storeName); + var request = await server.GetNextRequest(); + var headers = request.Request.Headers; + var actualSig = headers["BTCPay-Sig"].First(); + var bytes = await request.Request.Body.ReadBytesAsync((int)headers.ContentLength.Value); + var expectedSig = $"sha256={Encoders.Hex.EncodeData(new HMACSHA256(Encoding.UTF8.GetBytes("HelloWorld")).ComputeHash(bytes))}"; + Assert.Equal(expectedSig, actualSig); + request.Response.StatusCode = 200; + server.Done(); + + Logs.Tester.LogInformation("Let's make a failed event"); + s.CreateInvoice(store.storeName); + request = await server.GetNextRequest(); + request.Response.StatusCode = 404; + server.Done(); + + // The delivery is done asynchronously, so small wait here + await Task.Delay(500); + s.GoToStore(store.storeId, Views.Stores.StoreNavPages.Webhooks); + s.Driver.FindElement(By.LinkText("Modify")).Click(); + var elements = s.Driver.FindElements(By.ClassName("redeliver")); + // One worked, one failed + s.Driver.FindElement(By.ClassName("fa-times")); + s.Driver.FindElement(By.ClassName("fa-check")); + elements[0].Click(); + s.AssertHappyMessage(); + request = await server.GetNextRequest(); + request.Response.StatusCode = 404; + server.Done(); + + Logs.Tester.LogInformation("Can we browse the json content?"); + CanBrowseContent(s); + + s.GoToInvoices(); + s.Driver.FindElement(By.LinkText("Details")).Click(); + CanBrowseContent(s); + var element = s.Driver.FindElement(By.ClassName("redeliver")); + element.Click(); + s.AssertHappyMessage(); + request = await server.GetNextRequest(); + request.Response.StatusCode = 404; + server.Done(); + + Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside"); + s.GoToStore(store.storeId); + s.Driver.ExecuteJavaScript("window.scrollBy(0,1000);"); + s.Driver.FindElement(By.Id("danger-zone-expander")).Click(); + s.Driver.FindElement(By.Id("delete-store")).Click(); + s.Driver.FindElement(By.Id("continue")).Click(); + s.AssertHappyMessage(); + } + } + + private static void CanBrowseContent(SeleniumTester s) + { + s.Driver.FindElement(By.ClassName("delivery-content")).Click(); + var windows = s.Driver.WindowHandles; + Assert.Equal(2, windows.Count); + s.Driver.SwitchTo().Window(windows[1]); + JObject.Parse(s.Driver.FindElement(By.TagName("body")).Text); + s.Driver.Close(); + s.Driver.SwitchTo().Window(windows[0]); + } [Fact(Timeout = TestTimeout)] public async Task CanManageWallet() diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 541c1a4e2..3e72f9ba5 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -23,6 +23,7 @@ namespace BTCPayServer.Tests return new ServerTester(scope, newDb); } + public List Resources = new List(); readonly string _Directory; public ServerTester(string scope, bool newDb) { @@ -145,7 +146,7 @@ namespace BTCPayServer.Tests var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var sub = PayTester.GetService().Subscribe(evt => { - if(correctEvent is null) + if (correctEvent is null) tcs.TrySetResult(evt); else if (correctEvent(evt)) { @@ -207,6 +208,8 @@ namespace BTCPayServer.Tests public void Dispose() { + foreach (var r in this.Resources) + r.Dispose(); Logs.Tester.LogInformation("Disposing the BTCPayTester..."); foreach (var store in Stores) { diff --git a/BTCPayServer.Tests/StorageTests.cs b/BTCPayServer.Tests/StorageTests.cs index e72b1c460..ba04a922c 100644 --- a/BTCPayServer.Tests/StorageTests.cs +++ b/BTCPayServer.Tests/StorageTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Controllers; using BTCPayServer.Models; using BTCPayServer.Models.ServerViewModels; diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 7b3c1d9d8..81e3962fb 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Client.Models; @@ -23,8 +25,10 @@ using NBitcoin.Payment; using NBitpayClient; using NBXplorer.DerivationStrategy; using NBXplorer.Models; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; +using Xunit.Sdk; namespace BTCPayServer.Tests { @@ -427,5 +431,86 @@ namespace BTCPayServer.Tests return null; return parsedBip21; } + + class WebhookListener : IDisposable + { + private Client.Models.StoreWebhookData _wh; + private FakeServer _server; + private readonly List _webhookEvents; + private CancellationTokenSource _cts; + public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List 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(callback)); + req.Response.StatusCode = 200; + _server.Done(); + } + } + public void Dispose() + { + _cts.Cancel(); + _server.Dispose(); + } + } + + public List WebhookEvents { get; set; } = new List(); + public TEvent AssertHasWebhookEvent(WebhookEventType eventType, Action assert) where TEvent : class + { + foreach (var evt in WebhookEvents) + { + if (evt.Type == eventType) + { + var typedEvt = evt.ReadAs(); + try + { + assert(typedEvt); + return typedEvt; + } + catch (XunitException) + { + } + } + } + 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); + }); + } } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index f380f9281..86a8dc964 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -11,6 +11,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Configuration; @@ -770,30 +771,31 @@ namespace BTCPayServer.Tests BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m)); }); await tester.ExplorerNode.GenerateAsync(1); + await Task.Delay(100); // wait a bit for payment to process before fetching new invoice var newInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id); - var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; - var oldBolt11= invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; - Assert.NotEqual(newBolt11,oldBolt11); + var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; + var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; + Assert.NotEqual(newBolt11, oldBolt11); Assert.Equal(newInvoice.BtcDue.GetValue(), BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC)); - - Logs.Tester.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning" ); + + Logs.Tester.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning"); var evt = await tester.WaitForEvent(async () => { await tester.SendLightningPaymentAsync(newInvoice); }, evt => evt.InvoiceId == invoice.Id); var fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId); - Assert.Contains(fetchedInvoice.Status, new []{InvoiceStatus.Complete, InvoiceStatus.Confirmed}); + Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatus.Complete, InvoiceStatus.Confirmed }); Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus); - - Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice " ); + + Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice "); evt = await tester.WaitForEvent(async () => { await tester.SendLightningPaymentAsync(invoice); }, evt => evt.InvoiceId == invoice.Id); Assert.Equal(evt.InvoiceId, invoice.Id); fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId); - Assert.Equal( 3,fetchedInvoice.Payments.Count); + Assert.Equal(3, fetchedInvoice.Payments.Count); } [Fact(Timeout = 60 * 2 * 1000)] @@ -999,7 +1001,6 @@ namespace BTCPayServer.Tests } } } - var invoice2 = acc.BitPay.GetInvoice(invoice.Id); Assert.NotNull(invoice2); } @@ -1508,7 +1509,7 @@ namespace BTCPayServer.Tests ); } } - + // [Fact(Timeout = TestTimeout)] [Fact()] [Trait("Integration", "Integration")] @@ -1527,9 +1528,9 @@ namespace BTCPayServer.Tests BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.01m)); }); - - + + var payments = Assert.IsType( Assert.IsType(await user.GetController().Invoice(invoice.Id)).Model) .Payments; @@ -1990,7 +1991,60 @@ namespace BTCPayServer.Tests }; var criteriaCompat = store.GetPaymentMethodCriteria(tester.NetworkProvider, blob); Assert.Single(criteriaCompat); - Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) )); + Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance))); + } + } + + [Fact] + [Trait("Integration", "Integration")] + public async Task CanSetUnifiedQrCode() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit); + user.RegisterLightningNode("BTC", LightningConnectionType.CLightning); + + var invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 5.5m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + // validate that invoice data model doesn't have lightning string initially + var res = await user.GetController().Checkout(invoice.Id); + var paymentMethodFirst = Assert.IsType( + Assert.IsType(res).Model + ); + Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR); + + // enable unified QR code in settings + var vm = Assert.IsType(Assert + .IsType(user.GetController().CheckoutExperience()).Model + ); + vm.OnChainWithLnInvoiceFallback = true; + Assert.IsType( + user.GetController().CheckoutExperience(vm).Result + ); + + // validate that QR code now has both onchain and offchain payment urls + res = await user.GetController().Checkout(invoice.Id); + var paymentMethodSecond = Assert.IsType( + Assert.IsType(res).Model + ); + Assert.Contains("&lightning=", paymentMethodSecond.InvoiceBitcoinUrlQR); + Assert.StartsWith("BITCOIN:", paymentMethodSecond.InvoiceBitcoinUrlQR); + var split = paymentMethodSecond.InvoiceBitcoinUrlQR.Split('?')[0]; + Assert.True($"BITCOIN:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split); } } @@ -2030,7 +2084,7 @@ namespace BTCPayServer.Tests Assert.Single(invoice.CryptoInfo); Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); - + //test backward compat var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId); var blob = store.GetStoreBlob(); @@ -2044,8 +2098,8 @@ namespace BTCPayServer.Tests }; var criteriaCompat = store.GetPaymentMethodCriteria(tester.NetworkProvider, blob); Assert.Single(criteriaCompat); - Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && !methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", LightningPaymentType.Instance) )); - + Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && !methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", LightningPaymentType.Instance))); + } } @@ -2527,6 +2581,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); + await user.SetupWebhook(); var invoice = user.BitPay.CreateInvoice( new Invoice() { @@ -2583,7 +2638,6 @@ namespace BTCPayServer.Tests var cashCow = tester.ExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); - var iii = ctx.AddressInvoices.ToArray(); Assert.True(IsMapped(invoice, ctx)); cashCow.SendToAddress(invoiceAddress, firstPayment); @@ -2687,6 +2741,23 @@ namespace BTCPayServer.Tests Assert.Equal(Money.Zero, localInvoice.BtcDue); Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); }); + + // Test on the webhooks + user.AssertHasWebhookEvent(WebhookEventType.InvoiceConfirmed, + c => + { + Assert.False(c.ManuallyMarked); + }); + user.AssertHasWebhookEvent(WebhookEventType.InvoicePaidInFull, + c => + { + Assert.True(c.OverPaid); + }); + user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment, + c => + { + Assert.False(c.AfterExpiration); + }); } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 692df20b9..93d1d40de 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -247,5 +247,5 @@ <_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" /> - + diff --git a/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs b/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs index 8f135903d..e692f313f 100644 --- a/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs +++ b/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Models.NotificationViewModels; namespace BTCPayServer.Components.NotificationsDropdown diff --git a/BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs b/BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs index 9d4fedade..75fc2a9e8 100644 --- a/BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs +++ b/BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Components.UIExtensionPoint diff --git a/BTCPayServer/Controllers/AccessTokenController.cs b/BTCPayServer/Controllers/AccessTokenController.cs index 8259333b8..844c7a998 100644 --- a/BTCPayServer/Controllers/AccessTokenController.cs +++ b/BTCPayServer/Controllers/AccessTokenController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Filters; using BTCPayServer.Models; using BTCPayServer.Security.Bitpay; @@ -9,7 +10,7 @@ using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers { - [Authorize(AuthenticationSchemes = Security.AuthenticationSchemes.Bitpay)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Bitpay)] [BitpayAPIConstraint()] public class AccessTokenController : Controller { diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 3c2ac509f..0e74e30e3 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -1,6 +1,9 @@ using System; using System.Globalization; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Logging; diff --git a/BTCPayServer/Controllers/AppsController.cs b/BTCPayServer/Controllers/AppsController.cs index 3a788e384..71d7ec45a 100644 --- a/BTCPayServer/Controllers/AppsController.cs +++ b/BTCPayServer/Controllers/AppsController.cs @@ -1,6 +1,9 @@ using System; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Data; using BTCPayServer.Models; using BTCPayServer.Models.AppViewModels; diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 2366c00f0..80e572523 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Filters; diff --git a/BTCPayServer/Controllers/GreenField/ApiKeysController.cs b/BTCPayServer/Controllers/GreenField/ApiKeysController.cs index 6b95460d4..ca6549212 100644 --- a/BTCPayServer/Controllers/GreenField/ApiKeysController.cs +++ b/BTCPayServer/Controllers/GreenField/ApiKeysController.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index c80b4fe97..d05e2075d 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Models.InvoicingModels; diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs index ceb006127..7d86f75ed 100644 --- a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Configuration; diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs index d36a3ef75..789ae8f44 100644 --- a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Configuration; diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs index 5e10b25ab..d8eb1079b 100644 --- a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs @@ -139,7 +139,7 @@ namespace BTCPayServer.Controllers.GreenField ModelState.AddModelError(nameof(request.FeeRate), "FeeRate must be more than 0"); } - if (ModelState.IsValid) + if (!ModelState.IsValid) { return this.CreateValidationError(ModelState); } diff --git a/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs b/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs index 583c5f978..f07a5ebb4 100644 --- a/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs +++ b/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; diff --git a/BTCPayServer/Controllers/GreenField/PullPaymentController.cs b/BTCPayServer/Controllers/GreenField/PullPaymentController.cs index e49bac3f5..4f4903bd6 100644 --- a/BTCPayServer/Controllers/GreenField/PullPaymentController.cs +++ b/BTCPayServer/Controllers/GreenField/PullPaymentController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using BTCPayServer; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; diff --git a/BTCPayServer/Controllers/GreenField/ServerInfoController.cs b/BTCPayServer/Controllers/GreenField/ServerInfoController.cs index 6f965ca78..363f1a961 100644 --- a/BTCPayServer/Controllers/GreenField/ServerInfoController.cs +++ b/BTCPayServer/Controllers/GreenField/ServerInfoController.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.HostedServices; diff --git a/BTCPayServer/Controllers/GreenField/StoreWebhooksController.cs b/BTCPayServer/Controllers/GreenField/StoreWebhooksController.cs new file mode 100644 index 000000000..fc1b0ca26 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/StoreWebhooksController.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Amazon.Runtime; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.HostedServices; +using BTCPayServer.Security; +using BTCPayServer.Services.Stores; +using Google.Apis.Auth.OAuth2; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Controllers.GreenField +{ + [ApiController] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, + Policy = Policies.CanModifyStoreWebhooks)] + [EnableCors(CorsPolicies.All)] + public class StoreWebhooksController : ControllerBase + { + public StoreWebhooksController(StoreRepository storeRepository, WebhookNotificationManager webhookNotificationManager) + { + StoreRepository = storeRepository; + WebhookNotificationManager = webhookNotificationManager; + } + + public StoreRepository StoreRepository { get; } + public WebhookNotificationManager WebhookNotificationManager { get; } + + [HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId?}")] + public async Task ListWebhooks(string storeId, string webhookId) + { + if (webhookId is null) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + return Ok((await StoreRepository.GetWebhooks(storeId)) + .Select(o => FromModel(o, false)) + .ToList()); + } + else + { + var w = await StoreRepository.GetWebhook(storeId, webhookId); + if (w is null) + return NotFound(); + return Ok(FromModel(w, false)); + } + } + [HttpPost("~/api/v1/stores/{storeId}/webhooks")] + public async Task CreateWebhook(string storeId, Client.Models.CreateStoreWebhookRequest create) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + ValidateWebhookRequest(create); + if (!ModelState.IsValid) + return this.CreateValidationError(ModelState); + var webhookId = await StoreRepository.CreateWebhook(storeId, ToModel(create)); + var w = await StoreRepository.GetWebhook(storeId, webhookId); + if (w is null) + return NotFound(); + return Ok(FromModel(w, true)); + } + + private void ValidateWebhookRequest(StoreWebhookBaseData create) + { + if (!Uri.TryCreate(create?.Url, UriKind.Absolute, out var uri)) + ModelState.AddModelError(nameof(Url), "Invalid Url"); + } + + [HttpPut("~/api/v1/stores/{storeId}/webhooks/{webhookId}")] + public async Task UpdateWebhook(string storeId, string webhookId, Client.Models.UpdateStoreWebhookRequest update) + { + ValidateWebhookRequest(update); + if (!ModelState.IsValid) + return this.CreateValidationError(ModelState); + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + var w = await StoreRepository.GetWebhook(storeId, webhookId); + if (w is null) + return NotFound(); + await StoreRepository.UpdateWebhook(storeId, webhookId, ToModel(update)); + return await ListWebhooks(storeId, webhookId); + } + [HttpDelete("~/api/v1/stores/{storeId}/webhooks/{webhookId}")] + public async Task DeleteWebhook(string storeId, string webhookId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + var w = await StoreRepository.GetWebhook(storeId, webhookId); + if (w is null) + return NotFound(); + await StoreRepository.DeleteWebhook(storeId, webhookId); + return Ok(); + } + private WebhookBlob ToModel(StoreWebhookBaseData create) + { + return new WebhookBlob() + { + Active = create.Enabled, + Url = create.Url, + Secret = create.Secret, + AuthorizedEvents = create.AuthorizedEvents is Client.Models.StoreWebhookBaseData.AuthorizedEventsData aed ? + new AuthorizedWebhookEvents() + { + Everything = aed.Everything, + SpecificEvents = aed.SpecificEvents + }: + new AuthorizedWebhookEvents() { Everything = true }, + AutomaticRedelivery = create.AutomaticRedelivery, + }; + } + + + [HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId?}")] + public async Task ListDeliveries(string storeId, string webhookId, string deliveryId, int? count = null) + { + if (deliveryId is null) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + return Ok((await StoreRepository.GetWebhookDeliveries(storeId, webhookId, count)) + .Select(o => FromModel(o)) + .ToList()); + } + else + { + var delivery = await StoreRepository.GetWebhookDelivery(storeId, webhookId, deliveryId); + if (delivery is null) + return NotFound(); + return Ok(FromModel(delivery)); + } + } + [HttpPost("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")] + public async Task RedeliverWebhook(string storeId, string webhookId, string deliveryId) + { + var delivery = await StoreRepository.GetWebhookDelivery(HttpContext.GetStoreData().Id, webhookId, deliveryId); + if (delivery is null) + return NotFound(); + return this.Ok(new JValue(await WebhookNotificationManager.Redeliver(deliveryId))); + } + + [HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")] + public async Task GetDeliveryRequest(string storeId, string webhookId, string deliveryId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + var delivery = await StoreRepository.GetWebhookDelivery(storeId, webhookId, deliveryId); + if (delivery is null) + return NotFound(); + return File(delivery.GetBlob().Request, "application/json"); + } + + private Client.Models.WebhookDeliveryData FromModel(Data.WebhookDeliveryData data) + { + var b = data.GetBlob(); + return new Client.Models.WebhookDeliveryData() + { + Id = data.Id, + Timestamp = data.Timestamp, + Status = b.Status, + ErrorMessage = b.ErrorMessage, + HttpCode = b.HttpCode + }; + } + + Client.Models.StoreWebhookData FromModel(Data.WebhookData data, bool includeSecret) + { + var b = data.GetBlob(); + return new Client.Models.StoreWebhookData() + { + Id = data.Id, + Url = b.Url, + Enabled = b.Active, + Secret = includeSecret ? b.Secret : null, + AutomaticRedelivery = b.AutomaticRedelivery, + AuthorizedEvents = new Client.Models.StoreWebhookData.AuthorizedEventsData() + { + Everything = b.AuthorizedEvents.Everything, + SpecificEvents = b.AuthorizedEvents.SpecificEvents + } + }; + } + } +} diff --git a/BTCPayServer/Controllers/GreenField/StoresController.cs b/BTCPayServer/Controllers/GreenField/StoresController.cs index d113d58b2..ca275f719 100644 --- a/BTCPayServer/Controllers/GreenField/StoresController.cs +++ b/BTCPayServer/Controllers/GreenField/StoresController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; @@ -120,21 +121,22 @@ namespace BTCPayServer.Controllers.GreenField //we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572) NetworkFeeMode = storeBlob.NetworkFeeMode, RequiresRefundEmail = storeBlob.RequiresRefundEmail, + LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, + LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints, + OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback, + RedirectAutomatically = storeBlob.RedirectAutomatically, ShowRecommendedFee = storeBlob.ShowRecommendedFee, RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget, DefaultLang = storeBlob.DefaultLang, MonitoringExpiration = storeBlob.MonitoringExpiration, InvoiceExpiration = storeBlob.InvoiceExpiration, - LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, CustomLogo = storeBlob.CustomLogo, CustomCSS = storeBlob.CustomCSS, HtmlTitle = storeBlob.HtmlTitle, AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice, LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate, PaymentTolerance = storeBlob.PaymentTolerance, - RedirectAutomatically = storeBlob.RedirectAutomatically, - PayJoinEnabled = storeBlob.PayJoinEnabled, - LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints + PayJoinEnabled = storeBlob.PayJoinEnabled }; } @@ -155,21 +157,22 @@ namespace BTCPayServer.Controllers.GreenField //we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572) blob.NetworkFeeMode = restModel.NetworkFeeMode; blob.RequiresRefundEmail = restModel.RequiresRefundEmail; + blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi; + blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints; + blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback; + blob.RedirectAutomatically = restModel.RedirectAutomatically; blob.ShowRecommendedFee = restModel.ShowRecommendedFee; blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget; blob.DefaultLang = restModel.DefaultLang; blob.MonitoringExpiration = restModel.MonitoringExpiration; blob.InvoiceExpiration = restModel.InvoiceExpiration; - blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi; blob.CustomLogo = restModel.CustomLogo; blob.CustomCSS = restModel.CustomCSS; blob.HtmlTitle = restModel.HtmlTitle; blob.AnyoneCanInvoice = restModel.AnyoneCanCreateInvoice; blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate; blob.PaymentTolerance = restModel.PaymentTolerance; - blob.RedirectAutomatically = restModel.RedirectAutomatically; blob.PayJoinEnabled = restModel.PayJoinEnabled; - blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints; model.SetStoreBlob(blob); } diff --git a/BTCPayServer/Controllers/GreenField/TestApiKeyController.cs b/BTCPayServer/Controllers/GreenField/TestApiKeyController.cs index 804558fe9..248f17839 100644 --- a/BTCPayServer/Controllers/GreenField/TestApiKeyController.cs +++ b/BTCPayServer/Controllers/GreenField/TestApiKeyController.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security; diff --git a/BTCPayServer/Controllers/GreenField/UsersController.cs b/BTCPayServer/Controllers/GreenField/UsersController.cs index e76cd784c..fc2aa5231 100644 --- a/BTCPayServer/Controllers/GreenField/UsersController.cs +++ b/BTCPayServer/Controllers/GreenField/UsersController.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Configuration; diff --git a/BTCPayServer/Controllers/HomeController.cs b/BTCPayServer/Controllers/HomeController.cs index ead82b648..fe2c39b61 100644 --- a/BTCPayServer/Controllers/HomeController.cs +++ b/BTCPayServer/Controllers/HomeController.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Models; diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index fca1ded2b..4d6db5029 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Filters; using BTCPayServer.Models; @@ -51,7 +52,7 @@ namespace BTCPayServer.Controllers } [HttpGet] [Route("invoices")] - public async Task> GetInvoices( + public async Task GetInvoices( string token, DateTimeOffset? dateStart = null, DateTimeOffset? dateEnd = null, @@ -61,6 +62,8 @@ namespace BTCPayServer.Controllers int? limit = null, int? offset = null) { + if (User.Identity.AuthenticationType == Security.Bitpay.BitpayAuthenticationTypes.Anonymous) + return Forbid(Security.Bitpay.BitpayAuthenticationTypes.Anonymous); if (dateEnd != null) dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day @@ -79,7 +82,7 @@ namespace BTCPayServer.Controllers var entities = (await _InvoiceRepository.GetInvoices(query)) .Select((o) => o.EntityToDTO()).ToArray(); - return DataWrapper.Create(entities); + return Json(DataWrapper.Create(entities)); } } } diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index b94cb949d..e16625b9d 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -6,6 +6,9 @@ using System.Net.Mime; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; @@ -39,6 +42,51 @@ namespace BTCPayServer.Controllers { public partial class InvoiceController { + + [HttpGet] + [Route("invoices/{invoiceId}/deliveries/{deliveryId}/request")] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task WebhookDelivery(string invoiceId, string deliveryId) + { + var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() + { + InvoiceId = new[] { invoiceId }, + UserId = GetUserId() + })).FirstOrDefault(); + if (invoice is null) + return NotFound(); + var delivery = await _InvoiceRepository.GetWebhookDelivery(invoiceId, deliveryId); + if (delivery is null) + return NotFound(); + return this.File(delivery.GetBlob().Request, "application/json"); + } + [HttpPost] + [Route("invoices/{invoiceId}/deliveries/{deliveryId}/redeliver")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task RedeliverWebhook(string storeId, string invoiceId, string deliveryId) + { + var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() + { + InvoiceId = new[] { invoiceId }, + StoreId = new[] { storeId }, + UserId = GetUserId() + })).FirstOrDefault(); + if (invoice is null) + return NotFound(); + var delivery = await _InvoiceRepository.GetWebhookDelivery(invoiceId, deliveryId); + if (delivery is null) + return NotFound(); + var newDeliveryId = await WebhookNotificationManager.Redeliver(deliveryId); + if (newDeliveryId is null) + return NotFound(); + TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery"; + return RedirectToAction(nameof(Invoice), + new + { + invoiceId + }); + } + [HttpGet] [Route("invoices/{invoiceId}")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] @@ -58,6 +106,7 @@ namespace BTCPayServer.Controllers var store = await _StoreRepository.FindStore(invoice.StoreId); var model = new InvoiceDetailsModel() { + StoreId = store.Id, StoreName = store.StoreName, StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }), Id = invoice.Id, @@ -80,6 +129,9 @@ namespace BTCPayServer.Controllers PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData), Archived = invoice.Archived, CanRefund = CanRefund(invoice.GetInvoiceState()), + Deliveries = (await _InvoiceRepository.GetWebhookDeliveries(invoiceId)) + .Select(c => new Models.StoreViewModels.DeliveryViewModel(c)) + .ToList() }; model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index e5b2f2ccc..f14a4d9f4 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -45,6 +45,9 @@ namespace BTCPayServer.Controllers private readonly ApplicationDbContextFactory _dbContextFactory; private readonly PullPaymentHostedService _paymentHostedService; readonly IServiceProvider _ServiceProvider; + + public WebhookNotificationManager WebhookNotificationManager { get; } + public InvoiceController( IServiceProvider serviceProvider, InvoiceRepository invoiceRepository, @@ -57,7 +60,8 @@ namespace BTCPayServer.Controllers BTCPayNetworkProvider networkProvider, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, ApplicationDbContextFactory dbContextFactory, - PullPaymentHostedService paymentHostedService) + PullPaymentHostedService paymentHostedService, + WebhookNotificationManager webhookNotificationManager) { _ServiceProvider = serviceProvider; _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); @@ -70,6 +74,7 @@ namespace BTCPayServer.Controllers _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; _dbContextFactory = dbContextFactory; _paymentHostedService = paymentHostedService; + WebhookNotificationManager = webhookNotificationManager; _CSP = csp; } @@ -215,7 +220,7 @@ namespace BTCPayServer.Controllers if (paymentMethodCriteria.Value != null) { currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, paymentMethodCriteria.Value.Currency)); - } + } } } @@ -305,7 +310,9 @@ namespace BTCPayServer.Controllers }).ToArray()); } - private async Task CreatePaymentMethodAsync(Dictionary> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, StoreData store, InvoiceLogs logs) + private async Task CreatePaymentMethodAsync(Dictionary> fetchingByCurrencyPair, + IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, + StoreData store, InvoiceLogs logs) { try { @@ -317,12 +324,14 @@ namespace BTCPayServer.Controllers { return null; } - PaymentMethod paymentMethod = new PaymentMethod(); - paymentMethod.ParentEntity = entity; - paymentMethod.Network = network; + var paymentMethod = new PaymentMethod + { + ParentEntity = entity, + Network = network, + Rate = rate.BidAsk.Bid, + PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) + }; paymentMethod.SetId(supportedPaymentMethod.PaymentId); - paymentMethod.Rate = rate.BidAsk.Bid; - paymentMethod.PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); using (logs.Measure($"{logPrefix} Payment method details creation")) { @@ -339,7 +348,7 @@ namespace BTCPayServer.Controllers { var amount = paymentMethod.Calculate().Due.GetValue(network as BTCPayNetwork); var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid; - + if (amount < limitValueCrypto && criteria.Above) { logs.Write($"{logPrefix} invoice amount below accepted value for payment method", InvoiceEventData.EventSeverity.Error); @@ -369,7 +378,7 @@ namespace BTCPayServer.Controllers } catch (Exception ex) { - logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})", InvoiceEventData.EventSeverity.Error); + logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex})", InvoiceEventData.EventSeverity.Error); } return null; } diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 247d4202c..adafd5c29 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Models; @@ -465,6 +467,8 @@ namespace BTCPayServer.Controllers {BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")}, {BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to view, modify, delete and create new invoices on all your stores.")}, {$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")}, + {BTCPayServer.Client.Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will be mofidy the webhooks of all your stores.")}, + {$"{BTCPayServer.Client.Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will be mofidy the webhooks of the selected stores.")}, {BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")}, {$"{BTCPayServer.Client.Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")}, {BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")}, diff --git a/BTCPayServer/Controllers/ManageController.Notifications.cs b/BTCPayServer/Controllers/ManageController.Notifications.cs index df90cecf0..9795e32bb 100644 --- a/BTCPayServer/Controllers/ManageController.Notifications.cs +++ b/BTCPayServer/Controllers/ManageController.Notifications.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; diff --git a/BTCPayServer/Controllers/ManageController.U2F.cs b/BTCPayServer/Controllers/ManageController.U2F.cs index 2e4df074c..b6f1ece4e 100644 --- a/BTCPayServer/Controllers/ManageController.U2F.cs +++ b/BTCPayServer/Controllers/ManageController.U2F.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Models; using BTCPayServer.U2F.Models; using Microsoft.AspNetCore.Mvc; diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index 5fef8d0d8..ab8187ac4 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -1,6 +1,7 @@ using System; using System.Text.Encodings.Web; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Data; using BTCPayServer.Models.ManageViewModels; using BTCPayServer.Security; diff --git a/BTCPayServer/Controllers/NotificationsController.cs b/BTCPayServer/Controllers/NotificationsController.cs index d823bcb01..b0e3e22ca 100644 --- a/BTCPayServer/Controllers/NotificationsController.cs +++ b/BTCPayServer/Controllers/NotificationsController.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Models.NotificationViewModels; diff --git a/BTCPayServer/Controllers/PaymentRequestController.cs b/BTCPayServer/Controllers/PaymentRequestController.cs index cedf4309d..dbf56a4d1 100644 --- a/BTCPayServer/Controllers/PaymentRequestController.cs +++ b/BTCPayServer/Controllers/PaymentRequestController.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Events; diff --git a/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs b/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs index 4d869a39f..9ece7b317 100644 --- a/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs +++ b/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs @@ -41,20 +41,21 @@ namespace BTCPayServer.Controllers var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store); var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode); var nodeInfo = - await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails, + await _LightningLikePaymentHandler.GetNodeInfo(Request.IsOnion(), paymentMethodDetails, network); - return View(new ShowLightningNodeInfoViewModel() + return View(new ShowLightningNodeInfoViewModel { Available = true, NodeInfo = nodeInfo.ToString(), CryptoCode = cryptoCode, - CryptoImage = GetImage(paymentMethodDetails.PaymentId, network) + CryptoImage = GetImage(paymentMethodDetails.PaymentId, network), + StoreName = store.StoreName }); } catch (Exception) { - return View(new ShowLightningNodeInfoViewModel() { Available = false, CryptoCode = cryptoCode }); + return View(new ShowLightningNodeInfoViewModel { Available = false, CryptoCode = cryptoCode }); } } @@ -83,5 +84,6 @@ namespace BTCPayServer.Controllers public bool Available { get; set; } public string CryptoCode { get; set; } public string CryptoImage { get; set; } + public string StoreName { get; set; } } } diff --git a/BTCPayServer/Controllers/PullPaymentController.cs b/BTCPayServer/Controllers/PullPaymentController.cs index aeca1988b..ca24e3fe9 100644 --- a/BTCPayServer/Controllers/PullPaymentController.cs +++ b/BTCPayServer/Controllers/PullPaymentController.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using BTCPayServer; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Models; diff --git a/BTCPayServer/Controllers/RateController.cs b/BTCPayServer/Controllers/RateController.cs index 712193734..f10a2ba8b 100644 --- a/BTCPayServer/Controllers/RateController.cs +++ b/BTCPayServer/Controllers/RateController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Models; @@ -20,7 +21,7 @@ using Newtonsoft.Json; namespace BTCPayServer.Controllers { [EnableCors(CorsPolicies.All)] - [Authorize(Policy = ServerPolicies.CanGetRates.Key, AuthenticationSchemes = Security.AuthenticationSchemes.Bitpay)] + [Authorize(Policy = ServerPolicies.CanGetRates.Key, AuthenticationSchemes = AuthenticationSchemes.Bitpay)] public class RateController : Controller { public StoreData CurrentStore diff --git a/BTCPayServer/Controllers/ServerController.Plugins.cs b/BTCPayServer/Controllers/ServerController.Plugins.cs index 62c3e93bd..d0d88f872 100644 --- a/BTCPayServer/Controllers/ServerController.Plugins.cs +++ b/BTCPayServer/Controllers/ServerController.Plugins.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Configuration; -using BTCPayServer.Contracts; using BTCPayServer.Models; using BTCPayServer.Plugins; using Microsoft.AspNetCore.Http; diff --git a/BTCPayServer/Controllers/ServerController.Storage.cs b/BTCPayServer/Controllers/ServerController.Storage.cs index 8a067ab79..3af0950f0 100644 --- a/BTCPayServer/Controllers/ServerController.Storage.cs +++ b/BTCPayServer/Controllers/ServerController.Storage.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Models; using BTCPayServer.Models.ServerViewModels; using BTCPayServer.Storage.Models; diff --git a/BTCPayServer/Controllers/ServerController.Users.cs b/BTCPayServer/Controllers/ServerController.Users.cs index a5fc5e26c..24464bd6b 100644 --- a/BTCPayServer/Controllers/ServerController.Users.cs +++ b/BTCPayServer/Controllers/ServerController.Users.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Models; diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 7bb2728a7..ceb024d3a 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -7,6 +7,8 @@ using System.Net; using System.Net.Http; using System.Net.Mail; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Events; @@ -33,11 +35,12 @@ using Microsoft.Extensions.Logging; using NBitcoin; using NBitcoin.DataEncoders; using Renci.SshNet; +using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes; namespace BTCPayServer.Controllers { [Authorize(Policy = BTCPayServer.Client.Policies.CanModifyServerSettings, - AuthenticationSchemes = BTCPayServer.Security.AuthenticationSchemes.Cookie)] + AuthenticationSchemes = AuthenticationSchemes.Cookie)] public partial class ServerController : Controller { private readonly UserManager _UserManager; diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 99faddd50..e23694e26 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Events; diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index 6230d549b..503a7f43b 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -1,12 +1,16 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; +using System.Text; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; +using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Shopify; @@ -16,6 +20,10 @@ using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NBitcoin; +using NBitcoin.DataEncoders; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NicolasDorier.RateLimits; @@ -171,6 +179,127 @@ namespace BTCPayServer.Controllers return View("Integrations", vm); } + [HttpGet] + [Route("{storeId}/webhooks")] + public async Task Webhooks() + { + var webhooks = await this._Repo.GetWebhooks(CurrentStore.Id); + return View(nameof(Webhooks), new WebhooksViewModel() + { + Webhooks = webhooks.Select(w => new WebhooksViewModel.WebhookViewModel() + { + Id = w.Id, + Url = w.GetBlob().Url + }).ToArray() + }); + } + [HttpGet] + [Route("{storeId}/webhooks/new")] + public IActionResult NewWebhook() + { + return View(nameof(ModifyWebhook), new EditWebhookViewModel() + { + Active = true, + Everything = true, + IsNew = true, + Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)) + }); + } + + [HttpGet] + [Route("{storeId}/webhooks/{webhookId}/remove")] + public async Task DeleteWebhook(string webhookId) + { + var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + return View("Confirm", new ConfirmModel() + { + Title = $"Delete a webhook", + Description = "This webhook will be removed from this store, do you wish to continue?", + Action = "Delete" + }); + } + + [HttpPost] + [Route("{storeId}/webhooks/{webhookId}/remove")] + public async Task DeleteWebhookPost(string webhookId) + { + var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + await _Repo.DeleteWebhook(CurrentStore.Id, webhookId); + TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted"; + return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); + } + + [HttpPost] + [Route("{storeId}/webhooks/new")] + public async Task NewWebhook(string storeId, EditWebhookViewModel viewModel) + { + if (!ModelState.IsValid) + return View(viewModel); + + var webhookId = await _Repo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob()); + TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created"; + return RedirectToAction(nameof(Webhooks), new { storeId }); + } + [HttpGet] + [Route("{storeId}/webhooks/{webhookId}")] + public async Task ModifyWebhook(string webhookId) + { + var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + var blob = webhook.GetBlob(); + var deliveries = await _Repo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20); + return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob) + { + Deliveries = deliveries + .Select(s => new DeliveryViewModel(s)).ToList() + }); + } + [HttpPost] + [Route("{storeId}/webhooks/{webhookId}")] + public async Task ModifyWebhook(string webhookId, EditWebhookViewModel viewModel) + { + var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + + await _Repo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob()); + TempData[WellKnownTempData.SuccessMessage] = "The webhook has been updated"; + return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); + } + + [HttpPost] + [Route("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")] + public async Task RedeliverWebhook(string webhookId, string deliveryId) + { + var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); + if (delivery is null) + return NotFound(); + var newDeliveryId = await WebhookNotificationManager.Redeliver(deliveryId); + if (newDeliveryId is null) + return NotFound(); + TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery"; + return RedirectToAction(nameof(ModifyWebhook), + new + { + storeId = CurrentStore.Id, + webhookId + }); + } + [HttpGet] + [Route("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")] + public async Task WebhookDelivery(string webhookId, string deliveryId) + { + var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); + if (delivery is null) + return NotFound(); + return this.File(delivery.GetBlob().Request, "application/json"); + } + [HttpPost] [Route("{storeId}/integrations/shopify")] public async Task Integrations([FromServices] IHttpClientFactory clientFactory, diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 0d7153532..886233820 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -5,6 +5,9 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Configuration; using BTCPayServer.Data; @@ -63,7 +66,8 @@ namespace BTCPayServer.Controllers EventAggregator eventAggregator, CssThemeManager cssThemeManager, AppService appService, - IWebHostEnvironment webHostEnvironment) + IWebHostEnvironment webHostEnvironment, + WebhookNotificationManager webhookNotificationManager) { _RateFactory = rateFactory; _Repo = repo; @@ -78,6 +82,7 @@ namespace BTCPayServer.Controllers _CssThemeManager = cssThemeManager; _appService = appService; _webHostEnvironment = webHostEnvironment; + WebhookNotificationManager = webhookNotificationManager; _EventAggregator = eventAggregator; _NetworkProvider = networkProvider; _ExplorerProvider = explorerProvider; @@ -375,16 +380,19 @@ namespace BTCPayServer.Controllers Type = criteria.Above ? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan : PaymentMethodCriteriaViewModel.CriteriaType.LessThan, Value = criteria.Value?.ToString() ?? "" }).ToList(); + + vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; + vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; + vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; + vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; + vm.RedirectAutomatically = storeBlob.RedirectAutomatically; + vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee; + vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget; + vm.CustomCSS = storeBlob.CustomCSS; vm.CustomLogo = storeBlob.CustomLogo; vm.HtmlTitle = storeBlob.HtmlTitle; vm.SetLanguages(_LangService, storeBlob.DefaultLang); - vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; - vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee; - vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget; - vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; - vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; - vm.RedirectAutomatically = storeBlob.RedirectAutomatically; return View(vm); } @@ -450,16 +458,20 @@ namespace BTCPayServer.Controllers blob.LightningMaxValue = null; blob.OnChainMinValue = null; #pragma warning restore 612 + + blob.RequiresRefundEmail = model.RequiresRefundEmail; + blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; + blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints; + blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback; + blob.RedirectAutomatically = model.RedirectAutomatically; + blob.ShowRecommendedFee = model.ShowRecommendedFee; + blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget; + blob.CustomLogo = model.CustomLogo; blob.CustomCSS = model.CustomCSS; blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; blob.DefaultLang = model.DefaultLang; - blob.RequiresRefundEmail = model.RequiresRefundEmail; - blob.ShowRecommendedFee = model.ShowRecommendedFee; - blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget; - blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; - blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints; - blob.RedirectAutomatically = model.RedirectAutomatically; + if (CurrentStore.SetStoreBlob(blob)) { needUpdate = true; @@ -784,6 +796,7 @@ namespace BTCPayServer.Controllers } public string GeneratedPairingCode { get; set; } + public WebhookNotificationManager WebhookNotificationManager { get; } [HttpGet] [Route("{storeId}/Tokens/Create")] diff --git a/BTCPayServer/Controllers/UserStoresController.cs b/BTCPayServer/Controllers/UserStoresController.cs index 56d8fc2f3..6363d13b8 100644 --- a/BTCPayServer/Controllers/UserStoresController.cs +++ b/BTCPayServer/Controllers/UserStoresController.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Data; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index 69ba4dcee..a897fbd0b 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.HostedServices; using BTCPayServer.ModelBinders; using BTCPayServer.Models; diff --git a/BTCPayServer/Controllers/WalletsController.PullPayments.cs b/BTCPayServer/Controllers/WalletsController.PullPayments.cs index 040618817..6e50d7723 100644 --- a/BTCPayServer/Controllers/WalletsController.PullPayments.cs +++ b/BTCPayServer/Controllers/WalletsController.PullPayments.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.ModelBinders; diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 4382ba6e8..5b94bc87f 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -4,6 +4,9 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.HostedServices; diff --git a/BTCPayServer/Data/PaymentDataExtensions.cs b/BTCPayServer/Data/PaymentDataExtensions.cs index 45c267b75..199ca9db7 100644 --- a/BTCPayServer/Data/PaymentDataExtensions.cs +++ b/BTCPayServer/Data/PaymentDataExtensions.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using BTCPayServer.Services.Invoices; using Newtonsoft.Json.Linq; diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs index e86d767c8..79d3f8d65 100644 --- a/BTCPayServer/Data/StoreBlob.cs +++ b/BTCPayServer/Data/StoreBlob.cs @@ -31,22 +31,17 @@ namespace BTCPayServer.Data [Obsolete("Use NetworkFeeMode instead")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool? NetworkFeeDisabled - { - get; set; - } + public bool? NetworkFeeDisabled { get; set; } [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - public NetworkFeeMode NetworkFeeMode - { - get; - set; - } + public NetworkFeeMode NetworkFeeMode { get; set; } public bool RequiresRefundEmail { get; set; } - + public bool LightningAmountInSatoshi { get; set; } + public bool LightningPrivateRouteHints { get; set; } + public bool OnChainWithLnInvoiceFallback { get; set; } + public bool RedirectAutomatically { get; set; } public bool ShowRecommendedFee { get; set; } - public int RecommendedFeeBlockTarget { get; set; } CurrencyPair[] _DefaultCurrencyPairs; @@ -81,11 +76,7 @@ namespace BTCPayServer.Data [DefaultValue(typeof(TimeSpan), "00:15:00")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] - public TimeSpan InvoiceExpiration - { - get; - set; - } + public TimeSpan InvoiceExpiration { get; set; } public decimal Spread { get; set; } = 0.0m; @@ -106,8 +97,6 @@ namespace BTCPayServer.Data [Obsolete] [JsonConverter(typeof(CurrencyValueJsonConverter))] public CurrencyValue LightningMaxValue { get; set; } - public bool LightningAmountInSatoshi { get; set; } - public bool LightningPrivateRouteHints { get; set; } public string CustomCSS { get; set; } public string CustomLogo { get; set; } @@ -185,10 +174,10 @@ namespace BTCPayServer.Data public Dictionary WalletKeyPathRoots { get; set; } public EmailSettings EmailSettings { get; set; } - public bool RedirectAutomatically { get; set; } public bool PayJoinEnabled { get; set; } public StoreHints Hints { get; set; } + public class StoreHints { public bool Wallet { get; set; } @@ -229,7 +218,7 @@ namespace BTCPayServer.Data public CurrencyValue Value { get; set; } public bool Above { get; set; } } - + public class RateRule_Obsolete { public RateRule_Obsolete() diff --git a/BTCPayServer/Data/WebhookDataExtensions.cs b/BTCPayServer/Data/WebhookDataExtensions.cs new file mode 100644 index 000000000..53de1037a --- /dev/null +++ b/BTCPayServer/Data/WebhookDataExtensions.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SshNet.Security.Cryptography; + +namespace BTCPayServer.Data +{ + public class AuthorizedWebhookEvents + { + public bool Everything { get; set; } + + [JsonProperty(ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public WebhookEventType[] SpecificEvents { get; set; } = Array.Empty(); + public bool Match(WebhookEventType evt) + { + return Everything || SpecificEvents.Contains(evt); + } + } + + + public class WebhookDeliveryBlob + { + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public WebhookDeliveryStatus Status { get; set; } + public int? HttpCode { get; set; } + public string ErrorMessage { get; set; } + public byte[] Request { get; set; } + public T ReadRequestAs() + { + return JsonConvert.DeserializeObject(UTF8Encoding.UTF8.GetString(Request), HostedServices.WebhookNotificationManager.DefaultSerializerSettings); + } + } + public class WebhookBlob + { + public string Url { get; set; } + public bool Active { get; set; } = true; + public string Secret { get; set; } + public bool AutomaticRedelivery { get; set; } + public AuthorizedWebhookEvents AuthorizedEvents { get; set; } + } + public static class WebhookDataExtensions + { + public static WebhookBlob GetBlob(this WebhookData webhook) + { + return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(webhook.Blob)); + } + public static void SetBlob(this WebhookData webhook, WebhookBlob blob) + { + webhook.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob)); + } + public static WebhookDeliveryBlob GetBlob(this WebhookDeliveryData webhook) + { + return JsonConvert.DeserializeObject(ZipUtils.Unzip(webhook.Blob), HostedServices.WebhookNotificationManager.DefaultSerializerSettings); + } + public static void SetBlob(this WebhookDeliveryData webhook, WebhookDeliveryBlob blob) + { + webhook.Blob = ZipUtils.Zip(JsonConvert.SerializeObject(blob, Formatting.None, HostedServices.WebhookNotificationManager.DefaultSerializerSettings)); + } + } +} diff --git a/BTCPayServer/Events/IHasInvoiceId.cs b/BTCPayServer/Events/IHasInvoiceId.cs new file mode 100644 index 000000000..2d452a36c --- /dev/null +++ b/BTCPayServer/Events/IHasInvoiceId.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Events +{ + public interface IHasInvoiceId + { + string InvoiceId { get; } + } +} diff --git a/BTCPayServer/Events/InvoiceDataChangedEvent.cs b/BTCPayServer/Events/InvoiceDataChangedEvent.cs index 8c5124bff..e8339dbce 100644 --- a/BTCPayServer/Events/InvoiceDataChangedEvent.cs +++ b/BTCPayServer/Events/InvoiceDataChangedEvent.cs @@ -2,7 +2,7 @@ using BTCPayServer.Services.Invoices; namespace BTCPayServer.Events { - public class InvoiceDataChangedEvent + public class InvoiceDataChangedEvent : IHasInvoiceId { public InvoiceDataChangedEvent(InvoiceEntity invoice) { diff --git a/BTCPayServer/Events/InvoiceEvent.cs b/BTCPayServer/Events/InvoiceEvent.cs index ede5028ca..783f2e0be 100644 --- a/BTCPayServer/Events/InvoiceEvent.cs +++ b/BTCPayServer/Events/InvoiceEvent.cs @@ -3,7 +3,21 @@ using BTCPayServer.Services.Invoices; namespace BTCPayServer.Events { - public class InvoiceEvent + public enum InvoiceEventCode : int + { + Created = 1001, + ReceivedPayment = 1002, + PaidInFull = 1003, + Expired = 1004, + Confirmed = 1005, + Completed = 1006, + MarkedInvalid = 1008, + FailedToConfirm = 1013, + PaidAfterExpiration = 1009, + ExpiredPaidPartial = 2000, + MarkedCompleted = 2008, + } + public class InvoiceEvent : IHasInvoiceId { public const string Created = "invoice_created"; public const string ReceivedPayment = "invoice_receivedPayment"; @@ -16,20 +30,21 @@ namespace BTCPayServer.Events public const string FailedToConfirm = "invoice_failedToConfirm"; public const string Confirmed = "invoice_confirmed"; public const string Completed = "invoice_completed"; - - public static Dictionary EventCodes = new Dictionary() + + public string InvoiceId => Invoice.Id; + public static Dictionary EventCodes = new Dictionary() { - {Created, 1001}, - {ReceivedPayment, 1002}, - {PaidInFull, 1003}, - {Expired, 1004}, - {Confirmed, 1005}, - {Completed, 1006}, - {MarkedInvalid, 1008}, - {FailedToConfirm, 1013}, - {PaidAfterExpiration, 1009}, - {ExpiredPaidPartial, 2000}, - {MarkedCompleted, 2008}, + {Created, InvoiceEventCode.Created}, + {ReceivedPayment, InvoiceEventCode.ReceivedPayment}, + {PaidInFull, InvoiceEventCode.PaidInFull}, + {Expired, InvoiceEventCode.Expired}, + {Confirmed, InvoiceEventCode.Confirmed}, + {Completed, InvoiceEventCode.Completed}, + {MarkedInvalid, InvoiceEventCode.MarkedInvalid}, + {FailedToConfirm, InvoiceEventCode.FailedToConfirm}, + {PaidAfterExpiration, InvoiceEventCode.PaidAfterExpiration}, + {ExpiredPaidPartial, InvoiceEventCode.ExpiredPaidPartial}, + {MarkedCompleted, InvoiceEventCode.MarkedCompleted}, }; public InvoiceEvent(InvoiceEntity invoice, string name) @@ -40,14 +55,14 @@ namespace BTCPayServer.Events } public InvoiceEntity Invoice { get; set; } - public int EventCode { get; set; } + public InvoiceEventCode EventCode { get; set; } public string Name { get; set; } public PaymentEntity Payment { get; set; } public override string ToString() { - return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})"; + return $"Invoice {Invoice.Id} new event: {Name} ({(int)EventCode})"; } } } diff --git a/BTCPayServer/Events/InvoiceIPNEvent.cs b/BTCPayServer/Events/InvoiceIPNEvent.cs index 96922b8f9..b4423586a 100644 --- a/BTCPayServer/Events/InvoiceIPNEvent.cs +++ b/BTCPayServer/Events/InvoiceIPNEvent.cs @@ -1,6 +1,6 @@ namespace BTCPayServer.Events { - public class InvoiceIPNEvent + public class InvoiceIPNEvent : IHasInvoiceId { public InvoiceIPNEvent(string invoiceId, int? eventCode, string name) { diff --git a/BTCPayServer/Events/InvoiceStopWatchedEvent.cs b/BTCPayServer/Events/InvoiceStopWatchedEvent.cs index ef169ce21..bff5150b5 100644 --- a/BTCPayServer/Events/InvoiceStopWatchedEvent.cs +++ b/BTCPayServer/Events/InvoiceStopWatchedEvent.cs @@ -1,6 +1,6 @@ namespace BTCPayServer.Events { - public class InvoiceStopWatchedEvent + public class InvoiceStopWatchedEvent : IHasInvoiceId { public InvoiceStopWatchedEvent(string invoiceId) { diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 4ef2bf160..9896c2240 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -9,6 +9,7 @@ using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Lightning; diff --git a/BTCPayServer/Extensions/WebHostExtensions.cs b/BTCPayServer/Extensions/WebHostExtensions.cs index 4014c8967..6703be332 100644 --- a/BTCPayServer/Extensions/WebHostExtensions.cs +++ b/BTCPayServer/Extensions/WebHostExtensions.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Hosting; using Microsoft.Extensions.DependencyInjection; diff --git a/BTCPayServer/HostedServices/EventHostedServiceBase.cs b/BTCPayServer/HostedServices/EventHostedServiceBase.cs index d878a3211..b1de13f80 100644 --- a/BTCPayServer/HostedServices/EventHostedServiceBase.cs +++ b/BTCPayServer/HostedServices/EventHostedServiceBase.cs @@ -12,10 +12,11 @@ namespace BTCPayServer.HostedServices public class EventHostedServiceBase : IHostedService { private readonly EventAggregator _EventAggregator; + public EventAggregator EventAggregator => _EventAggregator; private List _Subscriptions; private CancellationTokenSource _Cts; - + public CancellationToken CancellationToken => _Cts.Token; public EventHostedServiceBase(EventAggregator eventAggregator) { _EventAggregator = eventAggregator; @@ -60,6 +61,11 @@ namespace BTCPayServer.HostedServices _Subscriptions.Add(_EventAggregator.Subscribe(e => _Events.Writer.TryWrite(e))); } + protected void PushEvent(object obj) + { + _Events.Writer.TryWrite(obj); + } + public virtual Task StartAsync(CancellationToken cancellationToken) { _Subscriptions = new List(); diff --git a/BTCPayServer/HostedServices/InvoiceEventSaverService.cs b/BTCPayServer/HostedServices/InvoiceEventSaverService.cs new file mode 100644 index 000000000..3e6a0c528 --- /dev/null +++ b/BTCPayServer/HostedServices/InvoiceEventSaverService.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Services.Invoices; +using NBXplorer; + +namespace BTCPayServer.HostedServices +{ + public class InvoiceEventSaverService : EventHostedServiceBase + { + private readonly InvoiceRepository _invoiceRepository; + public InvoiceEventSaverService(EventAggregator eventAggregator, InvoiceRepository invoiceRepository) : base( + eventAggregator) + { + _invoiceRepository = invoiceRepository; + } + + protected override void SubscribeToEvents() + { + Subscribe(); + Subscribe(); + Subscribe(); + Subscribe(); + } + + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + var e = (IHasInvoiceId)evt; + var severity = InvoiceEventData.EventSeverity.Info; + if (evt is InvoiceIPNEvent ipn) + { + severity = string.IsNullOrEmpty(ipn.Error) ? InvoiceEventData.EventSeverity.Success + : InvoiceEventData.EventSeverity.Error; + } + await _invoiceRepository.AddInvoiceEvent(e.InvoiceId, e, severity); + } + } +} diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index eb8fefee4..50f1ca557 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -86,7 +86,7 @@ namespace BTCPayServer.HostedServices }, Event = new InvoicePaymentNotificationEvent() { - Code = invoiceEvent.EventCode, + Code = (int)invoiceEvent.EventCode, Name = invoiceEvent.Name }, ExtendedNotification = extendedNotification, @@ -314,11 +314,6 @@ namespace BTCPayServer.HostedServices var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id); if (invoice == null) return; - List tasks = new List(); - - // Awaiting this later help make sure invoices should arrive in order - tasks.Add(SaveEvent(invoice.Id, e, InvoiceEventData.EventSeverity.Info)); - // 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) { @@ -344,32 +339,9 @@ namespace BTCPayServer.HostedServices _ = Notify(invoice, e, true); } })); - - - leases.Add(_EventAggregator.Subscribe(async e => - { - await SaveEvent(e.InvoiceId, e, InvoiceEventData.EventSeverity.Info); - })); - - - leases.Add(_EventAggregator.Subscribe(async e => - { - await SaveEvent(e.InvoiceId, e, InvoiceEventData.EventSeverity.Info); - })); - - leases.Add(_EventAggregator.Subscribe(async e => - { - await SaveEvent(e.InvoiceId, e, string.IsNullOrEmpty(e.Error)? InvoiceEventData.EventSeverity.Success: InvoiceEventData.EventSeverity.Error); - })); - return Task.CompletedTask; } - private Task SaveEvent(string invoiceId, object evt, InvoiceEventData.EventSeverity severity) - { - return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt, severity); - } - public Task StopAsync(CancellationToken cancellationToken) { leases.Dispose(); diff --git a/BTCPayServer/HostedServices/WebhookNotificationManager.cs b/BTCPayServer/HostedServices/WebhookNotificationManager.cs new file mode 100644 index 000000000..319936f08 --- /dev/null +++ b/BTCPayServer/HostedServices/WebhookNotificationManager.cs @@ -0,0 +1,320 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.NetworkInformation; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Amazon.Runtime.Internal; +using Amazon.S3.Model; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Logging; +using BTCPayServer.Services.Stores; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using NBitcoin; +using NBitcoin.DataEncoders; +using NBitcoin.Secp256k1; +using NBitpayClient; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using Org.BouncyCastle.Ocsp; +using TwentyTwenty.Storage; + +namespace BTCPayServer.HostedServices +{ + /// + /// This class send webhook notifications + /// It also make sure the events sent to a webhook are sent in order to the webhook + /// + public class WebhookNotificationManager : EventHostedServiceBase + { + readonly Encoding UTF8 = new UTF8Encoding(false); + public readonly static JsonSerializerSettings DefaultSerializerSettings; + static WebhookNotificationManager() + { + DefaultSerializerSettings = WebhookEvent.DefaultSerializerSettings; + } + public const string OnionNamedClient = "greenfield-webhook.onion"; + public const string ClearnetNamedClient = "greenfield-webhook.clearnet"; + private HttpClient GetClient(Uri uri) + { + return HttpClientFactory.CreateClient(uri.IsOnion() ? OnionNamedClient : ClearnetNamedClient); + } + class WebhookDeliveryRequest + { + public WebhookEvent WebhookEvent; + public Data.WebhookDeliveryData Delivery; + public WebhookBlob WebhookBlob; + public string WebhookId; + public WebhookDeliveryRequest(string webhookId, WebhookEvent webhookEvent, Data.WebhookDeliveryData delivery, WebhookBlob webhookBlob) + { + WebhookId = webhookId; + WebhookEvent = webhookEvent; + Delivery = delivery; + WebhookBlob = webhookBlob; + } + } + Dictionary> _InvoiceEventsByWebhookId = new Dictionary>(); + public StoreRepository StoreRepository { get; } + public IHttpClientFactory HttpClientFactory { get; } + + public WebhookNotificationManager(EventAggregator eventAggregator, + StoreRepository storeRepository, + IHttpClientFactory httpClientFactory) : base(eventAggregator) + { + StoreRepository = storeRepository; + HttpClientFactory = httpClientFactory; + } + + protected override void SubscribeToEvents() + { + Subscribe(); + } + + public async Task Redeliver(string deliveryId) + { + var deliveryRequest = await CreateRedeliveryRequest(deliveryId); + EnqueueDelivery(deliveryRequest); + return deliveryRequest.Delivery.Id; + } + + private async Task CreateRedeliveryRequest(string deliveryId) + { + using var ctx = StoreRepository.CreateDbContext(); + var webhookDelivery = await ctx.WebhookDeliveries.AsNoTracking() + .Where(o => o.Id == deliveryId) + .Select(o => new + { + Webhook = o.Webhook, + Delivery = o + }) + .FirstOrDefaultAsync(); + if (webhookDelivery is null) + return null; + var oldDeliveryBlob = webhookDelivery.Delivery.GetBlob(); + var newDelivery = NewDelivery(); + newDelivery.WebhookId = webhookDelivery.Webhook.Id; + var newDeliveryBlob = new WebhookDeliveryBlob(); + newDeliveryBlob.Request = oldDeliveryBlob.Request; + var webhookEvent = newDeliveryBlob.ReadRequestAs(); + webhookEvent.DeliveryId = newDelivery.Id; + webhookEvent.WebhookId = webhookDelivery.Webhook.Id; + // if we redelivered a redelivery, we still want the initial delivery here + webhookEvent.OrignalDeliveryId ??= deliveryId; + webhookEvent.IsRedelivery = true; + newDeliveryBlob.Request = ToBytes(webhookEvent); + newDelivery.SetBlob(newDeliveryBlob); + return new WebhookDeliveryRequest(webhookDelivery.Webhook.Id, webhookEvent, newDelivery, webhookDelivery.Webhook.GetBlob()); + } + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + if (evt is InvoiceEvent invoiceEvent) + { + var webhooks = await StoreRepository.GetWebhooks(invoiceEvent.Invoice.StoreId); + foreach (var webhook in webhooks) + { + var webhookBlob = webhook.GetBlob(); + if (!(GetWebhookEvent(invoiceEvent) is WebhookInvoiceEvent webhookEvent)) + continue; + if (!ShouldDeliver(webhookEvent.Type, webhookBlob)) + continue; + Data.WebhookDeliveryData delivery = NewDelivery(); + delivery.WebhookId = webhook.Id; + webhookEvent.InvoiceId = invoiceEvent.InvoiceId; + webhookEvent.StoreId = invoiceEvent.Invoice.StoreId; + webhookEvent.DeliveryId = delivery.Id; + webhookEvent.WebhookId = webhook.Id; + webhookEvent.OrignalDeliveryId = delivery.Id; + webhookEvent.IsRedelivery = false; + webhookEvent.Timestamp = delivery.Timestamp; + var context = new WebhookDeliveryRequest(webhook.Id, webhookEvent, delivery, webhookBlob); + EnqueueDelivery(context); + } + } + } + + private void EnqueueDelivery(WebhookDeliveryRequest context) + { + if (_InvoiceEventsByWebhookId.TryGetValue(context.WebhookId, out var channel)) + { + if (channel.Writer.TryWrite(context)) + return; + } + channel = Channel.CreateUnbounded(); + _InvoiceEventsByWebhookId.Add(context.WebhookId, channel); + channel.Writer.TryWrite(context); + _ = Process(context.WebhookId, channel); + } + + private WebhookInvoiceEvent GetWebhookEvent(InvoiceEvent invoiceEvent) + { + var eventCode = invoiceEvent.EventCode; + switch (eventCode) + { + case InvoiceEventCode.Completed: + return null; + case InvoiceEventCode.Confirmed: + case InvoiceEventCode.MarkedCompleted: + return new WebhookInvoiceConfirmedEvent(WebhookEventType.InvoiceConfirmed) + { + ManuallyMarked = eventCode == InvoiceEventCode.MarkedCompleted + }; + case InvoiceEventCode.Created: + return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated); + case InvoiceEventCode.Expired: + case InvoiceEventCode.ExpiredPaidPartial: + return new WebhookInvoiceExpiredEvent(WebhookEventType.InvoiceExpired) + { + PartiallyPaid = eventCode == InvoiceEventCode.ExpiredPaidPartial + }; + case InvoiceEventCode.FailedToConfirm: + case InvoiceEventCode.MarkedInvalid: + return new WebhookInvoiceInvalidEvent(WebhookEventType.InvoiceInvalid) + { + ManuallyMarked = eventCode == InvoiceEventCode.MarkedInvalid + }; + case InvoiceEventCode.PaidInFull: + case InvoiceEventCode.PaidAfterExpiration: + 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: + return null; + } + } + + private async Task Process(string id, Channel channel) + { + await foreach (var originalCtx in channel.Reader.ReadAllAsync()) + { + try + { + var ctx = originalCtx; + var wh = (await StoreRepository.GetWebhook(ctx.WebhookId)).GetBlob(); + if (!ShouldDeliver(ctx.WebhookEvent.Type, wh)) + continue; + var result = await SendDelivery(ctx); + if (ctx.WebhookBlob.AutomaticRedelivery && + !result.Success && + result.DeliveryId is string) + { + var originalDeliveryId = result.DeliveryId; + foreach (var wait in new[] + { + TimeSpan.FromSeconds(10), + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + }) + { + await Task.Delay(wait, CancellationToken); + ctx = await CreateRedeliveryRequest(originalDeliveryId); + // This may have changed + if (!ctx.WebhookBlob.AutomaticRedelivery || + !ShouldDeliver(ctx.WebhookEvent.Type, ctx.WebhookBlob)) + break; + result = await SendDelivery(ctx); + if (result.Success) + break; + } + } + } + catch when (CancellationToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + Logs.PayServer.LogError(ex, "Unexpected error when processing a webhook"); + } + } + } + + private static bool ShouldDeliver(WebhookEventType type, WebhookBlob wh) + { + return wh.Active && wh.AuthorizedEvents.Match(type); + } + + class DeliveryResult + { + public string DeliveryId { get; set; } + public bool Success { get; set; } + } + private async Task SendDelivery(WebhookDeliveryRequest ctx) + { + var uri = new Uri(ctx.WebhookBlob.Url, UriKind.Absolute); + var httpClient = GetClient(uri); + using var request = new HttpRequestMessage(); + request.RequestUri = uri; + request.Method = HttpMethod.Post; + byte[] bytes = ToBytes(ctx.WebhookEvent); + var content = new ByteArrayContent(bytes); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + using var hmac = new System.Security.Cryptography.HMACSHA256(UTF8.GetBytes(ctx.WebhookBlob.Secret ?? string.Empty)); + var sig = Encoders.Hex.EncodeData(hmac.ComputeHash(bytes)); + content.Headers.Add("BTCPay-Sig", $"sha256={sig}"); + request.Content = content; + var deliveryBlob = ctx.Delivery.Blob is null ? new WebhookDeliveryBlob() : ctx.Delivery.GetBlob(); + deliveryBlob.Request = bytes; + try + { + using var response = await httpClient.SendAsync(request, CancellationToken); + if (!response.IsSuccessStatusCode) + { + deliveryBlob.Status = WebhookDeliveryStatus.HttpError; + deliveryBlob.ErrorMessage = $"HTTP Error Code {(int)response.StatusCode}"; + } + else + { + deliveryBlob.Status = WebhookDeliveryStatus.HttpSuccess; + } + deliveryBlob.HttpCode = (int)response.StatusCode; + } + catch (Exception ex) when (!CancellationToken.IsCancellationRequested) + { + deliveryBlob.Status = WebhookDeliveryStatus.Failed; + deliveryBlob.ErrorMessage = ex.Message; + } + ctx.Delivery.SetBlob(deliveryBlob); + await StoreRepository.AddWebhookDelivery(ctx.Delivery); + return new DeliveryResult() { Success = deliveryBlob.ErrorMessage is null, DeliveryId = ctx.Delivery.Id }; + } + + private byte[] ToBytes(WebhookEvent webhookEvent) + { + var str = JsonConvert.SerializeObject(webhookEvent, Formatting.Indented, DefaultSerializerSettings); + var bytes = UTF8.GetBytes(str); + return bytes; + } + + private static Data.WebhookDeliveryData NewDelivery() + { + var delivery = new Data.WebhookDeliveryData(); + delivery.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); + delivery.Timestamp = DateTimeOffset.UtcNow; + return delivery; + } + } +} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 6b34bece1..5100daa72 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -1,8 +1,10 @@ using System; using System.IO; using System.Threading; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Configuration; -using BTCPayServer.Contracts; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.HostedServices; @@ -109,20 +111,19 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(o => + services.TryAddSingleton(o => { var opts = o.GetRequiredService(); - ApplicationDbContextFactory dbContext = null; if (!string.IsNullOrEmpty(opts.PostgresConnectionString)) { Logs.Configuration.LogInformation($"Postgres DB used"); - dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString); + return new DatabaseOptions(DatabaseType.Postgres, opts.PostgresConnectionString); } else if (!string.IsNullOrEmpty(opts.MySQLConnectionString)) { Logs.Configuration.LogInformation($"MySQL DB used"); Logs.Configuration.LogWarning("MySQL is not widely tested and should be considered experimental, we advise you to use postgres instead."); - dbContext = new ApplicationDbContextFactory(DatabaseType.MySQL, opts.MySQLConnectionString); + return new DatabaseOptions(DatabaseType.MySQL, opts.MySQLConnectionString); } else if (!string.IsNullOrEmpty(opts.SQLiteFileName)) { @@ -131,15 +132,14 @@ namespace BTCPayServer.Hosting : Path.Combine(opts.DataDir, opts.SQLiteFileName)); Logs.Configuration.LogInformation($"SQLite DB used"); Logs.Configuration.LogWarning("SQLite is not widely tested and should be considered experimental, we advise you to use postgres instead."); - dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr); + return new DatabaseOptions(DatabaseType.Sqlite, connStr); } else { throw new ConfigException("No database option was configured."); } - - return dbContext; }); + services.AddSingleton(); services.TryAddSingleton(o => { @@ -149,6 +149,7 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.AddSingleton(); + services.AddSingleton(provider => provider.GetService()); services.TryAddTransient(); services.TryAddSingleton(o => { @@ -216,7 +217,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); - + services.AddSingleton(); + services.AddSingleton(o => o.GetRequiredService()); services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); @@ -234,6 +236,7 @@ namespace BTCPayServer.Hosting services.AddScoped(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/BTCPayServer/Hosting/BlockExplorerLinkStartupTask.cs b/BTCPayServer/Hosting/BlockExplorerLinkStartupTask.cs index 8099a3ff7..2f6b8b855 100644 --- a/BTCPayServer/Hosting/BlockExplorerLinkStartupTask.cs +++ b/BTCPayServer/Hosting/BlockExplorerLinkStartupTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Services; namespace BTCPayServer.Hosting diff --git a/BTCPayServer/Hosting/MigrationStartupTask.cs b/BTCPayServer/Hosting/MigrationStartupTask.cs index a20b7011c..1bce3c1ea 100644 --- a/BTCPayServer/Hosting/MigrationStartupTask.cs +++ b/BTCPayServer/Hosting/MigrationStartupTask.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Logging; diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 2052f3365..c27d1dd2c 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -79,9 +79,12 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } + + public List Deliveries { get; set; } = new List(); public string TaxIncluded { get; set; } public string TransactionSpeed { get; set; } + public string StoreId { get; set; } public object StoreName { get; diff --git a/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs b/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs index 4183c9ed7..8f5a0a19f 100644 --- a/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs +++ b/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; namespace BTCPayServer.Models.NotificationViewModels { diff --git a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs index 8d9bbbea6..7a8648101 100644 --- a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs @@ -31,6 +31,31 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "Default payment method on checkout")] public string DefaultPaymentMethod { get; set; } + + + [Display(Name = "Requires a refund email")] + public bool RequiresRefundEmail { get; set; } + + [Display(Name = "Display lightning payment amounts in Satoshis")] + public bool LightningAmountInSatoshi { get; set; } + + [Display(Name = "Add hop hints for private channels to the lightning invoice")] + public bool LightningPrivateRouteHints { get; set; } + + [Display(Name = "Include lightning invoice fallback to on-chain BIP21 payment url")] + public bool OnChainWithLnInvoiceFallback { get; set; } + + [Display(Name = "Redirect invoice to redirect url automatically after paid")] + public bool RedirectAutomatically { get; set; } + + [Display(Name = "Show recommended fee")] + public bool ShowRecommendedFee { get; set; } + + [Display(Name = "Recommended fee confirmation target blocks")] + [Range(1, double.PositiveInfinity)] + public int RecommendedFeeBlockTarget { get; set; } + + [Display(Name = "Default language on checkout")] public string DefaultLang { get; set; } @@ -42,25 +67,6 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "Custom HTML title to display on Checkout page")] public string HtmlTitle { get; set; } - [Display(Name = "Requires a refund email")] - public bool RequiresRefundEmail { get; set; } - - [Display(Name = "Show recommended fee")] - public bool ShowRecommendedFee { get; set; } - - [Display(Name = "Recommended fee confirmation target blocks")] - [Range(1, double.PositiveInfinity)] - public int RecommendedFeeBlockTarget { get; set; } - - [Display(Name = "Display lightning payment amounts in Satoshis")] - public bool LightningAmountInSatoshi { get; set; } - - [Display(Name = "Add hop hints for private channels to the lightning invoice")] - public bool LightningPrivateRouteHints { get; set; } - - [Display(Name = "Redirect invoice to redirect url automatically after paid")] - public bool RedirectAutomatically { get; set; } - public List PaymentMethodCriteria { get; set; } } diff --git a/BTCPayServer/Models/StoreViewModels/EditWebhookViewModel.cs b/BTCPayServer/Models/StoreViewModels/EditWebhookViewModel.cs new file mode 100644 index 000000000..3b31b261c --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/EditWebhookViewModel.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.Validation; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class DeliveryViewModel + { + public DeliveryViewModel() + { + + } + public DeliveryViewModel(Data.WebhookDeliveryData s) + { + var blob = s.GetBlob(); + Id = s.Id; + Success = blob.Status == WebhookDeliveryStatus.HttpSuccess; + ErrorMessage = blob.ErrorMessage ?? "Success"; + Time = s.Timestamp; + Type = blob.ReadRequestAs().Type; + WebhookId = s.Id; + } + public string Id { get; set; } + public DateTimeOffset Time { get; set; } + public WebhookEventType Type { get; private set; } + public string WebhookId { get; set; } + public bool Success { get; set; } + public string ErrorMessage { get; set; } + } + public class EditWebhookViewModel + { + public EditWebhookViewModel() + { + + } + public EditWebhookViewModel(WebhookBlob blob) + { + Active = blob.Active; + AutomaticRedelivery = blob.AutomaticRedelivery; + Everything = blob.AuthorizedEvents.Everything; + Events = blob.AuthorizedEvents.SpecificEvents; + PayloadUrl = blob.Url; + Secret = blob.Secret; + IsNew = false; + } + public bool IsNew { get; set; } + public bool Active { get; set; } + public bool AutomaticRedelivery { get; set; } + public bool Everything { get; set; } + public WebhookEventType[] Events { get; set; } = Array.Empty(); + [Uri] + [Required] + public string PayloadUrl { get; set; } + [MaxLength(64)] + public string Secret { get; set; } + + public List Deliveries { get; set; } = new List(); + + public WebhookBlob CreateBlob() + { + return new WebhookBlob() + { + Active = Active, + Secret = Secret, + AutomaticRedelivery = AutomaticRedelivery, + Url = new Uri(PayloadUrl, UriKind.Absolute).AbsoluteUri, + AuthorizedEvents = new AuthorizedWebhookEvents() + { + Everything = Everything, + SpecificEvents = Events + } + }; + } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index ebe87c67f..93d2d1a5c 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -30,6 +30,7 @@ namespace BTCPayServer.Models.StoreViewModels } public bool CanDelete { get; set; } + [Display(Name = "Store ID")] public string Id { get; set; } [Display(Name = "Store Name")] [Required] diff --git a/BTCPayServer/Models/StoreViewModels/WebhooksViewModel.cs b/BTCPayServer/Models/StoreViewModels/WebhooksViewModel.cs new file mode 100644 index 000000000..b46bec728 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/WebhooksViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class WebhooksViewModel + { + public class WebhookViewModel + { + public string Id { get; set; } + public string Url { get; set; } + } + public WebhookViewModel[] Webhooks { get; set; } + } +} diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index 366757779..bef7640b4 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -52,9 +52,33 @@ namespace BTCPayServer.Payments.Bitcoin model.ShowRecommendedFee = storeBlob.ShowRecommendedFee; model.FeeRate = ((BitcoinLikeOnChainPaymentMethod) paymentMethod.GetPaymentMethodDetails()).GetFeeRate(); model.PaymentMethodName = GetPaymentMethodName(network); - model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21; - model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BIP21; + + + var lightningFallback = ""; + if (storeBlob.OnChainWithLnInvoiceFallback) + { + var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a => + a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike)); + if (!String.IsNullOrEmpty(lightningInfo?.PaymentUrls?.BOLT11)) + lightningFallback = "&" + lightningInfo.PaymentUrls.BOLT11.Replace("lightning:", "lightning=", StringComparison.OrdinalIgnoreCase); + } + + model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21 + lightningFallback; + // We're trying to make as many characters uppercase to make QR smaller + // Ref: https://github.com/btcpayserver/btcpayserver/pull/2060#issuecomment-723828348 + model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BIP21 + .Replace("bitcoin:", "BITCOIN:", StringComparison.OrdinalIgnoreCase) + + lightningFallback.ToUpperInvariant().Replace("LIGHTNING=", "lightning=", StringComparison.OrdinalIgnoreCase); + + if (bech32Prefixes.Any(a => model.BtcAddress.StartsWith(a, StringComparison.OrdinalIgnoreCase))) + { + model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrlQR.Replace( + $"BITCOIN:{model.BtcAddress}", $"BITCOIN:{model.BtcAddress.ToUpperInvariant()}", + StringComparison.OrdinalIgnoreCase + ); + } } + private static string[] bech32Prefixes = new[] { "bc1", "tb1", "bcrt1" }; public override string GetCryptoImage(PaymentMethodId paymentMethodId) { diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 01f28a882..002d1c002 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -82,7 +82,7 @@ namespace BTCPayServer.Payments.Lightning } catch (OperationCanceledException) when (cts.IsCancellationRequested) { - throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely manner"); + throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner"); } catch (Exception ex) { @@ -90,7 +90,7 @@ namespace BTCPayServer.Payments.Lightning } } var nodeInfo = await test; - return new LightningLikePaymentMethodDetails() + return new LightningLikePaymentMethodDetails { BOLT11 = lightningInvoice.BOLT11, InvoiceId = lightningInvoice.Id, @@ -101,19 +101,19 @@ namespace BTCPayServer.Payments.Lightning public async Task GetNodeInfo(bool preferOnion, LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network) { if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) - throw new PaymentMethodUnavailableException($"Full node not available"); + throw new PaymentMethodUnavailableException("Full node not available"); using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT)) { var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network); - LightningNodeInformation info = null; + LightningNodeInformation info; try { info = await client.GetInfo(cts.Token); } catch (OperationCanceledException) when (cts.IsCancellationRequested) { - throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely manner"); + throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner"); } catch (Exception ex) { @@ -122,7 +122,7 @@ namespace BTCPayServer.Payments.Lightning var nodeInfo = info.NodeInfoList.FirstOrDefault(i => i.IsTor == preferOnion) ?? info.NodeInfoList.FirstOrDefault(); if (nodeInfo == null) { - throw new PaymentMethodUnavailableException($"No lightning node public address has been configured"); + throw new PaymentMethodUnavailableException("No lightning node public address has been configured"); } var blocksGap = summary.Status.ChainHeight - info.BlockHeight; diff --git a/BTCPayServer/Payments/PayJoin/PayJoinExtensions.cs b/BTCPayServer/Payments/PayJoin/PayJoinExtensions.cs index 0e5f234ad..d3c1d85b4 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinExtensions.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinExtensions.cs @@ -19,6 +19,9 @@ namespace BTCPayServer.Payments.PayJoin services.AddHttpClient(PayjoinClient.PayjoinOnionNamedClient) .ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true) .ConfigurePrimaryHttpMessageHandler(); + services.AddHttpClient(WebhookNotificationManager.OnionNamedClient) + .ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true) + .ConfigurePrimaryHttpMessageHandler(); } } } diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index 0c509540c..4a54d4e80 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -12,15 +12,13 @@ namespace BTCPayServer.Payments public class BitcoinPaymentType : PaymentType { public static BitcoinPaymentType Instance { get; } = new BitcoinPaymentType(); - private BitcoinPaymentType() - { - - } + + private BitcoinPaymentType() { } public override string ToPrettyString() => "On-Chain"; public override string GetId() => "BTCLike"; + public override string GetBadge() => "🔗"; public override string ToStringNormalized() => "OnChain"; - public override string GetBadge() => "🔗"; public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { @@ -72,8 +70,8 @@ namespace BTCPayServer.Payments public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) { - var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue); - + var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue); + if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true) { bip21 += $"&{PayjoinClient.BIP21EndpointKey}={serverUri.WithTrailingSlash()}{network.CryptoCode}/{PayjoinClient.BIP21EndpointKey}"; diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs index e63848580..7400a5a9f 100644 --- a/BTCPayServer/Payments/PaymentTypes.Lightning.cs +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -11,18 +11,13 @@ namespace BTCPayServer.Payments { public static LightningPaymentType Instance { get; } = new LightningPaymentType(); - private LightningPaymentType() - { - } + private LightningPaymentType() { } public override string ToPrettyString() => "Off-Chain"; public override string GetId() => "LightningLike"; - public override string GetBadge() => "⚡"; + public override string GetBadge() => "⚡"; + public override string ToStringNormalized() => "LightningNetwork"; - public override string ToStringNormalized() - { - return "LightningNetwork"; - } public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { return ((BTCPayNetwork)network)?.ToObject(str); @@ -35,7 +30,7 @@ namespace BTCPayServer.Payments public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str) { - return JsonConvert.DeserializeObject(str); + return JsonConvert.DeserializeObject(str); } public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details) @@ -57,8 +52,10 @@ namespace BTCPayServer.Payments public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) { - return - $"lightning:{paymentMethodDetails.GetPaymentDestination().ToUpperInvariant().Replace("LIGHTNING:", "", StringComparison.InvariantCultureIgnoreCase)}"; + var lnInvoiceTrimmedOfScheme = paymentMethodDetails.GetPaymentDestination().ToLowerInvariant() + .Replace("lightning:", "", StringComparison.InvariantCultureIgnoreCase); + + return $"lightning:{lnInvoiceTrimmedOfScheme}"; } public override string InvoiceViewPaymentPartialName { get; } = "Lightning/ViewLightningLikePaymentData"; diff --git a/BTCPayServer/Plugins/BTCPayServerPlugin.cs b/BTCPayServer/Plugins/BTCPayServerPlugin.cs index dc0c59272..ce3b9b815 100644 --- a/BTCPayServer/Plugins/BTCPayServerPlugin.cs +++ b/BTCPayServer/Plugins/BTCPayServerPlugin.cs @@ -1,3 +1,4 @@ +using BTCPayServer.Abstractions.Models; using BTCPayServer.Models; namespace BTCPayServer.Plugins diff --git a/BTCPayServer/Plugins/PluginManager.cs b/BTCPayServer/Plugins/PluginManager.cs index 48c753491..264c6d231 100644 --- a/BTCPayServer/Plugins/PluginManager.cs +++ b/BTCPayServer/Plugins/PluginManager.cs @@ -5,8 +5,8 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Configuration; -using BTCPayServer.Contracts; using McMaster.NETCore.Plugins; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/BTCPayServer/Plugins/PluginService.cs b/BTCPayServer/Plugins/PluginService.cs index 79671887e..7eec0e369 100644 --- a/BTCPayServer/Plugins/PluginService.cs +++ b/BTCPayServer/Plugins/PluginService.cs @@ -6,8 +6,8 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Configuration; -using BTCPayServer.Contracts; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -15,18 +15,21 @@ using Newtonsoft.Json; namespace BTCPayServer.Plugins { - public class PluginService + public class PluginService: IPluginHookService { private readonly BTCPayServerOptions _btcPayServerOptions; private readonly HttpClient _githubClient; - + private readonly IEnumerable _actions; + private readonly IEnumerable _filters; public PluginService(IEnumerable btcPayServerPlugins, - IHttpClientFactory httpClientFactory, BTCPayServerOptions btcPayServerOptions) + IHttpClientFactory httpClientFactory, BTCPayServerOptions btcPayServerOptions,IEnumerable actions, IEnumerable filters) { LoadedPlugins = btcPayServerPlugins; _githubClient = httpClientFactory.CreateClient(); _githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1")); _btcPayServerOptions = btcPayServerOptions; + _actions = actions; + _filters = filters; } public IEnumerable LoadedPlugins { get; } @@ -128,5 +131,27 @@ namespace BTCPayServer.Plugins { PluginManager.CancelCommands(_btcPayServerOptions.PluginDir, plugin); } + + public async Task ApplyAction(string hook, object args) + { + var filters = _actions + .Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList(); + foreach (IPluginHookAction pluginHookFilter in filters) + { + await pluginHookFilter.Execute(args); + } + } + + public async Task ApplyFilter(string hook, object args) + { + var filters = _filters + .Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList(); + foreach (IPluginHookFilter pluginHookFilter in filters) + { + args = await pluginHookFilter.Execute(args); + } + + return args; + } } } diff --git a/BTCPayServer/Security/AuthenticationExtensions.cs b/BTCPayServer/Security/AuthenticationExtensions.cs index 5c6aa80c6..0fcfc6de7 100644 --- a/BTCPayServer/Security/AuthenticationExtensions.cs +++ b/BTCPayServer/Security/AuthenticationExtensions.cs @@ -1,3 +1,4 @@ +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Security.Bitpay; using Microsoft.AspNetCore.Authentication; diff --git a/BTCPayServer/Security/CookieAuthorizationHandler.cs b/BTCPayServer/Security/CookieAuthorizationHandler.cs index ae12981c3..998c556f7 100644 --- a/BTCPayServer/Security/CookieAuthorizationHandler.cs +++ b/BTCPayServer/Security/CookieAuthorizationHandler.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Services.Stores; diff --git a/BTCPayServer/Security/GreenField/APIKeyExtensions.cs b/BTCPayServer/Security/GreenField/APIKeyExtensions.cs index bb8136945..35a425965 100644 --- a/BTCPayServer/Security/GreenField/APIKeyExtensions.cs +++ b/BTCPayServer/Security/GreenField/APIKeyExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; diff --git a/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs b/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs index 2ba83d680..40e53649d 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs @@ -1,7 +1,8 @@ #if ALTCOINS using System.Net; using System.Net.Http; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Services; using BTCPayServer.HostedServices; using BTCPayServer.Payments; using BTCPayServer.Services.Altcoins.Ethereum.Payments; diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumSyncSummaryProvider.cs b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumSyncSummaryProvider.cs index ea929ebe7..791269c74 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumSyncSummaryProvider.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumSyncSummaryProvider.cs @@ -1,5 +1,5 @@ #if ALTCOINS -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; namespace BTCPayServer.Services.Altcoins.Ethereum.Services { diff --git a/BTCPayServer/Services/Altcoins/Ethereum/UI/EthereumConfigController.cs b/BTCPayServer/Services/Altcoins/Ethereum/UI/EthereumConfigController.cs index 39f344ae0..97df2d729 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/UI/EthereumConfigController.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/UI/EthereumConfigController.cs @@ -4,6 +4,9 @@ using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Models; diff --git a/BTCPayServer/Services/Altcoins/Ethereum/UI/EthereumLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Ethereum/UI/EthereumLikeStoreController.cs index e19c565fd..0b0a15723 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/UI/EthereumLikeStoreController.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/UI/EthereumLikeStoreController.cs @@ -4,6 +4,9 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Models; diff --git a/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs b/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs index b87841183..81755d430 100644 --- a/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs +++ b/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs @@ -1,8 +1,9 @@ #if ALTCOINS using System; using System.Linq; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Services; using BTCPayServer.Configuration; -using BTCPayServer.Contracts; using BTCPayServer.Payments; using BTCPayServer.Services.Altcoins.Monero.Configuration; using BTCPayServer.Services.Altcoins.Monero.Payments; diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroSyncSummaryProvider.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroSyncSummaryProvider.cs index b1beb68f6..910ff0ea0 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroSyncSummaryProvider.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroSyncSummaryProvider.cs @@ -1,6 +1,6 @@ #if ALTCOINS using System.Linq; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; namespace BTCPayServer.Services.Altcoins.Monero.Services { diff --git a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs index 5c9fbfc58..16179c8d9 100644 --- a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs +++ b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs @@ -7,6 +7,9 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Filters; diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 4f0af911e..eebfc89aa 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -36,73 +36,34 @@ namespace BTCPayServer.Services.Invoices } public string OrderId { get; set; } [JsonProperty(PropertyName = "buyerName")] - public string BuyerName - { - get; set; - } + public string BuyerName { get; set; } [JsonProperty(PropertyName = "buyerEmail")] - public string BuyerEmail - { - get; set; - } + public string BuyerEmail { get; set; } [JsonProperty(PropertyName = "buyerCountry")] - public string BuyerCountry - { - get; set; - } + public string BuyerCountry { get; set; } [JsonProperty(PropertyName = "buyerZip")] - public string BuyerZip - { - get; set; - } + public string BuyerZip { get; set; } [JsonProperty(PropertyName = "buyerState")] - public string BuyerState - { - get; set; - } + public string BuyerState { get; set; } [JsonProperty(PropertyName = "buyerCity")] - public string BuyerCity - { - get; set; - } + public string BuyerCity { get; set; } [JsonProperty(PropertyName = "buyerAddress2")] - public string BuyerAddress2 - { - get; set; - } + public string BuyerAddress2 { get; set; } [JsonProperty(PropertyName = "buyerAddress1")] - public string BuyerAddress1 - { - get; set; - } + public string BuyerAddress1 { get; set; } [JsonProperty(PropertyName = "buyerPhone")] - public string BuyerPhone - { - get; set; - } + public string BuyerPhone { get; set; } [JsonProperty(PropertyName = "itemDesc")] - public string ItemDesc - { - get; set; - } + public string ItemDesc { get; set; } [JsonProperty(PropertyName = "itemCode")] - public string ItemCode - { - get; set; - } + public string ItemCode { get; set; } [JsonProperty(PropertyName = "physical")] - public bool? Physical - { - get; set; - } + public bool? Physical { get; set; } [JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] - public decimal? TaxIncluded - { - get; set; - } + public decimal? TaxIncluded { get; set; } public string PosData { get; set; } [JsonExtensionData] public IDictionary AdditionalData { get; set; } @@ -122,133 +83,64 @@ namespace BTCPayServer.Services.Invoices class BuyerInformation { [JsonProperty(PropertyName = "buyerName")] - public string BuyerName - { - get; set; - } + public string BuyerName { get; set; } [JsonProperty(PropertyName = "buyerEmail")] - public string BuyerEmail - { - get; set; - } + public string BuyerEmail { get; set; } [JsonProperty(PropertyName = "buyerCountry")] - public string BuyerCountry - { - get; set; - } + public string BuyerCountry { get; set; } [JsonProperty(PropertyName = "buyerZip")] - public string BuyerZip - { - get; set; - } + public string BuyerZip { get; set; } [JsonProperty(PropertyName = "buyerState")] - public string BuyerState - { - get; set; - } + public string BuyerState { get; set; } [JsonProperty(PropertyName = "buyerCity")] - public string BuyerCity - { - get; set; - } + public string BuyerCity { get; set; } [JsonProperty(PropertyName = "buyerAddress2")] - public string BuyerAddress2 - { - get; set; - } + public string BuyerAddress2 { get; set; } [JsonProperty(PropertyName = "buyerAddress1")] - public string BuyerAddress1 - { - get; set; - } + public string BuyerAddress1 { get; set; } [JsonProperty(PropertyName = "buyerPhone")] - public string BuyerPhone - { - get; set; - } + public string BuyerPhone { get; set; } } + class ProductInformation { [JsonProperty(PropertyName = "itemDesc")] - public string ItemDesc - { - get; set; - } + public string ItemDesc { get; set; } [JsonProperty(PropertyName = "itemCode")] - public string ItemCode - { - get; set; - } + public string ItemCode { get; set; } [JsonProperty(PropertyName = "physical")] - public bool Physical - { - get; set; - } + public bool Physical { get; set; } [JsonProperty(PropertyName = "price")] - public decimal Price - { - get; set; - } + public decimal Price { get; set; } [JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] - public decimal TaxIncluded - { - get; set; - } + public decimal TaxIncluded { get; set; } [JsonProperty(PropertyName = "currency")] - public string Currency - { - get; set; - } + public string Currency { get; set; } } + [JsonIgnore] public BTCPayNetworkProvider Networks { get; set; } public const int InternalTagSupport_Version = 1; public const int GreenfieldInvoices_Version = 2; public const int Lastest_Version = 2; public int Version { get; set; } - public string Id - { - get; set; - } - public string StoreId - { - get; set; - } + public string Id { get; set; } + public string StoreId { get; set; } - public SpeedPolicy SpeedPolicy - { - get; set; - } + public SpeedPolicy SpeedPolicy { get; set; } [Obsolete("Use GetPaymentMethod(network) instead")] - public decimal Rate - { - get; set; - } - public DateTimeOffset InvoiceTime - { - get; set; - } - public DateTimeOffset ExpirationTime - { - get; set; - } + public decimal Rate { get; set; } + public DateTimeOffset InvoiceTime { get; set; } + public DateTimeOffset ExpirationTime { get; set; } [Obsolete("Use GetPaymentMethod(network).GetPaymentMethodDetails().GetDestinationAddress() instead")] - public string DepositAddress - { - get; set; - } - - public InvoiceMetadata Metadata - { - get; - set; - } + public string DepositAddress { get; set; } + public InvoiceMetadata Metadata { get; set; } public decimal Price { get; set; } public string Currency { get; set; } @@ -267,18 +159,10 @@ namespace BTCPayServer.Services.Invoices } [Obsolete("Use GetDerivationStrategies instead")] - public string DerivationStrategy - { - get; - set; - } + public string DerivationStrategy { get; set; } [Obsolete("Use GetPaymentMethodFactories() instead")] - public string DerivationStrategies - { - get; - set; - } + public string DerivationStrategies { get; set; } public IEnumerable GetSupportedPaymentMethod(PaymentMethodId paymentMethodId) where T : ISupportedPaymentMethod { return @@ -335,28 +219,18 @@ namespace BTCPayServer.Services.Invoices } [JsonIgnore] - public InvoiceStatus Status - { - get; - set; - } + public InvoiceStatus Status { get; set; } [JsonProperty(PropertyName = "status")] [Obsolete("Use Status instead")] public string StatusString => InvoiceState.ToString(Status); [JsonIgnore] - public InvoiceExceptionStatus ExceptionStatus - { - get; set; - } + public InvoiceExceptionStatus ExceptionStatus { get; set; } [JsonProperty(PropertyName = "exceptionStatus")] [Obsolete("Use ExceptionStatus instead")] public string ExceptionStatusString => InvoiceState.ToString(ExceptionStatus); [Obsolete("Use GetPayments instead")] - public List Payments - { - get; set; - } + public List Payments { get; set; } #pragma warning disable CS0618 public List GetPayments() @@ -372,22 +246,10 @@ namespace BTCPayServer.Services.Invoices return GetPayments(network.CryptoCode); } #pragma warning restore CS0618 - public bool Refundable - { - get; - set; - } - public string RefundMail - { - get; - set; - } + public bool Refundable { get; set; } + public string RefundMail { get; set; } [JsonProperty("redirectURL")] - public string RedirectURLTemplate - { - get; - set; - } + public string RedirectURLTemplate { get; set; } [JsonIgnore] public Uri RedirectURL => FillPlaceholdersUri(RedirectURLTemplate); @@ -401,65 +263,29 @@ namespace BTCPayServer.Services.Invoices return null; } - public bool RedirectAutomatically - { - get; - set; - } + public bool RedirectAutomatically { get; set; } [Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")] - public Money TxFee - { - get; - set; - } - public bool FullNotifications - { - get; - set; - } - public string NotificationEmail - { - get; - set; - } + public Money TxFee { get; set; } + public bool FullNotifications { get; set; } + public string NotificationEmail { get; set; } [JsonProperty("notificationURL")] - public string NotificationURLTemplate - { - get; - set; - } + public string NotificationURLTemplate { get; set; } [JsonIgnore] public Uri NotificationURL => FillPlaceholdersUri(NotificationURLTemplate); - public string ServerUrl - { - get; - set; - } + public string ServerUrl { get; set; } [Obsolete("Use Set/GetPaymentMethod() instead")] [JsonProperty(PropertyName = "cryptoData")] public JObject PaymentMethod { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public DateTimeOffset MonitoringExpiration - { - get; - set; - } - public HistoricalAddressInvoiceData[] HistoricalAddresses - { - get; - set; - } + public DateTimeOffset MonitoringExpiration { get; set; } + public HistoricalAddressInvoiceData[] HistoricalAddresses { get; set; } - public HashSet AvailableAddressHashes - { - get; - set; - } + public HashSet AvailableAddressHashes { get; set; } public bool ExtendedNotifications { get; set; } public List Events { get; internal set; } public double PaymentTolerance { get; set; } @@ -559,7 +385,7 @@ namespace BTCPayServer.Services.Invoices { var minerInfo = new MinerFeeInfo(); minerInfo.TotalFee = accounting.NetworkFee.Satoshi; - minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod) details).FeeRate + minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate .GetFee(1).Satoshi; dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); cryptoInfo.PaymentUrls = new InvoicePaymentUrls() @@ -1188,7 +1014,7 @@ namespace BTCPayServer.Services.Invoices { return null; } - + paymentData.Network = Network; if (paymentData is BitcoinLikePaymentData bitcoin) { @@ -1236,9 +1062,10 @@ namespace BTCPayServer.Services.Invoices PaymentType paymentType; if (string.IsNullOrEmpty(CryptoPaymentDataType)) { - paymentType = BitcoinPaymentType.Instance;; + paymentType = BitcoinPaymentType.Instance; + ; } - else if(!PaymentTypes.TryParse(CryptoPaymentDataType, out paymentType)) + else if (!PaymentTypes.TryParse(CryptoPaymentDataType, out paymentType)) { return null; } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index d9e704d79..35a3db316 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -2,12 +2,14 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Logging; using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using DBriize; using Microsoft.EntityFrameworkCore; @@ -59,6 +61,15 @@ retry: _eventAggregator = eventAggregator; } + public async Task GetWebhookDelivery(string invoiceId, string deliveryId) + { + using var ctx = _ContextFactory.CreateContext(); + return await ctx.InvoiceWebhookDeliveries + .Where(d => d.InvoiceId == invoiceId && d.DeliveryId == deliveryId) + .Select(d => d.Delivery) + .FirstOrDefaultAsync(); + } + public InvoiceEntity CreateNewInvoice() { return new InvoiceEntity() @@ -107,6 +118,16 @@ retry: } } + public async Task> GetWebhookDeliveries(string invoiceId) + { + using var ctx = _ContextFactory.CreateContext(); + return await ctx.InvoiceWebhookDeliveries + .Where(s => s.InvoiceId == invoiceId) + .Select(s => s.Delivery) + .OrderByDescending(s => s.Timestamp) + .ToListAsync(); + } + public async Task GetAppsTaggingStore(string storeId) { if (storeId == null) diff --git a/BTCPayServer/Services/NBXSyncSummaryProvider.cs b/BTCPayServer/Services/NBXSyncSummaryProvider.cs index 460d03e10..56499b585 100644 --- a/BTCPayServer/Services/NBXSyncSummaryProvider.cs +++ b/BTCPayServer/Services/NBXSyncSummaryProvider.cs @@ -1,4 +1,4 @@ -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.HostedServices; namespace BTCPayServer.Services diff --git a/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs b/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs index 4853b0f92..a4bc08e73 100644 --- a/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Configuration; -using BTCPayServer.Contracts; using BTCPayServer.Controllers; using BTCPayServer.Events; using BTCPayServer.Models.NotificationViewModels; diff --git a/BTCPayServer/Services/Notifications/Blobs/JunkNotification.cs b/BTCPayServer/Services/Notifications/Blobs/JunkNotification.cs index c334377db..859cf6d48 100644 --- a/BTCPayServer/Services/Notifications/Blobs/JunkNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/JunkNotification.cs @@ -1,6 +1,6 @@ #if DEBUG using System.Data; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; namespace BTCPayServer.Services.Notifications.Blobs { diff --git a/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs b/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs index 1959e11e8..1dced7dcc 100644 --- a/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs @@ -1,4 +1,4 @@ -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Models.NotificationViewModels; namespace BTCPayServer.Services.Notifications.Blobs diff --git a/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs b/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs index 5708c212b..3bb47bd9f 100644 --- a/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs @@ -1,5 +1,5 @@ +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Configuration; -using BTCPayServer.Contracts; using BTCPayServer.Controllers; using BTCPayServer.Models.NotificationViewModels; using Microsoft.AspNetCore.Routing; diff --git a/BTCPayServer/Services/Notifications/INotificationHandler.cs b/BTCPayServer/Services/Notifications/INotificationHandler.cs index abaf21b1e..40a559d07 100644 --- a/BTCPayServer/Services/Notifications/INotificationHandler.cs +++ b/BTCPayServer/Services/Notifications/INotificationHandler.cs @@ -1,5 +1,5 @@ using System; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; namespace BTCPayServer.Services.Notifications { diff --git a/BTCPayServer/Services/Notifications/NotificationManager.cs b/BTCPayServer/Services/Notifications/NotificationManager.cs index 1f548396d..86505ccf2 100644 --- a/BTCPayServer/Services/Notifications/NotificationManager.cs +++ b/BTCPayServer/Services/Notifications/NotificationManager.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Components.NotificationsDropdown; -using BTCPayServer.Contracts; using BTCPayServer.Data; using BTCPayServer.Models.NotificationViewModels; using Microsoft.AspNetCore.Identity; diff --git a/BTCPayServer/Services/Notifications/NotificationSender.cs b/BTCPayServer/Services/Notifications/NotificationSender.cs index ea57999cf..86b5f65dd 100644 --- a/BTCPayServer/Services/Notifications/NotificationSender.cs +++ b/BTCPayServer/Services/Notifications/NotificationSender.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Contracts; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Data; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -97,7 +97,9 @@ namespace BTCPayServer.Services.Notifications { // Cannot specify StringComparison as EF core does not support it and would attempt client-side evaluation // ReSharper disable once CA1307 - query = query.Where(user => user.DisabledNotifications == null || !user.DisabledNotifications.Contains(term )); +#pragma warning disable CA1307 // Specify StringComparison + query = query.Where(user => user.DisabledNotifications == null || !user.DisabledNotifications.Contains(term)); +#pragma warning restore CA1307 // Specify StringComparison } return query.Select(user => user.Id).ToArray(); diff --git a/BTCPayServer/Services/SettingsRepository.cs b/BTCPayServer/Services/SettingsRepository.cs index 6e08422fd..bb9730fad 100644 --- a/BTCPayServer/Services/SettingsRepository.cs +++ b/BTCPayServer/Services/SettingsRepository.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Data; using BTCPayServer.Events; using Microsoft.EntityFrameworkCore; diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs index b41ff2f6f..b62d3cd27 100644 --- a/BTCPayServer/Services/Stores/StoreRepository.cs +++ b/BTCPayServer/Services/Stores/StoreRepository.cs @@ -13,7 +13,10 @@ namespace BTCPayServer.Services.Stores public class StoreRepository { private readonly ApplicationDbContextFactory _ContextFactory; - + public ApplicationDbContext CreateDbContext() + { + return _ContextFactory.CreateContext(); + } public StoreRepository(ApplicationDbContextFactory contextFactory) { _ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory)); @@ -177,7 +180,7 @@ namespace BTCPayServer.Services.Stores ctx.Add(userStore); await ctx.SaveChangesAsync(); } - } + } public async Task CreateStore(string ownerId, string name) { @@ -193,6 +196,112 @@ namespace BTCPayServer.Services.Stores return store; } + public async Task GetWebhooks(string storeId) + { + using var ctx = _ContextFactory.CreateContext(); + return await ctx.StoreWebhooks + .Where(s => s.StoreId == storeId) + .Select(s => s.Webhook).ToArrayAsync(); + } + + public async Task GetWebhookDelivery(string storeId, string webhookId, string deliveryId) + { + using var ctx = _ContextFactory.CreateContext(); + return await ctx.StoreWebhooks + .Where(d => d.StoreId == storeId && d.WebhookId == webhookId) + .SelectMany(d => d.Webhook.Deliveries) + .Where(d => d.Id == deliveryId) + .FirstOrDefaultAsync(); + } + + public async Task AddWebhookDelivery(WebhookDeliveryData delivery) + { + using var ctx = _ContextFactory.CreateContext(); + ctx.WebhookDeliveries.Add(delivery); + var invoiceWebhookDelivery = delivery.GetBlob().ReadRequestAs(); + if (invoiceWebhookDelivery.InvoiceId != null) + { + ctx.InvoiceWebhookDeliveries.Add(new InvoiceWebhookDeliveryData() + { + InvoiceId = invoiceWebhookDelivery.InvoiceId, + DeliveryId = delivery.Id + }); + } + await ctx.SaveChangesAsync(); + } + + public async Task GetWebhookDeliveries(string storeId, string webhookId, int? count) + { + using var ctx = _ContextFactory.CreateContext(); + IQueryable req = ctx.StoreWebhooks + .Where(s => s.StoreId == storeId && s.WebhookId == webhookId) + .SelectMany(s => s.Webhook.Deliveries) + .OrderByDescending(s => s.Timestamp); + if (count is int c) + req = req.Take(c); + return await req + .ToArrayAsync(); + } + + public async Task CreateWebhook(string storeId, WebhookBlob blob) + { + using var ctx = _ContextFactory.CreateContext(); + WebhookData data = new WebhookData(); + data.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); + if (string.IsNullOrEmpty(blob.Secret)) + blob.Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); + data.SetBlob(blob); + StoreWebhookData storeWebhook = new StoreWebhookData(); + storeWebhook.StoreId = storeId; + storeWebhook.WebhookId = data.Id; + ctx.StoreWebhooks.Add(storeWebhook); + ctx.Webhooks.Add(data); + await ctx.SaveChangesAsync(); + return data.Id; + } + + public async Task GetWebhook(string storeId, string webhookId) + { + var ctx = _ContextFactory.CreateContext(); + return await ctx.StoreWebhooks + .Where(s => s.StoreId == storeId && s.WebhookId == webhookId) + .Select(s => s.Webhook) + .FirstOrDefaultAsync(); + } + public async Task GetWebhook(string webhookId) + { + var ctx = _ContextFactory.CreateContext(); + return await ctx.StoreWebhooks + .Where(s => s.WebhookId == webhookId) + .Select(s => s.Webhook) + .FirstOrDefaultAsync(); + } + public async Task DeleteWebhook(string storeId, string webhookId) + { + var ctx = _ContextFactory.CreateContext(); + var hook = await ctx.StoreWebhooks + .Where(s => s.StoreId == storeId && s.WebhookId == webhookId) + .Select(s => s.Webhook) + .FirstOrDefaultAsync(); + if (hook is null) + return; + ctx.Webhooks.Remove(hook); + await ctx.SaveChangesAsync(); + } + + public async Task UpdateWebhook(string storeId, string webhookId, WebhookBlob webhookBlob) + { + var ctx = _ContextFactory.CreateContext(); + var hook = await ctx.StoreWebhooks + .Where(s => s.StoreId == storeId && s.WebhookId == webhookId) + .Select(s => s.Webhook) + .FirstOrDefaultAsync(); + if (hook is null) + return; + hook.SetBlob(webhookBlob); + await ctx.SaveChangesAsync(); + } + public async Task RemoveStore(string storeId, string userId) { using (var ctx = _ContextFactory.CreateContext()) @@ -225,6 +334,11 @@ namespace BTCPayServer.Services.Stores var store = await ctx.Stores.FindAsync(storeId); if (store == null) return false; + var webhooks = await ctx.StoreWebhooks + .Select(o => o.Webhook) + .ToArrayAsync(); + foreach (var w in webhooks) + ctx.Webhooks.Remove(w); ctx.Stores.Remove(store); await ctx.SaveChangesAsync(); return true; diff --git a/BTCPayServer/Views/Home/SwaggerDocs.cshtml b/BTCPayServer/Views/Home/SwaggerDocs.cshtml index dfd5742c5..3fe3e04ba 100644 --- a/BTCPayServer/Views/Home/SwaggerDocs.cshtml +++ b/BTCPayServer/Views/Home/SwaggerDocs.cshtml @@ -1,4 +1,4 @@ -@{ +@{ Layout = null; } @@ -23,6 +23,6 @@ - + diff --git a/BTCPayServer/Views/Invoice/Checkout.cshtml b/BTCPayServer/Views/Invoice/Checkout.cshtml index a799a3782..6738e7763 100644 --- a/BTCPayServer/Views/Invoice/Checkout.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout.cshtml @@ -49,7 +49,7 @@

Alternatively, click below to continue to our HTML-only invoice.

- + Continue to javascript-disabled invoice > @@ -57,7 +57,7 @@