Compare commits

..

77 Commits

Author SHA1 Message Date
c7c0ba745d Lowercasing the protocol in QR code because it's not widely supported
https://github.com/btcpayserver/btcpayserver/issues/2099#issuecomment-738550565
2020-12-04 14:44:21 +09:00
e7e0b29382 bump 2020-12-04 14:14:36 +09:00
83ec280a29 Reverting uppercasing of Bech32 addresses in QR code 2020-12-04 14:12:24 +09:00
bffa0e7fe5 bump 2020-12-03 23:29:05 +09:00
6d2d917bf1 Fix sync not showing
fixes #2092
2020-12-03 23:27:25 +09:00
06d6d15441 Bump version 2020-11-26 00:15:40 +09:00
87676ece18 Merge pull request #2080 from NicolasDorier/changelog/1.0.5.10
Changelog 1.0.6.0
2020-11-26 00:14:52 +09:00
acfd3f1002 Changelog 1.0.5.10 2020-11-26 00:07:29 +09:00
d5cbe66b0e fix monero condition 2020-11-25 06:25:33 +01:00
88aa34747b Automatically generate permissions docs for GreenField (#2043)
* Automatically generate permissions docs for GreenField

* Do a test instead
2020-11-24 10:10:32 +01:00
cc8dcade49 Fix QR code wallet import when not electrum format 2020-11-24 09:54:17 +01:00
a64dd9af16 remove hack #2070 2020-11-23 19:39:53 +09:00
8405ce6369 Merge pull request #2079 from NicolasDorier/state-renaming
[Greenfield BREAKING CHANGE] Rename invoice states and payment states
2020-11-23 19:27:41 +09:00
18e68d04f9 Rename invoice states and payment states 2020-11-23 18:28:35 +09:00
f3010f622c fix test 2020-11-23 17:13:29 +09:00
ff87319a74 API: Handle lightning invoice creation errors (#2070)
Catches LightningClient exceptions and responds with a detailed API error (400) instead of a generic server failure (503).
2020-11-23 06:40:13 +01:00
fa517417ed Ensure tx labels display correctly when there are many (#2076)
fix #2075
2020-11-23 06:34:34 +01:00
f9b86a6b2e Merge pull request #1901 from btcpayserver/api/invoice-extras
GreenField: Add Invoice Payment methods endpoints
2020-11-20 10:25:00 +09:00
8a21dfa93f Merge pull request #2051 from Kukks/separate-ln-bitcoin-checkout-part2
Separate ln bitcoin checkout part2
2020-11-19 21:04:46 +09:00
c88ae2d7fe Merge pull request #2069 from Kukks/monero-fix
Fix monero sync height check
2020-11-19 21:03:46 +09:00
25e979687f Fix monero sync height check
based on issue at https://github.com/monero-project/monero/issues/7029
2020-11-19 12:27:05 +01:00
af866939f4 fix merge 2020-11-19 07:34:22 +01:00
e72becdfcb Merge branch 'master' into separate-ln-bitcoin-checkout-part2 2020-11-19 12:44:01 +09:00
4bd97eecc9 Merge pull request #2050 from Kukks/separate-btc-ln-checkout
Separate views of Bitcoin and LN
2020-11-19 12:42:17 +09:00
8ffe6dcfa0 Fix build 2020-11-19 12:40:07 +09:00
23002ac70d Merge pull request #2068 from Kukks/plugins-db
Plugins: Allow creation of independent DbContexts
2020-11-19 12:39:26 +09:00
1262ad3cd4 Fix warnings 2020-11-19 12:34:56 +09:00
d8f145c4dc Merge branch 'master' into plugins-db 2020-11-19 12:19:17 +09:00
9c5fd1b478 Merge pull request #2058 from NicolasDorier/webhook2
Add Webhooks in store's settings
2020-11-19 12:17:34 +09:00
24f3285003 Let migration assembly resolve from itself 2020-11-18 13:22:05 +01:00
179520a211 Plugins: Allow creation of independent DbContexts
This allows plugins to create custom dbcontexts, which would be namespaced in the scheme with a prefix. Migrations are supported too and the table would be prefixed too
2020-11-18 12:27:26 +01:00
ec31a4fe17 Improve Lightning node info view (#2066)
* Cleanups

* Add store name

* Unify js/non-js HTML
2020-11-17 08:57:14 +01:00
ab780485b2 Center pay button content (#2042)
close #1999
2020-11-17 08:18:50 +01:00
07c5c2972d Add callback doc 2020-11-16 12:13:27 +09:00
af593117e4 Merge pull request #2064 from Kukks/plugins-hooks-actions
Plugins: Hook System
2020-11-16 10:51:50 +09:00
931505d135 Plugins: Hook System
Almost an exact replica of https://developer.wordpress.org/plugins/hooks/
This will allow plugins to extend specific points in business logic, such as validation, invoice payload changes, etc
2020-11-15 14:39:21 +01:00
6c7433b2f1 Merge pull request #2060 from btcpayserver/feat/unified-qr
Unified QR code, BIP21 with lightning fallback
2020-11-14 12:11:26 -06:00
38ca0accde Removing section separators 2020-11-14 12:07:11 -06:00
df79c2cf48 Improve tests of webhooks 2020-11-14 13:39:44 +09:00
94bcbeb604 Add Greenfield API 2020-11-13 14:15:03 +09:00
35e45333b1 Uppercasing destination address in QR code if it's Bech32 2020-11-09 23:57:48 -06:00
f17a6f13a4 Adding uppercasing of BITCOIN: scheme 2020-11-09 23:46:31 -06:00
1bff7bdc47 Adding validation to ensure unified QR code works as expected 2020-11-09 23:41:29 -06:00
13cb2c695f Adding onChainWithLnInvoiceFallback parameter to swagger doc 2020-11-09 23:08:07 -06:00
c581b80132 ToUpper for lightning invoice part of unified QR code
Ref: https://github.com/btcpayserver/btcpayserver/pull/2060#issuecomment-723828348
2020-11-09 02:05:17 -06:00
4dd0ae8bdc UnitTest1.cs code reformatting 2020-11-09 01:48:45 -06:00
4db67e5bc5 Adding slight delay to make test less flaky
Ref:
https://app.circleci.com/pipelines/github/btcpayserver/btcpayserver/4181/workflows/101d7b31-e267-4f5a-9892-2abb7b6cb687/jobs/11557
2020-11-09 01:48:12 -06:00
6aa9122160 By convention ln invoices are usually lowercase 2020-11-09 01:14:28 -06:00
f84f2dca64 Adding option for unified onchain QR code to Checkout Experience 2020-11-09 01:11:03 -06:00
3c6992e910 Adding lightning invoice fallback to onchain bitcoin url if enabled 2020-11-09 00:23:09 -06:00
7f79d16f02 Code cleanup of used classes 2020-11-08 23:56:39 -06:00
790c386ba8 Renaming class for enabled text to be consistent between wallet and lightning 2020-11-08 23:33:49 -06:00
ee72badf21 Fixing HTML tag typo 2020-11-08 23:15:21 -06:00
c9c4453660 Fix-up links which ignore custom root path (#2059)
address #2057
2020-11-08 09:39:10 +01:00
f3611ac693 Add Webhooks in store's settings 2020-11-08 15:57:24 +09:00
cc6fe24e82 Show significant letters in the derivation scheme in store settings 2020-11-08 11:29:36 +09:00
5a7730951a Fix create token button 2020-11-08 11:26:23 +09:00
eef729b5f9 API: Fix open channel validation condition (#2054) 2020-11-06 16:52:58 +01:00
0bb0a38649 Merge pull request #2053 from NicolasDorier/eventsaver
Decouple event saving from InvoiceNotificationManager
2020-11-06 22:52:19 +09:00
39029adcd8 Merge pull request #2011 from dennisreimann/store-setup
Store setup restructuring
2020-11-06 22:28:20 +09:00
c967f91abb Move some code out of InvoiceNotificationManager into InvoiceEventSaverService 2020-11-06 22:24:02 +09:00
1ba0685596 API Docs: Fix lightning invoice for store payload (#2052) 2020-11-06 14:00:10 +01:00
f2daa6a150 Improve store setup display 2020-11-06 11:14:00 +01:00
e021d42551 Ensure that there is no LN/Bitcoin specific logic in the Invoice UI endpoint 2020-11-06 11:09:17 +01:00
13509e31ca Don't warn, but hint 2020-11-06 10:50:23 +01:00
b9af805ac1 Store UI: Improve wallet status display 2020-11-06 10:50:22 +01:00
378d2bc8ba Store settings: Improve wallet display 2020-11-06 10:50:21 +01:00
b73aa55a75 Store setup: Restructuring basics 2020-11-06 10:50:20 +01:00
a729dd1380 Separate views of Bitcoin and LN 2020-11-06 09:35:26 +01:00
09335e2cf1 Plugins UI improvements (#2048)
* Improve dependencies display

* Improve version information toggling
2020-11-06 08:06:37 +01:00
31738c465d Plugins: Load plugins by order, aesthetic plugin dependency system (#2020)
* Plugins: Load plugins by order, aesthetic plugin dependency system

Introduces plugins loading in order of installation, BTCPay itself shows up as a system plugin, and that plugins can define other plugins as dependencies.

* use a proper type for plugin dependencies

* rebase fixes

* message when cannot install
2020-11-05 15:43:14 +01:00
256d711fde fix 2020-10-27 11:01:22 +01:00
1d82c3779b Remove AwaitingConfirmation 2020-10-27 09:49:35 +01:00
abc9d07977 fix swagger format for dates 2020-10-27 09:47:22 +01:00
40d95acfb9 Apply suggestions from code review
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2020-10-27 09:47:22 +01:00
9a92bc05db fix json types 2020-10-27 09:47:22 +01:00
8962bf00f6 GreenField: Add Invoice Payment methods endpoints
lets you fetch all active payment methods data + payments made
2020-10-27 09:47:21 +01:00
223 changed files with 5681 additions and 1299 deletions

View File

@ -30,4 +30,10 @@
<ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
namespace BTCPayServer.Security
namespace BTCPayServer.Abstractions.Constants
{
public class AuthenticationSchemes
{

View File

@ -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<T> 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<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
break;
case DatabaseType.MySQL:
builder.UseMySql(_options.ConnectionString, o =>
{
o.EnableRetryOnFailure(10);
if (!string.IsNullOrEmpty(_schemaPrefix))
{
o.MigrationsHistoryTable(_schemaPrefix);
}
});
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@ -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
{

View File

@ -1,6 +1,6 @@
using System;
namespace BTCPayServer.Contracts
namespace BTCPayServer.Abstractions.Contracts
{
public abstract class BaseNotification
{

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace BTCPayServer.Abstractions.Contracts
{
public interface IPluginHookAction
{
public string Hook { get; }
Task Execute(object args);
}
}

View File

@ -0,0 +1,11 @@
using System.Threading.Tasks;
namespace BTCPayServer.Abstractions.Contracts
{
public interface IPluginHookFilter
{
public string Hook { get; }
Task<object> Execute(object args);
}
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace BTCPayServer.Abstractions.Contracts
{
public interface IPluginHookService
{
Task ApplyAction(string hook, object args);
Task<object> ApplyFilter(string hook, object args);
}
}

View File

@ -1,7 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Services
namespace BTCPayServer.Abstractions.Contracts
{
public interface ISettingsRepository
{

View File

@ -1,7 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Hosting
namespace BTCPayServer.Abstractions.Contracts
{
public interface IStartupTask
{

View File

@ -1,4 +1,4 @@
namespace BTCPayServer.Contracts
namespace BTCPayServer.Abstractions.Contracts
{
public interface ISyncSummaryProvider
{

View File

@ -0,0 +1,9 @@
namespace BTCPayServer.Abstractions.Contracts
{
public interface IUIExtension
{
string Partial { get; }
string Location { get; }
}
}

View File

@ -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
{

View File

@ -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
{

View File

@ -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
{

View File

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

View File

@ -0,0 +1,9 @@
namespace BTCPayServer.Abstractions.Models
{
public enum DatabaseType
{
Sqlite,
Postgres,
MySQL,
}
}

View File

@ -1,6 +1,6 @@
using System;
namespace BTCPayServer.Models
namespace BTCPayServer.Abstractions.Models
{
public class StatusMessageModel
{

View File

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Abstractions.Services
{
public abstract class PluginAction<T>:IPluginHookAction
{
public string Hook { get; }
public Task Execute(object args)
{
return Execute(args is T args1 ? args1 : default);
}
public abstract Task Execute(T arg);
}
}

View File

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Abstractions.Services
{
public abstract class PluginHookFilter<T>:IPluginHookFilter
{
public string Hook { get; }
public Task<object> Execute(object args)
{
return Execute(args is T args1 ? args1 : default).ContinueWith(task => task.Result as object);
}
public abstract Task<T> Execute(T arg);
}
}

View File

@ -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)

View File

@ -13,7 +13,7 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.1.1</Version>
<Version Condition=" '$(Version)' == '' ">1.2.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BTCPayServer.Client
{

View File

@ -26,6 +26,13 @@ namespace BTCPayServer.Client
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}"), token);
return await HandleResponse<InvoiceData>(response);
}
public virtual async Task<InvoicePaymentMethodDataModel[]> GetInvoicePaymentMethods(string storeId, string invoiceId,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods"), token);
return await HandleResponse<InvoicePaymentMethodDataModel[]>(response);
}
public virtual async Task ArchiveInvoice(string storeId, string invoiceId,
CancellationToken token = default)
@ -52,7 +59,7 @@ namespace BTCPayServer.Client
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.Status!= InvoiceStatus.Complete && request.Status!= InvoiceStatus.Invalid)
if (request.Status!= InvoiceStatus.Settled && request.Status!= InvoiceStatus.Invalid)
throw new ArgumentOutOfRangeException(nameof(request.Status), "Status can only be Invalid or Complete");
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/status", bodyPayload: request,

View File

@ -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<StoreWebhookData> 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<StoreWebhookData>(response);
}
public async Task<StoreWebhookData> 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<StoreWebhookData>(response);
}
public async Task<StoreWebhookData> 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<StoreWebhookData>(response);
}
public async Task<bool> 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<StoreWebhookData[]> GetWebhooks(string storeId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks"), token);
return await HandleResponse<StoreWebhookData[]>(response);
}
public async Task<WebhookDeliveryData[]> 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<WebhookDeliveryData[]>(response);
}
public async Task<WebhookDeliveryData> 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<WebhookDeliveryData>(response);
}
public async Task<string> 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<string>(response);
}
public async Task<WebhookEvent> 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<WebhookEvent>(response);
}
}
}

View File

@ -65,7 +65,8 @@ namespace BTCPayServer.Client
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
{
await HandleResponse(message);
return JsonConvert.DeserializeObject<T>(await message.Content.ReadAsStringAsync());
var str = await message.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(str);
}
protected virtual HttpRequestMessage CreateHttpRequest(string path,

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@ -20,4 +18,12 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset CreatedTime { get; set; }
}
public enum InvoiceStatus
{
New,
Processing,
Expired,
Invalid,
Settled
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public class InvoicePaymentMethodDataModel
{
public string Destination { get; set; }
public string PaymentLink { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Rate { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal PaymentMethodPaid { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal TotalPaid { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Due { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal NetworkFee { get; set; }
public List<Payment> Payments { get; set; }
public string PaymentMethod { get; set; }
public class Payment
{
public string Id { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTime ReceivedDate { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Value { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Fee { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public PaymentStatus Status { get; set; }
public string Destination { get; set; }
public enum PaymentStatus
{
Invalid,
Processing,
Settled
}
}
}
}

View File

@ -1,12 +0,0 @@
namespace BTCPayServer.Client.Models
{
public enum InvoiceStatus
{
New,
Paid,
Expired,
Invalid,
Complete,
Confirmed
}
}

View File

@ -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]

View File

@ -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<WebhookEventType>();
}
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; }
}
}

View File

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

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Client.Models
{
public enum WebhookDeliveryStatus
{
Failed,
HttpError,
HttpSuccess
}
}

View File

@ -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<string, JToken> AdditionalData { get; set; }
public T ReadAs<T>()
{
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);
return JsonConvert.DeserializeObject<T>(str, DefaultSerializerSettings);
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Client.Models
{
public enum WebhookEventType
{
InvoiceCreated,
InvoiceReceivedPayment,
InvoiceProcessing,
InvoiceExpired,
InvoiceSettled,
InvoiceInvalid
}
}

View File

@ -0,0 +1,85 @@
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 WebhookInvoiceSettledEvent : WebhookInvoiceEvent
{
public WebhookInvoiceSettledEvent()
{
}
public WebhookInvoiceSettledEvent(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 WebhookInvoiceProcessingEvent : WebhookInvoiceEvent
{
public WebhookInvoiceProcessingEvent()
{
}
public WebhookInvoiceProcessingEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool OverPaid { get; set; }
}
public class WebhookInvoiceReceivedPaymentEvent : WebhookInvoiceEvent
{
public WebhookInvoiceReceivedPaymentEvent()
{
}
public WebhookInvoiceReceivedPaymentEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool AfterExpiration { get; set; }
}
public class WebhookInvoiceExpiredEvent : WebhookInvoiceEvent
{
public WebhookInvoiceExpiredEvent()
{
}
public WebhookInvoiceExpiredEvent(WebhookEventType evtType) : base(evtType)
{
}
public bool PartiallyPaid { get; set; }
}
}

View File

@ -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:

View File

@ -7,12 +7,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
</ItemGroup>
</Project>

View File

@ -63,6 +63,11 @@ namespace BTCPayServer.Data
public DbSet<U2FDevice> U2FDevices { get; set; }
public DbSet<NotificationData> Notifications { get; set; }
public DbSet<StoreWebhookData> StoreWebhooks { get; set; }
public DbSet<WebhookData> Webhooks { get; set; }
public DbSet<WebhookDeliveryData> WebhookDeliveries { get; set; }
public DbSet<InvoiceWebhookDeliveryData> InvoiceWebhookDeliveries { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().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

View File

@ -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<ApplicationDbContext>
{
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<ApplicationDbContext>();
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<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
else if (_Type == DatabaseType.MySQL)
builder.UseMySql(_ConnectionString, o => o.MigrationsAssembly("BTCPayServer.Data").EnableRetryOnFailure(10));
}
}
}

View File

@ -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<InvoiceWebhookDeliveryData>()
.HasKey(p => new { p.InvoiceId, p.DeliveryId });
builder.Entity<InvoiceWebhookDeliveryData>()
.HasOne(o => o.Invoice)
.WithOne().OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceWebhookDeliveryData>()
.HasOne(o => o.Delivery)
.WithOne().OnDelete(DeleteBehavior.Cascade);
}
}
}

View File

@ -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<StoreWebhookData>()
.HasKey(p => new { p.StoreId, p.WebhookId });
builder.Entity<StoreWebhookData>()
.HasOne(o => o.Webhook)
.WithOne().OnDelete(DeleteBehavior.Cascade);
builder.Entity<StoreWebhookData>()
.HasOne(o => o.Store)
.WithOne().OnDelete(DeleteBehavior.Cascade);
}
}
}

View File

@ -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<WebhookDeliveryData> Deliveries { get; set; }
}
}

View File

@ -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<WebhookDeliveryData>()
.HasOne(o => o.Webhook)
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
}
}
}

View File

@ -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<string>(maxLength: 25, nullable: false),
Blob = table.Column<byte[]>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Webhooks", x => x.Id);
});
migrationBuilder.CreateTable(
name: "StoreWebhooks",
columns: table => new
{
StoreId = table.Column<string>(nullable: false),
WebhookId = table.Column<string>(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<string>(maxLength: 25, nullable: false),
WebhookId = table.Column<string>(maxLength: 25, nullable: false),
Timestamp = table.Column<DateTimeOffset>(nullable: false),
Blob = table.Column<byte[]>(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<string>(nullable: false),
DeliveryId = table.Column<string>(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");
}
}
}

View File

@ -257,6 +257,25 @@ namespace BTCPayServer.Migrations
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceWebhookDeliveryData", b =>
{
b.Property<string>("InvoiceId")
.HasColumnType("TEXT");
b.Property<string>("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<string>("Id")
@ -588,6 +607,25 @@ namespace BTCPayServer.Migrations
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.StoreWebhookData", b =>
{
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.Property<string>("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<string>("Id")
@ -696,6 +734,46 @@ namespace BTCPayServer.Migrations
b.ToTable("WalletTransactions");
});
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(25);
b.Property<byte[]>("Blob")
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Id");
b.ToTable("Webhooks");
});
modelBuilder.Entity("BTCPayServer.Data.WebhookDeliveryData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(25);
b.Property<byte[]>("Blob")
.IsRequired()
.HasColumnType("BLOB");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.Property<string>("WebhookId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.HasKey("Id");
b.HasIndex("WebhookId");
b.ToTable("WebhookDeliveries");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)

View File

@ -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
{

View File

@ -12,4 +12,8 @@
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4">
</PackageReference>
</ItemGroup>
</Project>

View File

@ -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<IActionResult> Index()
{
return View(new TestPluginPageViewModel()
{
Data = await _testPluginService.Get()
});
}
}
public class TestPluginPageViewModel
{
public List<TestPluginData> Data { get; set; }
}
}

View File

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

View File

@ -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<TestPluginData> TestPluginRecords { get; set; }
public TestPluginDbContext(DbContextOptions<TestPluginDbContext> 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());
}
}
}
}
}
}

View File

@ -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<string>(nullable: false),
Timestamp = table.Column<DateTimeOffset>(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");
}
}
}

View File

@ -0,0 +1,36 @@
// <auto-generated />
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<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("TestPluginRecords");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -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<TestPluginDbContext>
{
public TestPluginDbContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<TestPluginDbContext>();
builder.UseSqlite("Data Source=temp.db");
return new TestPluginDbContext(builder.Options, true);
}
}
public class TestPluginDbContextFactory : BaseDbContextFactory<TestPluginDbContext>
{
public TestPluginDbContextFactory(DatabaseOptions options) : base(options, "BTCPayServer.Plugins.Test")
{
}
public override TestPluginDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<TestPluginDbContext>();
ConfigureBuilder(builder);
return new TestPluginDbContext(builder.Options);
}
}
}

View File

@ -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<List<TestPluginData>> Get()
{
await using var context = _testPluginDbContextFactory.CreateContext();
return await context.TestPluginRecords.ToListAsync();
}
}
}

View File

@ -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<IUIExtension>(new UIExtension("TestExtensionNavExtension", "header-nav"));
services.AddHostedService<ApplicationPartsLogger>();
services.AddSingleton<TestPluginService>();
services.AddSingleton<TestPluginDbContextFactory>();
services.AddDbContext<TestPluginDbContext>((provider, o) =>
{
var factory = provider.GetRequiredService<TestPluginDbContextFactory>();
factory.ConfigureBuilder(o);
});
}
public override void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices)
{
base.Execute(applicationBuilder, applicationBuilderApplicationServices);
applicationBuilderApplicationServices.GetService<TestPluginDbContextFactory>().CreateContext().Database.Migrate();
applicationBuilderApplicationServices.GetService<TestPluginService>().AddTestDataRecord().GetAwaiter().GetResult();
}
}
}

View File

@ -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();
}
}
}

View File

@ -1,3 +1,4 @@
@model BTCPayServer.Plugins.Test.TestPluginPageViewModel
<section>
<div class="container">
<h1>Challenge Completed!!</h1>
@ -5,5 +6,16 @@
<a href="https://twitter.com/NicolasDorier/status/1307221679014256640">
<img src="/Resources/img/screengrab.png"/>
</a>
<div class="row">
<h2>Persisted Data</h2>
<p>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.</p>
<ul class="list-group">>
@foreach (var item in Model.Data)
{
<li class="list-group-item">@item.Id at @item.Timestamp.ToString("F")</li>
}
</ul>
</div>
</div>
</section>

View File

@ -875,7 +875,7 @@ normal:
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
});
var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC");

View File

@ -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();

View File

@ -21,6 +21,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="85.0.4183.8700" />
<PackageReference Include="xunit" Version="2.4.1" />

View File

@ -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
{

View File

@ -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<HttpContext> GetNextRequest()
public async Task<HttpContext> GetNextRequest(CancellationToken cancellationToken = default)
{
return await _channel.Reader.ReadAsync();
return await _channel.Reader.ReadAsync(cancellationToken);
}
}
}

View File

@ -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);
@ -848,9 +945,15 @@ namespace BTCPayServer.Tests
Assert.Single(invoices);
Assert.Equal(newInvoice.Id, invoices.First().Id);
//get payment request
//get
var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
Assert.Equal(newInvoice.Metadata, invoice.Metadata);
var paymentMethods = await viewOnly.GetInvoicePaymentMethods(user.StoreId, newInvoice.Id);
Assert.Single(paymentMethods);
var paymentMethod = paymentMethods.First();
Assert.Equal("BTC", paymentMethod.PaymentMethod);
Assert.Empty(paymentMethod.Payments);
//update
invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
@ -859,7 +962,7 @@ namespace BTCPayServer.Tests
{
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest()
{
Status = InvoiceStatus.Complete
Status = InvoiceStatus.Settled
});
});
@ -878,10 +981,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.Settled, 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.Settled)
{
Assert.Equal(InvoiceStatus.Settled, result.Status);
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
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<WebhookInvoiceInvalidEvent>(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 +1043,7 @@ namespace BTCPayServer.Tests
var info = await client.GetLightningNodeInfo("BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
var err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
Assert.Contains("503", err.Message);
// Not permission for the store!

View File

@ -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;
@ -269,7 +270,7 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status);
});
s.GoToInvoices();
@ -329,7 +330,7 @@ namespace BTCPayServer.Tests
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
var dto = invoice.EntityToDTO();
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status);
});
s.GoToInvoices();
paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}"))
@ -719,7 +720,7 @@ retry:
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(lastInvoiceId);
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status);
Assert.Equal(InvoiceExceptionStatus.None, invoice.ExceptionStatus);
var coins = await btcPayWallet.GetUnspentCoins(receiverUser.DerivationScheme);
foreach (var coin in coins)
@ -1046,7 +1047,7 @@ retry:
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatus.Paid, invoiceEntity.Status);
Assert.Equal(InvoiceStatusLegacy.Paid, invoiceEntity.Status);
Assert.Contains(invoiceEntity.GetPayments(), p => p.Accounted &&
((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null);
});
@ -1075,7 +1076,7 @@ retry:
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatus.New, invoiceEntity.Status);
Assert.Equal(InvoiceStatusLegacy.New, invoiceEntity.Status);
Assert.True(invoiceEntity.GetPayments().All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData().First().PayjoinInformation.ContributedOutPoints[0];
});

View File

@ -207,12 +207,12 @@ namespace BTCPayServer.Tests
pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(InvoiceState.ToString(InvoiceStatus.New), invoice.Status);
Assert.Equal(InvoiceState.ToString(InvoiceStatusLegacy.New), invoice.Status);
Assert.IsType<OkObjectResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(InvoiceState.ToString(InvoiceStatus.Invalid), invoice.Status);
Assert.Equal(InvoiceState.ToString(InvoiceStatusLegacy.Invalid), invoice.Status);
Assert.IsType<BadRequestObjectResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));

View File

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

View File

@ -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("InvoiceProcessing")).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=\"InvoiceProcessing\" 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()

View File

@ -23,6 +23,7 @@ namespace BTCPayServer.Tests
return new ServerTester(scope, newDb);
}
public List<IDisposable> Resources = new List<IDisposable>();
readonly string _Directory;
public ServerTester(string scope, bool newDb)
{
@ -145,7 +146,7 @@ namespace BTCPayServer.Tests
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
var sub = PayTester.GetService<EventAggregator>().Subscribe<T>(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)
{

View File

@ -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;
@ -222,7 +223,7 @@ namespace BTCPayServer.Tests
//delete file
Assert.IsType<RedirectToActionResult>(await controller.DeleteFile(fileId));
controller.TempData.GetStatusMessageModel();
statusMessageModel = controller.TempData.GetStatusMessageModel();
Assert.NotNull(statusMessageModel);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity);

View File

@ -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<WebhookInvoiceEvent> _webhookEvents;
private CancellationTokenSource _cts;
public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List<WebhookInvoiceEvent> webhookEvents)
{
_wh = wh;
_server = server;
_webhookEvents = webhookEvents;
_cts = new CancellationTokenSource();
_ = Listen(_cts.Token);
}
async Task Listen(CancellationToken cancellation)
{
while (!cancellation.IsCancellationRequested)
{
var req = await _server.GetNextRequest(cancellation);
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
var callback = Encoding.UTF8.GetString(bytes);
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
req.Response.StatusCode = 200;
_server.Done();
}
}
public void Dispose()
{
_cts.Cancel();
_server.Dispose();
}
}
public List<WebhookInvoiceEvent> WebhookEvents { get; set; } = new List<WebhookInvoiceEvent>();
public TEvent AssertHasWebhookEvent<TEvent>(WebhookEventType eventType, Action<TEvent> assert) where TEvent : class
{
foreach (var evt in WebhookEvents)
{
if (evt.Type == eventType)
{
var typedEvt = evt.ReadAs<TEvent>();
try
{
assert(typedEvt);
return typedEvt;
}
catch (XunitException)
{
}
}
}
Assert.True(false, "No webhook event match the assertion");
return null;
}
public async Task SetupWebhook()
{
FakeServer server = new FakeServer();
await server.Start();
var client = await CreateClient(Policies.CanModifyStoreWebhooks);
var wh = await client.CreateWebhook(StoreId, new CreateStoreWebhookRequest()
{
AutomaticRedelivery = false,
Url = server.ServerUri.AbsoluteUri
});
parent.Resources.Add(new WebhookListener(wh, server, WebhookEvents));
}
public async Task PayInvoice(string invoiceId)
{
var inv = await BitPay.GetInvoiceAsync(invoiceId);
var net = parent.ExplorerNode.Network;
this.parent.ExplorerNode.SendToAddress(BitcoinAddress.Create(inv.BitcoinAddress, net), inv.BtcDue);
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = await BitPay.GetInvoiceAsync(invoiceId, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
});
}
}
}

View File

@ -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;
@ -38,6 +39,7 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F.Models;
using BTCPayServer.Validation;
using DBriize.Utils;
using ExchangeSharp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -232,6 +234,55 @@ namespace BTCPayServer.Tests
Assert.True(valid);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task EnsureSwaggerPermissionsDocumented()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var acc = tester.NewAccount();
var description =
"BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n#OTHERPERMISSIONS#\n\nThe following permissions are available if the user is an administrator:\n\n#SERVERPERMISSIONS#\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n#STOREPERMISSIONS#\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n";
var storePolicies =
ManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair =>
Policies.IsStorePolicy(pair.Key) && !pair.Key.EndsWith(":", StringComparison.InvariantCulture));
var serverPolicies =
ManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair =>
Policies.IsServerPolicy(pair.Key));
var otherPolicies =
ManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair =>
!Policies.IsStorePolicy(pair.Key) && !Policies.IsServerPolicy(pair.Key));
description = description.ReplaceMultiple(new Dictionary<string, string>()
{
{
"#OTHERPERMISSIONS#",
string.Join("\n", otherPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))
},
{
"#SERVERPERMISSIONS#",
string.Join("\n", serverPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))
},
{
"#STOREPERMISSIONS#",
string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))
}
});
Logs.Tester.LogInformation(description);
var sresp = Assert
.IsType<JsonResult>(await tester.PayTester.GetController<HomeController>(acc.UserId, acc.StoreId)
.Swagger()).Value.ToJson();
JObject json = JObject.Parse(sresp);
Assert.Equal(description, json["components"]["securitySchemes"]["API Key"]["description"].Value<string>());
}
}
private static async Task CheckLinks(Regex regex, HttpClient httpClient, string file)
{
@ -364,7 +415,7 @@ namespace BTCPayServer.Tests
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
@ -563,7 +614,7 @@ namespace BTCPayServer.Tests
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
@ -770,30 +821,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<InvoiceDataChangedEvent>(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[] { InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.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<InvoiceDataChangedEvent>(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 +1051,6 @@ namespace BTCPayServer.Tests
}
}
}
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
Assert.NotNull(invoice2);
}
@ -1089,7 +1140,7 @@ namespace BTCPayServer.Tests
response.EnsureSuccessStatusCode();
AssertConnectionDropped();
Logs.Tester.LogInformation("Querying an onion address which can't be found should send http 500");
Logs.Tester.LogInformation("Querying an onin address which can't be found should send http 500");
response = await client.GetAsync("http://dwoduwoi.onion/");
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
AssertConnectionDropped();
@ -1459,7 +1510,7 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id);
Assert.Equal(InvoiceStatus.New, i.Status);
Assert.Equal(InvoiceStatusLegacy.New, i.Status);
Assert.Single(i.GetPayments());
Assert.False(i.GetPayments().First().Accounted);
});
@ -1508,7 +1559,7 @@ namespace BTCPayServer.Tests
);
}
}
// [Fact(Timeout = TestTimeout)]
[Fact()]
[Trait("Integration", "Integration")]
@ -1527,9 +1578,9 @@ namespace BTCPayServer.Tests
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest),
Money.Coins(0.01m));
});
var payments = Assert.IsType<InvoiceDetailsModel>(
Assert.IsType<ViewResult>(await user.GetController<InvoiceController>().Invoice(invoice.Id)).Model)
.Payments;
@ -1990,7 +2041,64 @@ 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<InvoiceController>().Checkout(invoice.Id);
var paymentMethodFirst = Assert.IsType<PaymentModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
// enable unified QR code in settings
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model
);
vm.OnChainWithLnInvoiceFallback = true;
Assert.IsType<RedirectToActionResult>(
user.GetController<StoresController>().CheckoutExperience(vm).Result
);
// validate that QR code now has both onchain and offchain payment urls
res = await user.GetController<InvoiceController>().Checkout(invoice.Id);
var paymentMethodSecond = Assert.IsType<PaymentModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.Contains("&lightning=", paymentMethodSecond.InvoiceBitcoinUrlQR);
Assert.StartsWith("bitcoin:", paymentMethodSecond.InvoiceBitcoinUrlQR);
var split = paymentMethodSecond.InvoiceBitcoinUrlQR.Split('?')[0];
// Standard for uppercase Bech32 addresses in QR codes is still not implemented in all wallets
// When it is widely propagated consider uncommenting these lines
//Assert.True($"BITCOIN:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress}" == split);
}
}
@ -2030,7 +2138,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 +2152,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 +2635,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 +2692,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 +2795,23 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
});
// Test on the webhooks
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
c =>
{
Assert.False(c.ManuallyMarked);
});
user.AssertHasWebhookEvent<WebhookInvoiceProcessingEvent>(WebhookEventType.InvoiceProcessing,
c =>
{
Assert.True(c.OverPaid);
});
user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(WebhookEventType.InvoiceReceivedPayment,
c =>
{
Assert.False(c.AfterExpiration);
});
}
}

View File

@ -21,10 +21,6 @@
<None Remove="Build\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Update="Views\Shared\NBXSyncSummary.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Remove="Currencies.txt" />
@ -251,5 +247,5 @@
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
</Project>

View File

@ -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

View File

@ -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

View File

@ -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
{

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
@ -212,8 +213,71 @@ namespace BTCPayServer.Controllers.GreenField
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId);
return await GetInvoice(storeId, invoiceId);
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
{
return NotFound();
}
return Ok(ToPaymentMethodModels(invoice));
}
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity)
{
return entity.GetPaymentMethods().Select(
method =>
{
var accounting = method.Calculate();
var details = method.GetPaymentMethodDetails();
var payments = method.ParentEntity.GetPayments().Where(paymentEntity =>
paymentEntity.GetPaymentMethodId() == method.GetId());
return new InvoicePaymentMethodDataModel()
{
PaymentMethod = method.GetId().ToStringNormalized(),
Destination = details.GetPaymentDestination(),
Rate = method.Rate,
Due = accounting.Due.ToDecimal(MoneyUnit.BTC),
TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC),
PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
Amount = accounting.Due.ToDecimal(MoneyUnit.BTC),
NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC),
PaymentLink =
method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due,
Request.GetAbsoluteRoot()),
Payments = payments.Select(paymentEntity =>
{
var data = paymentEntity.GetCryptoPaymentData();
return new InvoicePaymentMethodDataModel.Payment()
{
Destination = data.GetDestination(),
Id = data.GetPaymentId(),
Status = !paymentEntity.Accounted
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid
: data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) ||
data.PaymentCompleted(paymentEntity)
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled
: InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing,
Fee = paymentEntity.NetworkFee,
Value = data.GetValue(),
ReceivedDate = paymentEntity.ReceivedTime.DateTime
};
}).ToList()
};
}).ToArray();
}
private InvoiceData ToModel(InvoiceEntity entity)
{
return new InvoiceData()
@ -223,7 +287,7 @@ namespace BTCPayServer.Controllers.GreenField
CreatedTime = entity.InvoiceTime,
Amount = entity.Price,
Id = entity.Id,
Status = entity.Status,
Status = entity.Status.ToModernStatus(),
AdditionalStatus = entity.ExceptionStatus,
Currency = entity.Currency,
Metadata = entity.Metadata.ToJObject(),

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;

View File

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

View File

@ -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);
}
@ -263,13 +263,21 @@ namespace BTCPayServer.Controllers.GreenField
{
return this.CreateValidationError(ModelState);
}
var invoice = await lightningClient.CreateInvoice(
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
{
PrivateRouteHints = request.PrivateRouteHints
},
CancellationToken.None);
return Ok(ToModel(invoice));
try
{
var invoice = await lightningClient.CreateInvoice(
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
{
PrivateRouteHints = request.PrivateRouteHints
},
CancellationToken.None);
return Ok(ToModel(invoice));
}
catch (Exception ex)
{
return this.CreateAPIError("generic-error", ex.Message);
}
}
private LightningInvoiceData ToModel(LightningInvoice invoice)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Security;

View File

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

View File

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

View File

@ -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<DataWrapper<InvoiceResponse[]>> GetInvoices(
public async Task<IActionResult> 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));
}
}
}

View File

@ -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<IActionResult> 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<IActionResult> 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
@ -98,13 +150,13 @@ namespace BTCPayServer.Controllers
bool CanRefund(InvoiceState invoiceState)
{
return invoiceState.Status == InvoiceStatus.Confirmed ||
invoiceState.Status == InvoiceStatus.Complete ||
(invoiceState.Status == InvoiceStatus.Expired &&
return invoiceState.Status == InvoiceStatusLegacy.Confirmed ||
invoiceState.Status == InvoiceStatusLegacy.Complete ||
(invoiceState.Status == InvoiceStatusLegacy.Expired &&
(invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidLate ||
invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidOver ||
invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) ||
invoiceState.Status == InvoiceStatus.Invalid;
invoiceState.Status == InvoiceStatusLegacy.Invalid;
}
[HttpGet]
@ -464,12 +516,9 @@ namespace BTCPayServer.Controllers
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO();
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var storeBlob = store.GetStoreBlob();
var currency = invoice.Currency;
var accounting = paymentMethod.Calculate();
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
storeBlob.CoinSwitchSettings.IsConfigured())
? storeBlob.CoinSwitchSettings
@ -492,12 +541,11 @@ namespace BTCPayServer.Controllers
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ShowMoney(divisibility),
InvoiceCurrency = invoice.Currency,
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
FeeRate = paymentMethodDetails.GetFeeRate(),
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
@ -506,7 +554,6 @@ namespace BTCPayServer.Controllers
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? "/",
RedirectAutomatically = invoice.RedirectAutomatically,
StoreName = store.StoreName,
PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo,
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ShowMoney(divisibility),
#pragma warning disable CS0618 // Type or member is obsolete
@ -544,12 +591,7 @@ namespace BTCPayServer.Controllers
.OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
.ToList()
};
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob);
if (model.IsLightning && storeBlob.LightningAmountInSatoshi && model.CryptoCode == "Sats")
{
model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.Currency);
}
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
model.PaymentMethodId = paymentMethodId.ToString();
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
@ -596,7 +638,7 @@ namespace BTCPayServer.Controllers
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
if (invoice == null || invoice.Status == InvoiceStatusLegacy.Complete || invoice.Status == InvoiceStatusLegacy.Invalid || invoice.Status == InvoiceStatusLegacy.Expired)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
CompositeDisposable leases = new CompositeDisposable();
@ -673,7 +715,7 @@ namespace BTCPayServer.Controllers
model.Invoices.Add(new InvoiceModel()
{
Status = state,
ShowCheckout = invoice.Status == InvoiceStatus.New,
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
OrderId = invoice.Metadata.OrderId ?? string.Empty,
@ -831,7 +873,7 @@ namespace BTCPayServer.Controllers
}
else if (newState == "complete")
{
await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Complete);
await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Settled);
model.StatusString = new InvoiceState("complete", "marked").ToString();
}

View File

@ -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;
}
@ -195,7 +200,7 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.Metadata.BuyerEmail;
}
entity.Status = InvoiceStatus.New;
entity.Status = InvoiceStatusLegacy.New;
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
@ -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<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> 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;
}

View File

@ -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")},

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More