Compare commits
50 Commits
v2.0.0-alp
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
363b60385b | |||
90635ffc4e | |||
056f850268 | |||
336f2d88e9 | |||
e16b4062b5 | |||
747dacf3b1 | |||
f00a71922f | |||
c97c9d4ece | |||
9d3f8672d9 | |||
fe48cd4236 | |||
587d3aa612 | |||
8a951940fd | |||
b726ef8a2e | |||
25e360e175 | |||
1d9ec253fb | |||
3cf1aa00fa | |||
36a5d0ee3f | |||
f5e5174045 | |||
ba2301ebfe | |||
df651a2157 | |||
2d2c1d5f2d | |||
0f93581ff5 | |||
397452a7fe | |||
cd3157361a | |||
29a89f185a | |||
2f7a5c2967 | |||
f07ed53f7e | |||
7348a6a62f | |||
b7ba53eb60 | |||
e389d6a96b | |||
0238dffc7a | |||
666445e8f7 | |||
36bada8feb | |||
b4946f4db1 | |||
f3d485da53 | |||
3342122be2 | |||
f59751853a | |||
a60c55c6df | |||
3bad5883bb | |||
d4c30866b7 | |||
7c92ce771f | |||
4601359ebe | |||
04b1130837 | |||
c377617b5a | |||
222e8f66df | |||
87e2f5f414 | |||
7de05700e9 | |||
841f41da2f | |||
73dcde7780 | |||
377ff52222 |
BTCPayServer.Client
BTCPayServerClient.Apps.csBTCPayServerClient.OnChainPaymentMethods.csBTCPayServerClient.OnChainWallet.Objects.csBTCPayServerClient.OnChainWallet.cs
Models
AppItemStats.csAppSalesStats.csCreatePullPaymentRequest.csPayoutData.csPayoutProcessorData.csStoreBaseData.cs
Permissions.csBTCPayServer.Common
BTCPayServer.Data
ApplicationDbContext.csApplicationDbContextFactory.csBTCPayServer.Data.csproj
DBScripts
Data
AddressInvoiceData.csApplicationUser.csInvoiceData.Migration.csInvoiceData.csMigrationExtensions.csMigrationInterceptor.csPaymentData.Migration.csPaymentData.csPendingInvoiceData.cs
Migrations
20240405052858_cleanup_address_invoices.cs20240827034505_migratepayouts.cs20240904092905_UpdateStoreOwnerRole.cs20240906010127_renamecol.cs20240913034505_refactorpendinginvoicespayments.cs20240919085726_refactorinvoiceaddress.cs20240923065254_refactorpayments.cs20240924065254_monitoredinvoices.csApplicationDbContextModelSnapshot.cs
BTCPayServer.Tests
BTCPayServer.Tests.csprojBTCPayServerTester.csCheckoutUITests.csDatabaseTester.csDatabaseTests.csFastTests.csGreenfieldAPITests.csLanguageServiceTests.csSeleniumTester.csSeleniumTests.csServerTester.csTestAccount.cs
TestData
UnitTest1.csdocker-compose.altcoins.ymldocker-compose.ymlsetup-dev-basics.shBTCPayServer
Components
AppSales
AppTopItems
TruncateCenter
Controllers
BitpayInvoiceController.cs
GreenField
GreenfieldAppsController.csGreenfieldInvoiceController.csGreenfieldNotificationsController.csGreenfieldPayoutProcessorsController.csGreenfieldPullPaymentController.csGreenfieldServerRolesController.csGreenfieldStoreAutomatedLightningPayoutProcessorsController.csGreenfieldStoreAutomatedOnChainPayoutProcessorsController.csGreenfieldStoreOnChainPaymentMethodsController.WalletGeneration.csGreenfieldStoreOnChainPaymentMethodsController.csGreenfieldStoreOnChainWalletsController.csGreenfieldStorePayoutProcessorsController.csGreenfieldStoreRolesController.csGreenfieldStoreWebhooksController.csGreenfieldStoresController.csLocalBTCPayServerClient.cs
UIAccountController.csUIInvoiceController.UI.csUIManageController.APIKeys.csUIServerController.Roles.csUIServerController.Translations.csUIServerController.Users.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Onchain.csUIStoresController.Roles.csUIStoresController.Settings.csUIWalletsController.csData
AddressInvoiceDataExtensions.csInvoiceDataExtensions.csPaymentDataExtensions.cs
Payouts/LightningLike
StoreBlob.csEvents
Extensions.csHostedServices
BitpayIPNSender.csBlobMigratorHostedService.csInvoiceBlobMigratorHostedService.csInvoiceEventSaverService.csInvoiceWatcher.csPullPaymentHostedService.csUserEventHostedService.cs
Hosting
Models
Payments
PayoutProcessors
Plugins
Program.csProperties
Services
Altcoins
Monero
Zcash
Apps
DisplayFormatter.csInvoices
MigrationSettings.csStores
Translations.csUserService.csViews
Shared
Bitcoin
LayoutHead.cshtmlLayoutHeadStoreBranding.cshtmlLightning
ListRoles.cshtmlMonero
PosData.cshtmlPosDataEntry.cshtmlZcash
UIAccount
UIServer
CreateUser.cshtmlEditDictionary.cshtmlListUsers.cshtmlPolicies.cshtmlResetUserPassword.cshtmlUser.cshtml
UIStores
UIUserStores
UIWallets
wwwroot
img/readme
main
pos
swagger/v1
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -58,6 +59,20 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<CrowdfundAppData>($"api/v1/apps/crowdfund/{appId}", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<AppSalesStats> GetAppSales(string appId, int numberOfDays = 7, CancellationToken token = default)
|
||||
{
|
||||
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||
var queryPayload = new Dictionary<string, object> { { nameof(numberOfDays), numberOfDays } };
|
||||
return await SendHttpRequest<AppSalesStats>($"api/v1/apps/{appId}/sales", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<List<AppItemStats>> GetAppTopItems(string appId, int offset = 0, int count = 10, CancellationToken token = default)
|
||||
{
|
||||
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||
var queryPayload = new Dictionary<string, object> { { nameof(offset), offset }, { nameof(count), count } };
|
||||
return await SendHttpRequest<List<AppItemStats>>($"api/v1/apps/{appId}/top-items", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteApp(string appId, CancellationToken token = default)
|
||||
{
|
||||
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||
|
@ -15,7 +15,7 @@ public partial class BTCPayServerClient
|
||||
int amount = 10,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<UpdatePaymentMethodRequest, OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
|
||||
return await SendHttpRequest<UpdatePaymentMethodRequest, OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/preview",
|
||||
new Dictionary<string, object> { { "offset", offset }, { "amount", amount } },
|
||||
new UpdatePaymentMethodRequest { Config = JValue.CreateString(derivationScheme) },
|
||||
HttpMethod.Post, token);
|
||||
@ -25,7 +25,7 @@ public partial class BTCPayServerClient
|
||||
string storeId, string paymentMethodId, int offset = 0, int amount = 10,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
|
||||
return await SendHttpRequest<OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/preview",
|
||||
new Dictionary<string, object> { { "offset", offset }, { "amount", amount } }, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ public partial class BTCPayServerClient
|
||||
string paymentMethodId, GenerateOnChainWalletRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<GenerateOnChainWalletResponse>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate", request, HttpMethod.Post, token);
|
||||
return await SendHttpRequest<GenerateOnChainWalletResponse>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/generate", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ public partial class BTCPayServerClient
|
||||
parameters.Add("includeNeighbourData", v);
|
||||
try
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, HttpMethod.Get, token);
|
||||
}
|
||||
catch (GreenfieldAPIException err) when (err.APIError.Code == "wallet-object-not-found")
|
||||
{
|
||||
@ -31,17 +31,17 @@ public partial class BTCPayServerClient
|
||||
parameters.Add("ids", ids);
|
||||
if (query?.IncludeNeighbourData is bool v)
|
||||
parameters.Add("includeNeighbourData", v);
|
||||
return await SendHttpRequest<OnChainWalletObjectData[]>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", parameters, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletObjectData[]>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects", parameters, HttpMethod.Get, token);
|
||||
}
|
||||
public virtual async Task RemoveOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", null, HttpMethod.Delete, token);
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}", null, HttpMethod.Delete, token);
|
||||
}
|
||||
public virtual async Task<OnChainWalletObjectData> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode, AddOnChainWalletObjectRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", request, HttpMethod.Post, token);
|
||||
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
public virtual async Task AddOrUpdateOnChainWalletLink(string storeId, string cryptoCode,
|
||||
@ -49,7 +49,7 @@ public partial class BTCPayServerClient
|
||||
AddOnChainWalletObjectLinkRequest request = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links", request, HttpMethod.Post, token);
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}/links", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
public virtual async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
@ -57,6 +57,6 @@ public partial class BTCPayServerClient
|
||||
OnChainWalletObjectId link,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", null, HttpMethod.Delete, token);
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", null, HttpMethod.Delete, token);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ public partial class BTCPayServerClient
|
||||
public virtual async Task<OnChainWalletOverviewData> ShowOnChainWalletOverview(string storeId, string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet", null, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet", null, HttpMethod.Get, token);
|
||||
}
|
||||
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
|
||||
CancellationToken token = default)
|
||||
@ -24,13 +24,13 @@ public partial class BTCPayServerClient
|
||||
{
|
||||
queryParams.Add("blockTarget", blockTarget);
|
||||
}
|
||||
return await SendHttpRequest<OnChainWalletFeeRateData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feeRate", queryParams, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletFeeRateData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/feerate", queryParams, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletAddressData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>
|
||||
return await SendHttpRequest<OnChainWalletAddressData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/address", new Dictionary<string, object>
|
||||
{
|
||||
{"forceGenerate", forceGenerate}
|
||||
}, HttpMethod.Get, token);
|
||||
@ -39,7 +39,7 @@ public partial class BTCPayServerClient
|
||||
public virtual async Task UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", null, HttpMethod.Delete, token);
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/address", null, HttpMethod.Delete, token);
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
|
||||
@ -59,14 +59,14 @@ public partial class BTCPayServerClient
|
||||
{
|
||||
query.Add(nameof(skip), skip);
|
||||
}
|
||||
return await SendHttpRequest<IEnumerable<OnChainWalletTransactionData>>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<IEnumerable<OnChainWalletTransactionData>>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", query, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletTransactionData> GetOnChainWalletTransaction(
|
||||
string storeId, string cryptoCode, string transactionId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}", null, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions/{transactionId}", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(
|
||||
@ -74,7 +74,7 @@ public partial class BTCPayServerClient
|
||||
PatchOnChainTransactionRequest request,
|
||||
bool force = false, CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<PatchOnChainTransactionRequest, OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}",
|
||||
return await SendHttpRequest<PatchOnChainTransactionRequest, OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions/{transactionId}",
|
||||
new Dictionary<string, object> { {"force", force} }, request, HttpMethod.Patch, token);
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ public partial class BTCPayServerClient
|
||||
string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<IEnumerable<OnChainWalletUTXOData>>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos", null, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<IEnumerable<OnChainWalletUTXOData>>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/utxos", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletTransactionData> CreateOnChainTransaction(string storeId,
|
||||
@ -94,7 +94,7 @@ public partial class BTCPayServerClient
|
||||
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
|
||||
"Please use CreateOnChainTransactionButDoNotBroadcast when wanting to only create the transaction");
|
||||
}
|
||||
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", request, HttpMethod.Post, token);
|
||||
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
public virtual async Task<Transaction> CreateOnChainTransactionButDoNotBroadcast(string storeId,
|
||||
@ -106,6 +106,6 @@ public partial class BTCPayServerClient
|
||||
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
|
||||
"Please use CreateOnChainTransaction when wanting to also broadcast the transaction");
|
||||
}
|
||||
return Transaction.Parse(await SendHttpRequest<string>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", request, HttpMethod.Post, token), network);
|
||||
return Transaction.Parse(await SendHttpRequest<string>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", request, HttpMethod.Post, token), network);
|
||||
}
|
||||
}
|
||||
|
15
BTCPayServer.Client/Models/AppItemStats.cs
Normal file
15
BTCPayServer.Client/Models/AppItemStats.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class AppItemStats
|
||||
{
|
||||
public string ItemCode { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int SalesCount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Total { get; set; }
|
||||
public string TotalFormatted { get; set; }
|
||||
}
|
19
BTCPayServer.Client/Models/AppSalesStats.cs
Normal file
19
BTCPayServer.Client/Models/AppSalesStats.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class AppSalesStats
|
||||
{
|
||||
public int SalesCount { get; set; }
|
||||
public IEnumerable<AppSalesStatsItem> Series { get; set; }
|
||||
}
|
||||
|
||||
public class AppSalesStatsItem
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTime Date { get; set; }
|
||||
public string Label { get; set; }
|
||||
public int SalesCount { get; set; }
|
||||
}
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? StartsAt { get; set; }
|
||||
public string[] PaymentMethods { get; set; }
|
||||
public string[] PayoutMethods { get; set; }
|
||||
public bool AutoApproveClaims { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -22,11 +22,12 @@ namespace BTCPayServer.Client.Models
|
||||
public string PullPaymentId { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public decimal OriginalAmount { get; set; }
|
||||
public string OriginalCurrency { get; set; }
|
||||
public string PayoutCurrency { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? PaymentMethodAmount { get; set; }
|
||||
public decimal? PayoutAmount { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PayoutState State { get; set; }
|
||||
public int Revision { get; set; }
|
||||
|
@ -4,6 +4,6 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FriendlyName { get; set; }
|
||||
public string[] PaymentMethods { get; set; }
|
||||
public string[] PayoutMethods { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string Website { get; set; }
|
||||
|
||||
public string BrandColor { get; set; }
|
||||
public bool ApplyBrandColorToBackend { get; set; }
|
||||
public string LogoUrl { get; set; }
|
||||
public string CssUrl { get; set; }
|
||||
public string PaymentSoundUrl { get; set; }
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
@ -16,7 +18,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 CanModifyWebhooks = "btcpay.store.webhooks.canmodifywebhooks";
|
||||
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
|
||||
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
||||
public const string CanViewReports = "btcpay.store.canviewreports";
|
||||
@ -48,7 +50,7 @@ namespace BTCPayServer.Client
|
||||
yield return CanViewInvoices;
|
||||
yield return CanCreateInvoice;
|
||||
yield return CanModifyInvoices;
|
||||
yield return CanModifyStoreWebhooks;
|
||||
yield return CanModifyWebhooks;
|
||||
yield return CanModifyServerSettings;
|
||||
yield return CanModifyStoreSettings;
|
||||
yield return CanViewStoreSettings;
|
||||
@ -104,6 +106,16 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
return policy.StartsWith("btcpay.user", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static readonly CultureInfo _culture = new (CultureInfo.InvariantCulture.Name);
|
||||
public static string DisplayName(string policy)
|
||||
{
|
||||
var p = policy.Split(".");
|
||||
if (p.Length < 3 || p[0] != "btcpay") return policy;
|
||||
var constName = typeof(Policies).GetFields().Select(f => f.Name).FirstOrDefault(f => f.Equals(p[^1], StringComparison.OrdinalIgnoreCase));
|
||||
var perm = string.IsNullOrEmpty(constName) ? string.Join(' ', p[2..]) : Regex.Replace(constName, "([A-Z])", " $1", RegexOptions.Compiled).Trim();
|
||||
return $"{_culture.TextInfo.ToTitleCase(p[1])}: {_culture.TextInfo.ToTitleCase(perm)}";
|
||||
}
|
||||
}
|
||||
|
||||
public class PermissionSet
|
||||
@ -247,7 +259,7 @@ namespace BTCPayServer.Client
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreWebhooks,
|
||||
Policies.CanModifyWebhooks,
|
||||
Policies.CanModifyPaymentRequests,
|
||||
Policies.CanManagePayouts,
|
||||
Policies.CanUseLightningNodeInStore);
|
||||
|
@ -133,7 +133,7 @@ namespace BTCPayServer
|
||||
|
||||
public string GetTrackedDestination(Script scriptPubKey)
|
||||
{
|
||||
return scriptPubKey.Hash.ToString() + "#" + CryptoCode.ToUpperInvariant();
|
||||
return scriptPubKey.Hash.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,6 @@ namespace BTCPayServer.Data
|
||||
public DbSet<PaymentRequestData> PaymentRequests { get; set; }
|
||||
public DbSet<PaymentData> Payments { get; set; }
|
||||
public DbSet<PayoutData> Payouts { get; set; }
|
||||
public DbSet<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
public DbSet<PlannedTransaction> PlannedTransactions { get; set; }
|
||||
public DbSet<PullPaymentData> PullPayments { get; set; }
|
||||
public DbSet<RefundData> Refunds { get; set; }
|
||||
@ -83,9 +82,8 @@ namespace BTCPayServer.Data
|
||||
PairingCodeData.OnModelCreating(builder);
|
||||
//PayjoinLock.OnModelCreating(builder);
|
||||
PaymentRequestData.OnModelCreating(builder, Database);
|
||||
PaymentData.OnModelCreating(builder, Database);
|
||||
PaymentData.OnModelCreating(builder);
|
||||
PayoutData.OnModelCreating(builder, Database);
|
||||
PendingInvoiceData.OnModelCreating(builder);
|
||||
//PlannedTransaction.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder, Database);
|
||||
RefundData.OnModelCreating(builder);
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -23,7 +21,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
builder.UseLoggerFactory(LoggerFactory);
|
||||
builder.AddInterceptors(Data.InvoiceData.MigrationInterceptor.Instance);
|
||||
builder.AddInterceptors(MigrationInterceptor.Instance);
|
||||
ConfigureBuilder(builder, npgsqlOptionsAction);
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
|
@ -20,5 +20,7 @@
|
||||
<ItemGroup>
|
||||
<None Remove="DBScripts\001.InvoiceFunctions.sql" />
|
||||
<None Remove="DBScripts\002.RefactorPayouts.sql" />
|
||||
<None Remove="DBScripts\003.RefactorPendingInvoicesPayments.sql" />
|
||||
<None Remove="DBScripts\004.MonitoredInvoices.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -0,0 +1,9 @@
|
||||
CREATE OR REPLACE FUNCTION is_pending(status TEXT)
|
||||
RETURNS BOOLEAN AS $$
|
||||
SELECT status = 'Processing' OR status = 'New';
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
CREATE INDEX "IX_Invoices_Pending" ON "Invoices"((1)) WHERE is_pending("Status");
|
||||
CREATE INDEX "IX_Payments_Pending" ON "Payments"((1)) WHERE is_pending("Status");
|
||||
DROP TABLE "PendingInvoices";
|
||||
ANALYZE "Invoices";
|
23
BTCPayServer.Data/DBScripts/004.MonitoredInvoices.sql
Normal file
23
BTCPayServer.Data/DBScripts/004.MonitoredInvoices.sql
Normal file
@ -0,0 +1,23 @@
|
||||
CREATE OR REPLACE FUNCTION get_prompt(invoice_blob JSONB, payment_method_id TEXT)
|
||||
RETURNS JSONB AS $$
|
||||
SELECT invoice_blob->'prompts'->payment_method_id
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_monitored_invoices(arg_payment_method_id TEXT, include_non_activated BOOLEAN)
|
||||
RETURNS TABLE (invoice_id TEXT, payment_id TEXT, payment_method_id TEXT) AS $$
|
||||
WITH cte AS (
|
||||
-- Get all the invoices which are pending. Even if no payments.
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i LEFT JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(i."Status")
|
||||
UNION ALL
|
||||
-- For invoices not pending, take all of those which have pending payments
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i INNER JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(p."Status") AND NOT is_pending(i."Status"))
|
||||
SELECT cte.* FROM cte
|
||||
JOIN "Invoices" i ON cte.invoice_id=i."Id"
|
||||
LEFT JOIN "Payments" p ON cte.payment_id=p."Id" AND cte.payment_method_id=p."PaymentMethodId"
|
||||
WHERE (p."PaymentMethodId" IS NOT NULL AND p."PaymentMethodId" = arg_payment_method_id) OR
|
||||
(p."PaymentMethodId" IS NULL AND get_prompt(i."Blob2", arg_payment_method_id) IS NOT NULL AND
|
||||
(include_non_activated IS TRUE OR (get_prompt(i."Blob2", arg_payment_method_id)->'inactive')::BOOLEAN IS NOT TRUE));
|
||||
$$ LANGUAGE SQL STABLE;
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Data
|
||||
public string Address { get; set; }
|
||||
public InvoiceData InvoiceData { get; set; }
|
||||
public string InvoiceDataId { get; set; }
|
||||
public string PaymentMethodId { get; set; }
|
||||
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
@ -18,7 +19,7 @@ namespace BTCPayServer.Data
|
||||
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
#pragma warning disable CS0618
|
||||
.HasKey(o => o.Address);
|
||||
.HasKey(o => new { o.Address, o.PaymentMethodId });
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
||||
|
@ -46,5 +46,6 @@ namespace BTCPayServer.Data
|
||||
public bool ShowInvoiceStatusChangeHint { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string InvitationToken { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -15,32 +15,13 @@ using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using BTCPayServer.Migrations;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class InvoiceData
|
||||
public partial class InvoiceData : MigrationInterceptor.IHasMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// We have a migration running in the background that will migrate the data from the old blob to the new blob
|
||||
/// Meanwhile, we need to make sure that invoices which haven't been migrated yet are migrated on the fly.
|
||||
/// </summary>
|
||||
public class MigrationInterceptor : IMaterializationInterceptor
|
||||
{
|
||||
public static readonly MigrationInterceptor Instance = new MigrationInterceptor();
|
||||
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
|
||||
{
|
||||
if (entity is InvoiceData invoiceData && invoiceData.Currency is null)
|
||||
{
|
||||
invoiceData.Migrate();
|
||||
}
|
||||
else if (entity is PaymentData paymentData && paymentData.Currency is null)
|
||||
{
|
||||
paymentData.Migrate();
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
static HashSet<string> superflousProperties = new HashSet<string>()
|
||||
{
|
||||
"availableAddressHashes",
|
||||
@ -74,13 +55,14 @@ namespace BTCPayServer.Data
|
||||
};
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public void Migrate()
|
||||
public bool TryMigrate()
|
||||
{
|
||||
if (Currency is not null)
|
||||
return;
|
||||
return false;
|
||||
if (Blob is not (null or { Length: 0 }))
|
||||
{
|
||||
Blob2 = MigrationExtensions.Unzip(Blob);
|
||||
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
|
||||
Blob = null;
|
||||
}
|
||||
var blob = JObject.Parse(Blob2);
|
||||
@ -228,7 +210,7 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
blob.ConvertNumberToString("price");
|
||||
Currency = blob["currency"].Value<string>();
|
||||
Currency = blob["currency"].Value<string>().ToUpperInvariant();
|
||||
var isTopup = blob["type"]?.Value<string>() is "TopUp";
|
||||
var amount = decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
|
||||
Amount = isTopup && amount == 0 ? null : decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
|
||||
@ -368,7 +350,11 @@ namespace BTCPayServer.Data
|
||||
};
|
||||
blob["version"] = 3;
|
||||
Blob2 = blob.ToString(Formatting.None);
|
||||
return true;
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
static string[] detailsRemoveDefault =
|
||||
[
|
||||
"paymentMethodFeeRate",
|
||||
|
@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class InvoiceData : IHasBlobUntyped
|
||||
@ -25,14 +26,14 @@ namespace BTCPayServer.Data
|
||||
public string ExceptionStatus { get; set; }
|
||||
public List<AddressInvoiceData> AddressInvoices { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
public List<InvoiceSearchData> InvoiceSearchData { get; set; }
|
||||
public List<RefundData> Refunds { get; set; }
|
||||
|
||||
public static string GetOrderId(string blob) => throw new NotSupportedException();
|
||||
public static string GetItemCode(string blob) => throw new NotSupportedException();
|
||||
public static bool IsPending(string status) => throw new NotSupportedException();
|
||||
|
||||
[Timestamp]
|
||||
[Timestamp]
|
||||
// With this, update of InvoiceData will fail if the row was modified by another process
|
||||
public uint XMin { get; set; }
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
@ -50,7 +51,7 @@ namespace BTCPayServer.Data
|
||||
.HasColumnType("NUMERIC");
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(GetOrderId), new[] { typeof(string) }), b => b.HasName("get_orderid"));
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(GetItemCode), new[] { typeof(string) }), b => b.HasName("get_itemcode"));
|
||||
|
||||
}
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(IsPending), new[] { typeof(string) }), b => b.HasName("is_pending"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,5 +147,8 @@ namespace BTCPayServer.Data
|
||||
return $"{splitted[0]}-CHAIN";
|
||||
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
|
||||
}
|
||||
|
||||
// Make postgres happy
|
||||
public static string SanitizeJSON(string json) => json.Replace("\\u0000", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
45
BTCPayServer.Data/Data/MigrationInterceptor.cs
Normal file
45
BTCPayServer.Data/Data/MigrationInterceptor.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// We have a migration running in the background that will migrate the data from the old blob to the new blob
|
||||
/// Meanwhile, we need to make sure that invoices which haven't been migrated yet are migrated on the fly.
|
||||
/// </summary>
|
||||
public class MigrationInterceptor : IMaterializationInterceptor, ISaveChangesInterceptor
|
||||
{
|
||||
public interface IHasMigration
|
||||
{
|
||||
bool TryMigrate();
|
||||
bool Migrated { get; set; }
|
||||
}
|
||||
public static readonly MigrationInterceptor Instance = new MigrationInterceptor();
|
||||
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
|
||||
{
|
||||
if (entity is IHasMigration hasMigration && hasMigration.TryMigrate())
|
||||
{
|
||||
hasMigration.Migrated = true;
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
public ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
|
||||
foreach (var entry in eventData.Context.ChangeTracker.Entries())
|
||||
{
|
||||
if (entry is { Entity: IHasMigration { Migrated: true }, State: EntityState.Modified })
|
||||
// It seems doing nothing, but this actually set all properties as modified
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
return new ValueTask<InterceptionResult<int>>(result);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Altcoins;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -13,16 +16,17 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class PaymentData
|
||||
public partial class PaymentData : MigrationInterceptor.IHasMigration
|
||||
{
|
||||
public void Migrate()
|
||||
public bool TryMigrate()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (Currency is not null)
|
||||
return;
|
||||
return false;
|
||||
if (Blob is not (null or { Length: 0 }))
|
||||
{
|
||||
Blob2 = MigrationExtensions.Unzip(Blob);
|
||||
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
|
||||
Blob = null;
|
||||
}
|
||||
var blob = JObject.Parse(Blob2);
|
||||
@ -42,9 +46,10 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
var cryptoCode = blob["cryptoCode"].Value<string>();
|
||||
Type = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
|
||||
Type = MigrationExtensions.MigratePaymentMethodId(Type);
|
||||
var divisibility = MigrationExtensions.GetDivisibility(Type);
|
||||
MigratedPaymentMethodId = PaymentMethodId;
|
||||
PaymentMethodId = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
|
||||
PaymentMethodId = MigrationExtensions.MigratePaymentMethodId(PaymentMethodId);
|
||||
var divisibility = MigrationExtensions.GetDivisibility(PaymentMethodId);
|
||||
Currency = blob["cryptoCode"].Value<string>();
|
||||
blob.Remove("cryptoCode");
|
||||
blob.Remove("cryptoPaymentDataType");
|
||||
@ -157,7 +162,13 @@ namespace BTCPayServer.Data
|
||||
Blob2 = blob.ToString(Formatting.None);
|
||||
Accounted = null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return true;
|
||||
}
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
[NotMapped]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public string MigratedPaymentMethodId { get; set; }
|
||||
|
||||
static readonly DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
public static long DateTimeToMilliUnixTime(in DateTime time)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
@ -26,13 +27,15 @@ namespace BTCPayServer.Data
|
||||
[Obsolete("Use Blob2 instead")]
|
||||
public byte[] Blob { get; set; }
|
||||
public string Blob2 { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string PaymentMethodId { get; set; }
|
||||
[Obsolete("Use Status instead")]
|
||||
public bool? Accounted { get; set; }
|
||||
public PaymentStatus? Status { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
public static bool IsPending(PaymentStatus? status) => throw new NotSupportedException();
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<PaymentData>()
|
||||
.HasKey(o => new { o.Id, o.PaymentMethodId });
|
||||
builder.Entity<PaymentData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
|
||||
@ -47,6 +50,7 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<PaymentData>()
|
||||
.Property(o => o.Amount)
|
||||
.HasColumnType("NUMERIC");
|
||||
builder.HasDbFunction(typeof(PaymentData).GetMethod(nameof(IsPending), new[] { typeof(PaymentStatus?) }), b => b.HasName("is_pending"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class PendingInvoiceData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public InvoiceData InvoiceData { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<PendingInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(o => o.PendingInvoices)
|
||||
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240405052858_cleanup_address_invoices")]
|
||||
public partial class cleanup_address_invoices : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
DELETE FROM ""AddressInvoices"" WHERE ""Address"" LIKE '%_LightningLike';
|
||||
ALTER TABLE ""AddressInvoices"" DROP COLUMN IF EXISTS ""CreatedTime"";
|
||||
");
|
||||
migrationBuilder.Sql(@"VACUUM (FULL, ANALYZE) ""AddressInvoices"";", true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -11,5 +11,30 @@ namespace BTCPayServer.Migrations
|
||||
[DBScript("002.RefactorPayouts.sql")]
|
||||
public partial class migratepayouts : DBScriptsMigration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
base.Up(migrationBuilder);
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Destination",
|
||||
table: "Payouts",
|
||||
newName: "DedupId");
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Payouts_Destination_State",
|
||||
table: "Payouts",
|
||||
newName: "IX_Payouts_DedupId_State");
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PaymentMethod",
|
||||
table: "PayoutProcessors",
|
||||
newName: "PayoutMethodId");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "PayoutProcessors"
|
||||
SET
|
||||
"PayoutMethodId" = CASE WHEN STRPOS("PayoutMethodId", '_') = 0 THEN "PayoutMethodId" || '-CHAIN'
|
||||
WHEN STRPOS("PayoutMethodId", '_LightningLike') > 0 THEN split_part("PayoutMethodId", '_LightningLike', 1) || '-LN'
|
||||
WHEN STRPOS("PayoutMethodId", '_LNURLPAY') > 0 THEN split_part("PayoutMethodId",'_LNURLPAY', 1) || '-LN'
|
||||
ELSE "PayoutMethodId" END
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240904092905_UpdateStoreOwnerRole")]
|
||||
public partial class UpdateStoreOwnerRole : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
"StoreRoles",
|
||||
keyColumns: new[] { "Id" },
|
||||
keyColumnTypes: new[] { "TEXT" },
|
||||
keyValues: new[] { "Owner" },
|
||||
columns: new[] { "Permissions" },
|
||||
columnTypes: new[] { "TEXT[]" },
|
||||
values: new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
"StoreRoles",
|
||||
keyColumns: new[] { "Id" },
|
||||
keyColumnTypes: new[] { "TEXT" },
|
||||
keyValues: new[] { "Owner" },
|
||||
columns: new[] { "Permissions" },
|
||||
columnTypes: new[] { "TEXT[]" },
|
||||
values: new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
"btcpay.store.canmodifystoresettings",
|
||||
"btcpay.store.cantradecustodianaccount",
|
||||
"btcpay.store.canwithdrawfromcustodianaccount",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240906010127_renamecol")]
|
||||
public partial class renamecol : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Destination",
|
||||
table: "Payouts",
|
||||
newName: "DedupId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Payouts_Destination_State",
|
||||
table: "Payouts",
|
||||
newName: "IX_Payouts_DedupId_State");
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PaymentMethod",
|
||||
table: "PayoutProcessors",
|
||||
newName: "PayoutMethodId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240913034505_refactorpendinginvoicespayments")]
|
||||
[DBScript("003.RefactorPendingInvoicesPayments.sql")]
|
||||
public partial class refactorpendinginvoicespayments : DBScriptsMigration
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240919085726_refactorinvoiceaddress")]
|
||||
public partial class refactorinvoiceaddress : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PaymentMethodId",
|
||||
table: "AddressInvoices",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "AddressInvoices"
|
||||
SET
|
||||
"Address" = (string_to_array("Address", '#'))[1],
|
||||
"PaymentMethodId" = CASE WHEN (string_to_array("Address", '#'))[2] IS NULL THEN 'BTC-CHAIN'
|
||||
WHEN STRPOS((string_to_array("Address", '#'))[2], '_') = 0 THEN (string_to_array("Address", '#'))[2] || '-CHAIN'
|
||||
WHEN STRPOS((string_to_array("Address", '#'))[2], '_MoneroLike') > 0 THEN replace((string_to_array("Address", '#'))[2],'_MoneroLike','-CHAIN')
|
||||
WHEN STRPOS((string_to_array("Address", '#'))[2], '_ZcashLike') > 0 THEN replace((string_to_array("Address", '#'))[2],'_ZcashLike','-CHAIN')
|
||||
ELSE '' END;
|
||||
ALTER TABLE "AddressInvoices" DROP COLUMN IF EXISTS "CreatedTime";
|
||||
DELETE FROM "AddressInvoices" WHERE "PaymentMethodId" = '';
|
||||
""");
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices",
|
||||
columns: new[] { "Address", "PaymentMethodId" });
|
||||
migrationBuilder.Sql("VACUUM (ANALYZE) \"AddressInvoices\";", true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PaymentMethodId",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices",
|
||||
column: "Address");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PendingInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PendingInvoices", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PendingInvoices_Invoices_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240923065254_refactorpayments")]
|
||||
public partial class refactorpayments : DBScriptsMigration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Payments",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Type",
|
||||
table: "Payments",
|
||||
newName: "PaymentMethodId");
|
||||
migrationBuilder.Sql("UPDATE \"Payments\" SET \"PaymentMethodId\"='' WHERE \"PaymentMethodId\" IS NULL;");
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Payments",
|
||||
table: "Payments",
|
||||
columns: new[] { "Id", "PaymentMethodId" });
|
||||
base.Up(migrationBuilder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240924065254_monitoredinvoices")]
|
||||
[DBScript("004.MonitoredInvoices.sql")]
|
||||
public partial class monitoredinvoices : DBScriptsMigration
|
||||
{
|
||||
}
|
||||
}
|
@ -63,10 +63,13 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PaymentMethodId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Address");
|
||||
b.HasKey("Address", "PaymentMethodId");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
@ -479,6 +482,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PaymentMethodId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool?>("Accounted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@ -503,10 +509,7 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
b.HasKey("Id", "PaymentMethodId");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
@ -634,16 +637,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("PayoutProcessors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PlannedTransaction", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -1331,17 +1324,6 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("InvoiceData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PullPaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1572,8 +1554,6 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.Navigation("Payments");
|
||||
|
||||
b.Navigation("PendingInvoices");
|
||||
|
||||
b.Navigation("Refunds");
|
||||
});
|
||||
|
||||
|
@ -34,7 +34,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="125.0.6422.14100" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="128.0.6613.11900" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -177,7 +177,7 @@ namespace BTCPayServer.Tests
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
.AddFilter("Microsoft.EntityFrameworkCore.Migrations", LogLevel.Information)
|
||||
.AddFilter("Fido2NetLib.DistributedCacheMetadataService", LogLevel.Error)
|
||||
.AddProvider(LoggerProvider);
|
||||
});
|
||||
|
@ -58,8 +58,8 @@ namespace BTCPayServer.Tests
|
||||
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
var clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
|
||||
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text);
|
||||
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
Assert.StartsWith("bcrt", address);
|
||||
Assert.DoesNotContain("lightning=", payUrl);
|
||||
Assert.Equal($"bitcoin:{address}", payUrl);
|
||||
Assert.Equal($"bitcoin:{address}", clipboard);
|
||||
@ -80,7 +80,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
Assert.StartsWith("lightning:lnurl", payUrl);
|
||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text);
|
||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC-CHAIN"));
|
||||
});
|
||||
|
||||
@ -94,7 +94,7 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center-start")).Text;
|
||||
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center")).GetAttribute("data-text");
|
||||
Assert.Equal($"lightning:{address}", payUrl);
|
||||
Assert.Equal($"lightning:{address}", clipboard);
|
||||
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
|
||||
@ -147,7 +147,7 @@ namespace BTCPayServer.Tests
|
||||
invoiceId = s.CreateInvoice(2100, "EUR");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
await Task.Delay(200);
|
||||
address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
|
||||
address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
var amountFraction = "0.00001";
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Parse(amountFraction));
|
||||
@ -261,8 +261,8 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
|
||||
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text;
|
||||
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
Assert.StartsWith($"bitcoin:{copyAddressOnchain}?amount=", payUrl);
|
||||
Assert.Contains("?amount=", payUrl);
|
||||
Assert.Contains("&lightning=", payUrl);
|
||||
@ -327,8 +327,8 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
|
||||
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text;
|
||||
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
Assert.StartsWith($"bitcoin:{copyAddressOnchain}", payUrl);
|
||||
Assert.Contains("?lightning=lnurl", payUrl);
|
||||
Assert.DoesNotContain("amount=", payUrl);
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -42,6 +43,13 @@ namespace BTCPayServer.Tests
|
||||
}), _loggerFactory);
|
||||
}
|
||||
|
||||
public InvoiceRepository GetInvoiceRepository()
|
||||
{
|
||||
var logs = new BTCPayServer.Logging.Logs();
|
||||
logs.Configure(_loggerFactory);
|
||||
return new InvoiceRepository(CreateContextFactory(), new EventAggregator(logs));
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateContext() => CreateContextFactory().CreateContext();
|
||||
|
||||
public async Task MigrateAsync()
|
||||
@ -59,18 +67,21 @@ namespace BTCPayServer.Tests
|
||||
await conn.ExecuteAsync($"CREATE DATABASE \"{dbname}\";");
|
||||
}
|
||||
|
||||
public async Task MigrateUntil(string migration)
|
||||
public async Task MigrateUntil(string migration = null)
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var db = ctx.Database.GetDbConnection();
|
||||
await EnsureCreatedAsync();
|
||||
var migrations = ctx.Database.GetMigrations().ToArray();
|
||||
var untilMigrationIdx = Array.IndexOf(migrations, migration);
|
||||
if (untilMigrationIdx == -1)
|
||||
throw new InvalidOperationException($"Migration {migration} not found");
|
||||
notAppliedMigrations = migrations[untilMigrationIdx..];
|
||||
await db.ExecuteAsync("CREATE TABLE IF NOT EXISTS \"__EFMigrationsHistory\" (\"MigrationId\" TEXT, \"ProductVersion\" TEXT)");
|
||||
await db.ExecuteAsync("INSERT INTO \"__EFMigrationsHistory\" VALUES (@migration, '8.0.0')", notAppliedMigrations.Select(m => new { migration = m }).ToArray());
|
||||
if (migration is not null)
|
||||
{
|
||||
var untilMigrationIdx = Array.IndexOf(migrations, migration);
|
||||
if (untilMigrationIdx == -1)
|
||||
throw new InvalidOperationException($"Migration {migration} not found");
|
||||
notAppliedMigrations = migrations[untilMigrationIdx..];
|
||||
await db.ExecuteAsync("CREATE TABLE IF NOT EXISTS \"__EFMigrationsHistory\" (\"MigrationId\" TEXT, \"ProductVersion\" TEXT)");
|
||||
await db.ExecuteAsync("INSERT INTO \"__EFMigrationsHistory\" VALUES (@migration, '8.0.0')", notAppliedMigrations.Select(m => new { migration = m }).ToArray());
|
||||
}
|
||||
await ctx.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,9 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Altcoins;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -18,6 +20,130 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanQueryMonitoredInvoices()
|
||||
{
|
||||
var tester = CreateDBTester();
|
||||
await tester.MigrateUntil();
|
||||
var invoiceRepository = tester.GetInvoiceRepository();
|
||||
using var ctx = tester.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
|
||||
async Task AddPrompt(string invoiceId, string paymentMethodId, bool activated = true)
|
||||
{
|
||||
JObject prompt = new JObject();
|
||||
if (!activated)
|
||||
prompt["inactive"] = true;
|
||||
prompt["currency"] = "USD";
|
||||
var query = """
|
||||
UPDATE "Invoices" SET "Blob2" = jsonb_set('{"prompts": {}}'::JSONB || COALESCE("Blob2",'{}'), ARRAY['prompts','@paymentMethodId'], '@prompt'::JSONB)
|
||||
WHERE "Id" = '@invoiceId'
|
||||
""";
|
||||
query = query.Replace("@paymentMethodId", paymentMethodId);
|
||||
query = query.Replace("@prompt", prompt.ToString());
|
||||
query = query.Replace("@invoiceId", invoiceId);
|
||||
Assert.Equal(1, await conn.ExecuteAsync(query));
|
||||
}
|
||||
|
||||
await conn.ExecuteAsync("""
|
||||
INSERT INTO "Invoices" ("Id", "Created", "Status","Currency") VALUES
|
||||
('BTCOnly', NOW(), 'New', 'USD'),
|
||||
('LTCOnly', NOW(), 'New', 'USD'),
|
||||
('LTCAndBTC', NOW(), 'New', 'USD'),
|
||||
('LTCAndBTCLazy', NOW(), 'New', 'USD')
|
||||
""");
|
||||
foreach (var invoiceId in new string[] { "LTCOnly", "LTCAndBTCLazy", "LTCAndBTC" })
|
||||
{
|
||||
await AddPrompt(invoiceId, "LTC-CHAIN", true);
|
||||
}
|
||||
foreach (var invoiceId in new string[] { "BTCOnly", "LTCAndBTC" })
|
||||
{
|
||||
await AddPrompt(invoiceId, "BTC-CHAIN", true);
|
||||
}
|
||||
await AddPrompt("LTCAndBTCLazy", "BTC-CHAIN", false);
|
||||
|
||||
var btc = PaymentMethodId.Parse("BTC-CHAIN");
|
||||
var ltc = PaymentMethodId.Parse("LTC-CHAIN");
|
||||
var invoices = await invoiceRepository.GetMonitoredInvoices(btc);
|
||||
Assert.Equal(2, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(btc, true);
|
||||
Assert.Equal(3, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC", "LTCAndBTCLazy" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(ltc);
|
||||
Assert.Equal(3, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "LTCAndBTC", "LTCAndBTC", "LTCAndBTCLazy" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
|
||||
await conn.ExecuteAsync("""
|
||||
INSERT INTO "Payments" ("Id", "InvoiceDataId", "PaymentMethodId", "Status", "Blob2", "Created", "Amount", "Currency") VALUES
|
||||
('1','LTCAndBTC', 'LTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('2','LTCAndBTC', 'BTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('3','LTCAndBTC', 'BTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('4','LTCAndBTC', 'BTC-CHAIN', 'Settled', '{}'::JSONB, NOW(), 123, 'USD');
|
||||
|
||||
INSERT INTO "AddressInvoices" ("InvoiceDataId", "Address", "PaymentMethodId") VALUES
|
||||
('LTCAndBTC', 'BTC1', 'BTC-CHAIN'),
|
||||
('LTCAndBTC', 'BTC2', 'BTC-CHAIN'),
|
||||
('LTCAndBTC', 'LTC1', 'LTC-CHAIN');
|
||||
""");
|
||||
|
||||
var invoice = Assert.Single(await invoiceRepository.GetMonitoredInvoices(ltc), i => i.Id == "LTCAndBTC");
|
||||
var payment = Assert.Single(invoice.GetPayments(false));
|
||||
Assert.Equal("1", payment.Id);
|
||||
|
||||
foreach (var includeNonActivated in new[] { true, false })
|
||||
{
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(btc, includeNonActivated);
|
||||
invoice = Assert.Single(invoices, i => i.Id == "LTCAndBTC");
|
||||
var payments = invoice.GetPayments(false);
|
||||
Assert.Equal(3, payments.Count);
|
||||
|
||||
foreach (var paymentId in new[] { "2", "3", "4" })
|
||||
{
|
||||
Assert.Contains(payments, p => p.Id == paymentId);
|
||||
}
|
||||
Assert.Equal(2, invoice.Addresses.Count);
|
||||
foreach (var addr in new[] { "BTC1", "BTC2" })
|
||||
{
|
||||
Assert.Contains(invoice.Addresses, p => p.Address == addr);
|
||||
}
|
||||
if (!includeNonActivated)
|
||||
Assert.DoesNotContain(invoices, i => i.Id == "LTCAndBTCLazy");
|
||||
else
|
||||
Assert.Contains(invoices, i => i.Id == "LTCAndBTCLazy");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanMigrateInvoiceAddresses()
|
||||
{
|
||||
var tester = CreateDBTester();
|
||||
await tester.MigrateUntil("20240919085726_refactorinvoiceaddress");
|
||||
using var ctx = tester.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
await conn.ExecuteAsync("INSERT INTO \"Invoices\" (\"Id\", \"Created\") VALUES ('i', NOW())");
|
||||
await conn.ExecuteAsync(
|
||||
"INSERT INTO \"AddressInvoices\" VALUES ('aaa#BTC', 'i'),('bbb','i'),('ccc#BTC_LNU', 'i'),('ddd#XMR_MoneroLike', 'i'),('eee#ZEC_ZcashLike', 'i')");
|
||||
await tester.ContinueMigration();
|
||||
foreach (var v in new[] { ("aaa", "BTC-CHAIN"), ("bbb", "BTC-CHAIN"), ("ddd", "XMR-CHAIN") , ("eee", "ZEC-CHAIN") })
|
||||
{
|
||||
var ok = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"AddressInvoices\" WHERE \"Address\"=@a AND \"PaymentMethodId\"=@b", new { a = v.Item1, b = v.Item2 });
|
||||
Assert.True(ok);
|
||||
}
|
||||
var notok = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"AddressInvoices\" WHERE \"Address\"='ccc'");
|
||||
Assert.False(notok);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanMigratePayoutsAndPullPayments()
|
||||
{
|
||||
|
@ -20,6 +20,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
@ -505,7 +506,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
|
||||
Assert.Equal(1.1m, accounting.Due);
|
||||
Assert.Equal(1.1m, accounting.TotalDue);
|
||||
|
||||
@ -2246,7 +2246,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
Data.InvoiceData data = new Data.InvoiceData();
|
||||
obj = data;
|
||||
data.Blob2 = v.Input.ToString();
|
||||
data.Migrate();
|
||||
data.TryMigrate();
|
||||
var actual = JObject.Parse(data.Blob2);
|
||||
AssertSameJson(v.Expected, actual);
|
||||
if (!v.SkipRountripTest)
|
||||
@ -2266,7 +2266,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
//data.
|
||||
obj = data;
|
||||
data.Blob2 = v.Input.ToString();
|
||||
data.Migrate();
|
||||
data.TryMigrate();
|
||||
var actual = JObject.Parse(data.Blob2);
|
||||
AssertSameJson(v.Expected, actual);
|
||||
if (!v.SkipRountripTest)
|
||||
@ -2381,7 +2381,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
});
|
||||
var data = new Data.InvoiceData();
|
||||
data.Blob2 = o.ToString();
|
||||
data.Migrate();
|
||||
data.TryMigrate();
|
||||
var migrated = JObject.Parse(data.Blob2);
|
||||
return migrated["prompts"]["BTC-CHAIN"]["details"]["accountDerivation"].Value<string>();
|
||||
})
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -13,9 +14,14 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -30,6 +36,7 @@ using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -678,6 +685,93 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(apps[2].Archived);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanGetAppStats()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
await user.MakeAdmin();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
|
||||
var item1 = new ViewPointOfSaleViewModel.Item { Id = "item1", Title = "Item 1", Price = 1, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var item2 = new ViewPointOfSaleViewModel.Item { Id = "item2", Title = "Item 2", Price = 2, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var item3 = new ViewPointOfSaleViewModel.Item { Id = "item3", Title = "Item 3", Price = 3, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var posItems = AppService.SerializeTemplate([item1, item2, item3]);
|
||||
var posApp = await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest { AppName = "test pos", Template = posItems, });
|
||||
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test crowdfund" });
|
||||
|
||||
// empty states
|
||||
var posSales = await client.GetAppSales(posApp.Id);
|
||||
Assert.NotNull(posSales);
|
||||
Assert.Equal(0, posSales.SalesCount);
|
||||
Assert.Equal(7, posSales.Series.Count());
|
||||
|
||||
var crowdfundSales = await client.GetAppSales(crowdfundApp.Id);
|
||||
Assert.NotNull(crowdfundSales);
|
||||
Assert.Equal(0, crowdfundSales.SalesCount);
|
||||
Assert.Equal(7, crowdfundSales.Series.Count());
|
||||
|
||||
var posTopItems = await client.GetAppTopItems(posApp.Id);
|
||||
Assert.Empty(posTopItems);
|
||||
|
||||
var crowdfundItems = await client.GetAppTopItems(crowdfundApp.Id);
|
||||
Assert.Empty(crowdfundItems);
|
||||
|
||||
// with sales - fiddle invoices via the UI controller
|
||||
var uiPosController = tester.PayTester.GetController<UIPointOfSaleController>();
|
||||
|
||||
var action = Assert.IsType<RedirectToActionResult>(uiPosController.ViewPointOfSale(posApp.Id, PosViewType.Static, 1, choiceKey: item1.Id).GetAwaiter().GetResult());
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
Assert.True(action.RouteValues!.TryGetValue("invoiceId", out var i1Id));
|
||||
|
||||
var cart = new JObject {
|
||||
["cart"] = new JArray
|
||||
{
|
||||
new JObject { ["id"] = item2.Id, ["count"] = 4 },
|
||||
new JObject { ["id"] = item3.Id, ["count"] = 2 }
|
||||
},
|
||||
["subTotal"] = 14,
|
||||
["total"] = 14,
|
||||
["amounts"] = new JArray()
|
||||
}.ToString();
|
||||
action = Assert.IsType<RedirectToActionResult>(uiPosController.ViewPointOfSale(posApp.Id, PosViewType.Cart, 7, posData: cart).GetAwaiter().GetResult());
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
Assert.True(action.RouteValues!.TryGetValue("invoiceId", out var i2Id));
|
||||
|
||||
await user.PayInvoice(i1Id!.ToString());
|
||||
await user.PayInvoice(i2Id!.ToString());
|
||||
|
||||
posSales = await client.GetAppSales(posApp.Id);
|
||||
Assert.Equal(7, posSales.SalesCount);
|
||||
Assert.Equal(7, posSales.Series.Count());
|
||||
Assert.Equal(0, posSales.Series.First().SalesCount);
|
||||
Assert.Equal(7, posSales.Series.Last().SalesCount);
|
||||
|
||||
posTopItems = await client.GetAppTopItems(posApp.Id);
|
||||
Assert.Equal(3, posTopItems.Count);
|
||||
Assert.Equal(item2.Id, posTopItems[0].ItemCode);
|
||||
Assert.Equal(4, posTopItems[0].SalesCount);
|
||||
|
||||
Assert.Equal(item3.Id, posTopItems[1].ItemCode);
|
||||
Assert.Equal(2, posTopItems[1].SalesCount);
|
||||
|
||||
Assert.Equal(item1.Id, posTopItems[2].ItemCode);
|
||||
Assert.Equal(1, posTopItems[2].SalesCount);
|
||||
|
||||
// with count and offset
|
||||
posTopItems = await client.GetAppTopItems(posApp.Id,1, 5);
|
||||
Assert.Equal(2, posTopItems.Count);
|
||||
Assert.Equal(item3.Id, posTopItems[0].ItemCode);
|
||||
Assert.Equal(2, posTopItems[0].SalesCount);
|
||||
|
||||
Assert.Equal(item1.Id, posTopItems[1].ItemCode);
|
||||
Assert.Equal(1, posTopItems[1].SalesCount);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanDeleteUsersViaApi()
|
||||
@ -1010,7 +1104,7 @@ namespace BTCPayServer.Tests
|
||||
Description = "Test description",
|
||||
Amount = 12.3m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
void VerifyResult()
|
||||
@ -1041,7 +1135,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 2",
|
||||
Amount = 12.3m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
BOLT11Expiration = TimeSpan.FromDays(31.0)
|
||||
});
|
||||
Assert.Equal(TimeSpan.FromDays(31.0), test2.BOLT11Expiration);
|
||||
@ -1088,13 +1182,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
payouts = await unauthenticated.GetPayouts(pps[0].Id);
|
||||
var payout2 = Assert.Single(payouts);
|
||||
Assert.Equal(payout.Amount, payout2.Amount);
|
||||
Assert.Equal(payout.OriginalAmount, payout2.OriginalAmount);
|
||||
Assert.Equal(payout.Id, payout2.Id);
|
||||
Assert.Equal(destination, payout2.Destination);
|
||||
Assert.Equal(PayoutState.AwaitingApproval, payout.State);
|
||||
Assert.Equal("BTC-CHAIN", payout2.PayoutMethodId);
|
||||
Assert.Equal("BTC", payout2.CryptoCode);
|
||||
Assert.Null(payout.PaymentMethodAmount);
|
||||
Assert.Equal("BTC", payout2.PayoutCurrency);
|
||||
Assert.Null(payout.PayoutAmount);
|
||||
|
||||
TestLogs.LogInformation("Can't overdraft");
|
||||
|
||||
@ -1136,7 +1230,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 12.3m,
|
||||
StartsAt = start,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
Assert.Equal(start, inFuture.StartsAt);
|
||||
Assert.Null(inFuture.ExpiresAt);
|
||||
@ -1154,7 +1248,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 12.3m,
|
||||
ExpiresAt = expires,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
await this.AssertAPIError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest()
|
||||
{
|
||||
@ -1178,7 +1272,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test USD",
|
||||
Amount = 5000m,
|
||||
Currency = "USD",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
await this.AssertAPIError("lnurl-not-supported", async () => await unauthenticated.GetPullPaymentLNURL(pp.Id));
|
||||
@ -1203,8 +1297,8 @@ namespace BTCPayServer.Tests
|
||||
Revision = payout.Revision
|
||||
});
|
||||
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
|
||||
Assert.NotNull(payout.PaymentMethodAmount);
|
||||
Assert.Equal(1.0m, payout.PaymentMethodAmount); // 1 BTC == 5000 USD in tests
|
||||
Assert.NotNull(payout.PayoutAmount);
|
||||
Assert.Equal(1.0m, payout.PayoutAmount); // 1 BTC == 5000 USD in tests
|
||||
await this.AssertAPIError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
|
||||
{
|
||||
Revision = payout.Revision
|
||||
@ -1216,7 +1310,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 2",
|
||||
Amount = 12.303228134m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
|
||||
payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
|
||||
@ -1226,8 +1320,8 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
|
||||
// The payout should round the value of the payment down to the network of the payment method
|
||||
Assert.Equal(12.30322814m, payout.PaymentMethodAmount);
|
||||
Assert.Equal(12.303228134m, payout.Amount);
|
||||
Assert.Equal(12.30322814m, payout.PayoutAmount);
|
||||
Assert.Equal(12.303228134m, payout.OriginalAmount);
|
||||
|
||||
await client.MarkPayoutPaid(storeId, payout.Id);
|
||||
payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
|
||||
@ -1240,7 +1334,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 3",
|
||||
Amount = 12.303228134m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
@ -1315,7 +1409,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test SATS",
|
||||
Amount = 21000,
|
||||
Currency = "SATS",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
@ -1333,7 +1427,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
AutoApproveClaims = true
|
||||
});
|
||||
});
|
||||
@ -1353,7 +1447,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
AutoApproveClaims = true
|
||||
});
|
||||
|
||||
@ -1529,6 +1623,7 @@ namespace BTCPayServer.Tests
|
||||
CssUrl = "https://example.org/style.css",
|
||||
LogoUrl = "https://example.org/logo.svg",
|
||||
BrandColor = "#003366",
|
||||
ApplyBrandColorToBackend = true,
|
||||
PaymentMethodCriteria = new List<PaymentMethodCriteriaData>
|
||||
{
|
||||
new()
|
||||
@ -1544,6 +1639,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("https://example.org/style.css", updatedStore.CssUrl);
|
||||
Assert.Equal("https://example.org/logo.svg", updatedStore.LogoUrl);
|
||||
Assert.Equal("#003366", updatedStore.BrandColor);
|
||||
Assert.True(updatedStore.ApplyBrandColorToBackend);
|
||||
var s = (await client.GetStore(newStore.Id));
|
||||
Assert.Equal("B", s.Name);
|
||||
var pmc = Assert.Single(s.PaymentMethodCriteria);
|
||||
@ -1711,7 +1807,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var clientProfile = await user.CreateClient(Policies.CanModifyStoreWebhooks, Policies.CanCreateInvoice);
|
||||
var clientProfile = await user.CreateClient(Policies.CanModifyWebhooks, Policies.CanCreateInvoice);
|
||||
var hook = await clientProfile.CreateWebhook(user.StoreId, new CreateStoreWebhookRequest()
|
||||
{
|
||||
Url = fakeServer.ServerUri.AbsoluteUri,
|
||||
@ -2616,6 +2712,14 @@ namespace BTCPayServer.Tests
|
||||
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
|
||||
Assert.DoesNotContain(invoiceObject.Links.Select(l => l.Type), t => t == "address");
|
||||
|
||||
// Check if we can get the monitored invoice
|
||||
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var includeNonActivated = true;
|
||||
Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
|
||||
includeNonActivated = false;
|
||||
Assert.DoesNotContain(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
|
||||
Assert.DoesNotContain(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoice.Id);
|
||||
//
|
||||
|
||||
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
|
||||
Assert.Single(paymentMethods);
|
||||
@ -3105,6 +3209,8 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
|
||||
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC", xpub)).Addresses.First().Address);
|
||||
// Testing if the rewrite rule to old API path is working
|
||||
await viewOnlyClient.SendHttpRequest($"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview", new JObject() { ["config"] = xpub.ToString() }, HttpMethod.Post);
|
||||
|
||||
var method = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = JValue.CreateString(xpub.ToString())});
|
||||
|
||||
@ -3384,6 +3490,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
|
||||
});
|
||||
|
||||
// Testing if the rewrite rule to old API path is working
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.SendHttpRequest($"api/v1/stores/{walletId.StoreId}/payment-methods/onchain/{walletId.CryptoCode}/wallet/address", null as object);
|
||||
});
|
||||
var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
|
||||
var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
|
||||
var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true);
|
||||
@ -4010,7 +4122,12 @@ namespace BTCPayServer.Tests
|
||||
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
|
||||
Assert.Equal(PayResult.Ok, resp.Result);
|
||||
|
||||
|
||||
var store = tester.PayTester.GetService<StoreRepository>();
|
||||
Assert.True(await store.InternalNodePayoutAuthorized(admin.StoreId));
|
||||
Assert.False(await store.InternalNodePayoutAuthorized("blah"));
|
||||
await admin.MakeAdmin(false);
|
||||
Assert.False(await store.InternalNodePayoutAuthorized(admin.StoreId));
|
||||
await admin.MakeAdmin(true);
|
||||
|
||||
var customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
|
||||
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
|
||||
@ -4071,7 +4188,7 @@ namespace BTCPayServer.Tests
|
||||
PayoutMethodId = "BTC_LightningNetwork",
|
||||
Destination = customerInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
Assert.Equal(payout2.OriginalAmount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
@ -4115,7 +4232,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
var notapprovedPayoutWithPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
@ -4141,7 +4258,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Equal(3, payouts.Length);
|
||||
Assert.Empty(payouts.Where(data => data.State == PayoutState.AwaitingApproval));
|
||||
Assert.Empty(payouts.Where(data => data.PaymentMethodAmount is null));
|
||||
Assert.Empty(payouts.Where(data => data.PayoutAmount is null));
|
||||
|
||||
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
|
||||
|
||||
@ -4154,12 +4271,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
|
||||
|
||||
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PaymentMethods));
|
||||
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PayoutMethods));
|
||||
//still too poor to process any payouts
|
||||
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
|
||||
|
||||
|
||||
await adminClient.RemovePayoutProcessor(admin.StoreId, tpGen.Name, tpGen.PaymentMethods.First());
|
||||
await adminClient.RemovePayoutProcessor(admin.StoreId, tpGen.Name, tpGen.PayoutMethods.First());
|
||||
|
||||
Assert.Empty(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
|
@ -50,10 +50,8 @@ namespace BTCPayServer.Tests
|
||||
tester.Driver.FindElement(By.Name("Name")).SendKeys("English (Custom)");
|
||||
tester.ClickPagePrimary();
|
||||
var translations = tester.Driver.FindElement(By.Name("Translations"));
|
||||
var text = translations.Text;
|
||||
text = text.Replace("Password => Password", "Password => Mot de passe");
|
||||
translations.Clear();
|
||||
translations.SendKeys("Password => Mot de passe");
|
||||
translations.SendKeys("{ \"Password\": \"Mot de passe\" }");
|
||||
tester.ClickPagePrimary();
|
||||
|
||||
// Check English (Custom) can be selected
|
||||
@ -64,7 +62,7 @@ namespace BTCPayServer.Tests
|
||||
// Check if we can remove English (Custom)
|
||||
tester.LogIn();
|
||||
tester.GoToServer(Views.Server.ServerNavPages.Translations);
|
||||
text = tester.Driver.PageSource;
|
||||
var text = tester.Driver.PageSource;
|
||||
Assert.Contains("Select-Cypherpunk", text);
|
||||
Assert.DoesNotContain("Select-English (Custom)", text);
|
||||
// Cypherpunk is loaded from file, can't edit
|
||||
|
@ -55,6 +55,7 @@ namespace BTCPayServer.Tests
|
||||
options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}");
|
||||
options.AddArgument("shm-size=2g");
|
||||
options.AddArgument("start-maximized");
|
||||
options.AddArgument("disable-search-engine-choice-screen");
|
||||
if (Server.PayTester.InContainer)
|
||||
{
|
||||
// Shot in the dark to fix https://stackoverflow.com/questions/53902507/unknown-error-session-deleted-because-of-page-crash-from-unknown-error-cannot
|
||||
@ -546,7 +547,6 @@ retry:
|
||||
{
|
||||
walletId ??= WalletId;
|
||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
for (var i = 0; i < coins; i++)
|
||||
|
@ -19,6 +19,7 @@ using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
@ -371,7 +372,7 @@ namespace BTCPayServer.Tests
|
||||
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||
s.ClickPagePrimary();
|
||||
var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text;
|
||||
var url = s.Driver.FindElement(By.Id("InvitationUrl")).GetAttribute("data-text");
|
||||
|
||||
s.Logout();
|
||||
s.Driver.Navigate().GoToUrl(url);
|
||||
@ -402,6 +403,84 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("/login", s.Driver.Url);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanManageUsers()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
//Create Users
|
||||
s.RegisterNewUser();
|
||||
var user = s.AsTestAccount();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser(true);
|
||||
var admin = s.AsTestAccount();
|
||||
s.GoToHome();
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
|
||||
// Manage user password reset
|
||||
var rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .reset-password")).Click();
|
||||
s.Driver.WaitForElement(By.Id("Password")).SendKeys("Password@1!");
|
||||
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("Password@1!");
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Password successfully set", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
|
||||
// Manage user status (disable and enable)
|
||||
// Disable user
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .disable-user")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
|
||||
Assert.Contains("User disabled", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
//Enable user
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .enable-user")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
|
||||
Assert.Contains("User enabled", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
|
||||
// Manage user details (edit)
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click();
|
||||
s.Driver.WaitForElement(By.Id("Name")).SendKeys("Test User");
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("User successfully updated", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
|
||||
// Manage user deletion
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .delete-user")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
|
||||
Assert.Contains("User deleted", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
|
||||
s.Driver.AssertNoError();
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanRequireApprovalForNewAccounts()
|
||||
{
|
||||
@ -745,6 +824,14 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
|
||||
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
|
||||
|
||||
// zero amount invoice should redirect to receipt
|
||||
var zeroAmountId = s.CreateInvoice(0);
|
||||
s.GoToUrl($"/i/{zeroAmountId}");
|
||||
Assert.EndsWith("/receipt", s.Driver.Url);
|
||||
Assert.Contains("$0.00", s.Driver.PageSource);
|
||||
s.GoToInvoice(zeroAmountId);
|
||||
Assert.Equal("Settled", s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge]")).Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1544,7 +1631,6 @@ namespace BTCPayServer.Tests
|
||||
s.GenerateWallet("BTC", "", false, true);
|
||||
var walletId = new WalletId(storeId, "BTC");
|
||||
s.GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
s.Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
var address = BitcoinAddress.Create(addressStr,
|
||||
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
|
||||
@ -1749,7 +1835,6 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
|
||||
|
||||
//generate a receiving address
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
// no previous page in the wizard, hence no back button
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
@ -1771,10 +1856,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(s.Driver.FindElement(By.CssSelector("[data-value='test-label']")));
|
||||
});
|
||||
|
||||
//unreserve
|
||||
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
|
||||
//generate it again, should be the same one as before as nothing got used in the meantime
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
|
||||
TestUtils.Eventually(() =>
|
||||
@ -1864,7 +1945,7 @@ namespace BTCPayServer.Tests
|
||||
// Check the tx sent earlier arrived
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
s.Driver.FindElement(By.PartialLinkText(tx.ToString()));
|
||||
s.Driver.FindElement(By.CssSelector($"[data-text='{tx}']"));
|
||||
|
||||
var walletTransactionUri = new Uri(s.Driver.Url);
|
||||
|
||||
@ -1892,13 +1973,13 @@ namespace BTCPayServer.Tests
|
||||
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
SetTransactionOutput(s, 0, jack, 0.01m);
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
|
||||
s.Driver.WaitForElement(By.CssSelector("button[value=broadcast]"));
|
||||
Assert.Contains(jack.ToString(), s.Driver.PageSource);
|
||||
Assert.Contains("0.01000000", s.Driver.PageSource);
|
||||
Assert.EndsWith("psbt/ready", s.Driver.Url);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
|
||||
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>()).CryptoInfo.First().PaymentUrls.BIP21;
|
||||
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(), s.Server.PayTester.GetService<CurrencyNameTable>()).CryptoInfo.First().PaymentUrls.BIP21;
|
||||
//let's make bip21 more interesting
|
||||
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
|
||||
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
|
||||
@ -2682,9 +2763,9 @@ namespace BTCPayServer.Tests
|
||||
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Custom Amount 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Custom Amount 2", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
@ -2705,9 +2786,9 @@ namespace BTCPayServer.Tests
|
||||
sums = paymentDetails.FindElements(By.CssSelector("tr.sums-data"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("Custom Amount 1", items[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("Custom Amount 2", items[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector(".val")).Text);
|
||||
@ -2768,7 +2849,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Custom Amount 1", items[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Total", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
@ -2787,7 +2868,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("Custom Amount 1", items[2].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Total", sums[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector(".val")).Text);
|
||||
@ -3012,7 +3093,7 @@ namespace BTCPayServer.Tests
|
||||
// Topup Invoice test
|
||||
var i = s.CreateInvoice(storeId, null, cryptoCode);
|
||||
s.GoToInvoiceCheckout(i);
|
||||
var lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
|
||||
var lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
|
||||
var parsed = LNURL.LNURL.Parse(lnurl, out var tag);
|
||||
var fetchedReuqest =
|
||||
Assert.IsType<LNURL.LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
|
||||
@ -3049,7 +3130,7 @@ namespace BTCPayServer.Tests
|
||||
i = s.CreateInvoice(storeId, 0.0000001m, cryptoCode);
|
||||
s.GoToInvoiceCheckout(i);
|
||||
// BOLT11 is also displayed for standard invoice (not LNURL, even if it is available)
|
||||
var bolt11 = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center-start")).Text;
|
||||
var bolt11 = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center")).GetAttribute("data-text");
|
||||
BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
|
||||
var invoiceId = s.Driver.Url.Split('/').Last();
|
||||
using (var resp = await s.Server.PayTester.HttpClient.GetAsync("BTC/lnurl/pay/i/" + invoiceId))
|
||||
@ -3102,7 +3183,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
i = s.CreateInvoice(storeId, null, cryptoCode);
|
||||
s.GoToInvoiceCheckout(i);
|
||||
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
|
||||
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
|
||||
Assert.StartsWith("lnurlp", lnurl);
|
||||
LNURL.LNURL.Parse(lnurl, out tag);
|
||||
|
||||
@ -3115,7 +3196,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
var invForPP = s.CreateInvoice(null, cryptoCode);
|
||||
s.GoToInvoiceCheckout(invForPP);
|
||||
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
|
||||
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
|
||||
LNURL.LNURL.Parse(lnurl, out tag);
|
||||
|
||||
// Check that pull payment has lightning option
|
||||
|
@ -247,8 +247,6 @@ namespace BTCPayServer.Tests
|
||||
get; set;
|
||||
}
|
||||
|
||||
readonly HttpClient _Http = new HttpClient();
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
|
@ -527,7 +527,7 @@ retry:
|
||||
{
|
||||
var server = new FakeServer();
|
||||
await server.Start();
|
||||
var client = await CreateClient(Policies.CanModifyStoreWebhooks);
|
||||
var client = await CreateClient(Policies.CanModifyWebhooks);
|
||||
var wh = await client.CreateWebhook(StoreId, new CreateStoreWebhookRequest()
|
||||
{
|
||||
AutomaticRedelivery = false,
|
||||
@ -698,11 +698,13 @@ retry:
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
isHeader = true;
|
||||
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"PaymentMethodId\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
{
|
||||
foreach (var invoice in oldPayments)
|
||||
{
|
||||
var localPayment = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
|
||||
// Old data could have Type to null.
|
||||
localPayment += "UNKNOWN";
|
||||
await writer.WriteLineAsync(localPayment);
|
||||
}
|
||||
await writer.FlushAsync();
|
||||
|
@ -604,7 +604,7 @@
|
||||
},
|
||||
"expectedProperties": {
|
||||
"Created": "04/23/2019 18:27:56 +00:00",
|
||||
"Type": "BTC-CHAIN",
|
||||
"PaymentMethodId": "BTC-CHAIN",
|
||||
"Currency": "BTC",
|
||||
"Status": "Settled",
|
||||
"Amount": "0.07299962",
|
||||
@ -634,7 +634,7 @@
|
||||
},
|
||||
"expectedProperties": {
|
||||
"Created": "10/01/2018 14:13:22 +00:00",
|
||||
"Type": "BTC-CHAIN",
|
||||
"PaymentMethodId": "BTC-CHAIN",
|
||||
"Currency": "BTC",
|
||||
"Status": "Settled",
|
||||
"Amount": "0.00017863",
|
||||
@ -666,7 +666,7 @@
|
||||
"Created": "03/21/2024 07:24:35 +00:00",
|
||||
"CreatedInMs": "1711005875969",
|
||||
"Amount": "0.00000001",
|
||||
"Type": "BTC-LNURL",
|
||||
"PaymentMethodId": "BTC-LNURL",
|
||||
"Currency": "BTC"
|
||||
}
|
||||
},
|
||||
@ -697,7 +697,7 @@
|
||||
"Created": "03/20/2024 22:39:08 +00:00",
|
||||
"CreatedInMs": "1710974348741",
|
||||
"Amount": "0.00197864",
|
||||
"Type": "BTC-CHAIN",
|
||||
"PaymentMethodId": "BTC-CHAIN",
|
||||
"Currency": "BTC",
|
||||
"Status": "Settled",
|
||||
"Accounted": null
|
||||
|
@ -1463,7 +1463,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Currency = "BTC",
|
||||
Amount = 1.0m,
|
||||
PaymentMethods = [ "BTC-CHAIN" ]
|
||||
PayoutMethods = [ "BTC-CHAIN" ]
|
||||
});
|
||||
var controller = user.GetController<UIInvoiceController>();
|
||||
var invoice = await controller.CreateInvoiceCoreRaw(new()
|
||||
@ -1479,7 +1479,7 @@ namespace BTCPayServer.Tests
|
||||
var payout = Assert.Single(payouts);
|
||||
Assert.Equal("TOPUP", payout.PayoutMethodId);
|
||||
Assert.Equal(invoice.Id, payout.Destination);
|
||||
Assert.Equal(-0.5m, payout.Amount);
|
||||
Assert.Equal(-0.5m, payout.OriginalAmount);
|
||||
});
|
||||
}
|
||||
|
||||
@ -2449,9 +2449,10 @@ namespace BTCPayServer.Tests
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
{
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
|
||||
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
|
||||
return (ctx.AddressInvoices.Where(i => i.InvoiceDataId == invoice.Id).ToArrayAsync().GetAwaiter()
|
||||
.GetResult())
|
||||
.Where(i => i.GetAddress() == h).Any();
|
||||
.Where(i => i.Address == h && i.PaymentMethodId == pmi.ToString()).Any();
|
||||
}
|
||||
|
||||
|
||||
@ -2835,7 +2836,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("coingecko", b.PreferredExchange);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanDoInvoiceMigrations()
|
||||
@ -3157,7 +3157,15 @@ namespace BTCPayServer.Tests
|
||||
var invoiceId = GetInvoiceId(resp);
|
||||
await acc.PayOnChain(invoiceId);
|
||||
|
||||
app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest
|
||||
// Quick unrelated test on GetMonitoredInvoices
|
||||
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoiceId);
|
||||
Assert.Single(monitored.Payments);
|
||||
monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), true), i => i.Id == invoiceId);
|
||||
Assert.Single(monitored.Payments);
|
||||
//
|
||||
|
||||
app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest
|
||||
{
|
||||
AppName = "Cart",
|
||||
DefaultView = PosViewType.Cart,
|
||||
|
@ -12,7 +12,6 @@ services:
|
||||
args:
|
||||
CONFIGURATION_NAME: Release
|
||||
environment:
|
||||
TESTS_EXPERIMENTALV2_CONFIRM: "true"
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
|
@ -12,7 +12,6 @@ services:
|
||||
args:
|
||||
CONFIGURATION_NAME: Release
|
||||
environment:
|
||||
TESTS_EXPERIMENTALV2_CONFIRM: "true"
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=btcpayserver
|
||||
|
@ -83,7 +83,7 @@ curl -s -k -X PUT -H 'Content-Type: application/json' \
|
||||
# Fund Satoshis Steaks wallet
|
||||
btcaddress_satoshis_steaks=$(curl -s -k -X GET -H 'Content-Type: application/json' \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-CHAIN/wallet/address" | jq -r '.address')
|
||||
|
||||
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_satoshis_steaks" 6.15 >/dev/null 2>&1
|
||||
|
||||
@ -167,7 +167,7 @@ printf "Nakamoto Nuggets Cart POS ID: %s\n" "$cart_app_id_nakamoto_nuggets"
|
||||
# Fund Nakamoto Nuggets wallet
|
||||
btcaddress_nakamoto_nuggets=$(curl -s -k -X GET -H 'Content-Type: application/json' \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-CHAIN/wallet/address" | jq -r '.address')
|
||||
|
||||
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_nakamoto_nuggets" 6.15 >/dev/null 2>&1
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
@ -12,6 +13,6 @@ public class AppSalesViewModel
|
||||
public string AppUrl { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
public long SalesCount { get; set; }
|
||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
||||
public IEnumerable<AppSalesStatsItem> Series { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
@ -10,7 +11,7 @@ public class AppTopItemsViewModel
|
||||
public string AppType { get; set; }
|
||||
public string AppUrl { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
public List<ItemStats> Entries { get; set; }
|
||||
public List<AppItemStats> Entries { get; set; }
|
||||
public List<int> SalesCount { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
@ -1,22 +1,21 @@
|
||||
@model BTCPayServer.Components.TruncateCenter.TruncateCenterViewModel
|
||||
@{
|
||||
var classes = string.IsNullOrEmpty(Model.Classes) ? string.Empty : Model.Classes.Trim();
|
||||
var isTruncated = !string.IsNullOrEmpty(Model.Start) && !string.IsNullOrEmpty(Model.End);
|
||||
@if (Model.Copy) classes += " truncate-center--copy";
|
||||
@if (Model.Elastic) classes += " truncate-center--elastic";
|
||||
var prefix = Model.IsVue ? ":" : "";
|
||||
}
|
||||
<span class="truncate-center @classes" id="@Model.Id" data-text="@Model.Text">
|
||||
<span class="truncate-center @classes" id="@Model.Id" @(prefix)data-text="@Model.Text">
|
||||
@if (Model.IsVue)
|
||||
{
|
||||
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title="@Model.Text">
|
||||
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title="@Model.Text">
|
||||
@if (Model.Elastic)
|
||||
{
|
||||
<span class="truncate-center-start" v-text="@Model.Text"></span>
|
||||
<span class="truncate-center-start" v-text="@(Model.Text).slice(0, @(Model.Text).length - @(Model.Padding))"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="truncate-center-start" v-text="@(Model.Text).length > 2 * @(Model.Padding) ? (@(Model.Text).slice(0, @(Model.Padding)) + '…') : @(Model.Text)"></span>
|
||||
<span class="truncate-center-start" v-text="@(Model.Text).slice(0, @(Model.Padding)) + @(Model.Text).length > 2 * @(Model.Padding) ? '…' : ''"></span>
|
||||
}
|
||||
<span class="truncate-center-end" v-text="@(Model.Text).slice(-@(Model.Padding))" v-if="@(Model.Text).length > 2 * @(Model.Padding)"></span>
|
||||
</span>
|
||||
@ -24,12 +23,9 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="truncate-center-truncated" @(isTruncated ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>
|
||||
<span class="truncate-center-start">@(Model.Elastic || !isTruncated ? Model.Text : $"{Model.Start}…")</span>
|
||||
@if (isTruncated)
|
||||
{
|
||||
<span class="truncate-center-end">@Model.End</span>
|
||||
}
|
||||
<span class="truncate-center-truncated" @(Model.IsTruncated ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>
|
||||
<span class="truncate-center-start">@Model.Start</span>
|
||||
<span class="truncate-center-end">@Model.End</span>
|
||||
</span>
|
||||
<span class="truncate-center-text">@Model.Text</span>
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ public class TruncateCenter : ViewComponent
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
|
||||
var vm = new TruncateCenterViewModel
|
||||
{
|
||||
Classes = classes,
|
||||
@ -28,12 +29,15 @@ public class TruncateCenter : ViewComponent
|
||||
Copy = copy,
|
||||
Text = text,
|
||||
Link = link,
|
||||
Id = id
|
||||
Id = id,
|
||||
IsTruncated = text.Length > 2 * padding
|
||||
};
|
||||
if (!isVue && text.Length > 2 * padding)
|
||||
if (!vm.IsVue)
|
||||
{
|
||||
vm.Start = text[..padding];
|
||||
vm.End = text[^padding..];
|
||||
vm.Start = vm.IsTruncated ? text[..padding] : text;
|
||||
vm.End = vm.IsTruncated ? text[^padding..] : string.Empty;
|
||||
if (!vm.Elastic && vm.IsTruncated)
|
||||
vm.Start = $"{vm.Start}…";
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
namespace BTCPayServer.Components.TruncateCenter
|
||||
#nullable enable
|
||||
namespace BTCPayServer.Components.TruncateCenter;
|
||||
|
||||
public class TruncateCenterViewModel
|
||||
{
|
||||
public class TruncateCenterViewModel
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public string Start { get; set; }
|
||||
public string End { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Classes { get; set; }
|
||||
public string Link { get; set; }
|
||||
public int Padding { get; set; }
|
||||
public bool Copy { get; set; }
|
||||
public bool Elastic { get; set; }
|
||||
public bool IsVue { get; set; }
|
||||
}
|
||||
public string Text { get; init; } = null!;
|
||||
public string? Start { get; set; }
|
||||
public string? End { get; set; }
|
||||
public string? Id { get; init; }
|
||||
public string? Classes { get; init; }
|
||||
public string? Link { get; init; }
|
||||
public int Padding { get; init; }
|
||||
public bool Copy { get; init; }
|
||||
public bool Elastic { get; init; }
|
||||
public bool IsVue { get; init; }
|
||||
public bool IsTruncated { get; init; }
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
@ -27,14 +28,17 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly UIInvoiceController _InvoiceController;
|
||||
private readonly Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
|
||||
public BitpayInvoiceController(UIInvoiceController invoiceController,
|
||||
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_bitpayExtensions = bitpayExtensions;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
@ -59,7 +63,7 @@ namespace BTCPayServer.Controllers
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
return new DataWrapper<InvoiceResponse>(invoice.EntityToDTO(_bitpayExtensions, Url));
|
||||
return new DataWrapper<InvoiceResponse>(invoice.EntityToDTO(_bitpayExtensions, Url, _currencyNameTable));
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
@ -93,7 +97,7 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO(_bitpayExtensions, Url)).ToArray();
|
||||
.Select((o) => o.EntityToDTO(_bitpayExtensions, Url, _currencyNameTable)).ToArray();
|
||||
|
||||
return Json(DataWrapper.Create(entities));
|
||||
}
|
||||
@ -103,7 +107,7 @@ namespace BTCPayServer.Controllers
|
||||
CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
|
||||
{
|
||||
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken, entityManipulator);
|
||||
var resp = entity.EntityToDTO(_bitpayExtensions, Url);
|
||||
var resp = entity.EntityToDTO(_bitpayExtensions, Url, _currencyNameTable);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
|
@ -226,6 +226,30 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/apps/{appId}/sales")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetAppSales(string appId, [FromQuery] int numberOfDays = 7)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||
if (app == null) return AppNotFound();
|
||||
|
||||
var stats = await _appService.GetSalesStats(app, numberOfDays);
|
||||
return Ok(stats);
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/apps/{appId}/top-items")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetAppTopItems(string appId, [FromQuery] int offset = 0, [FromQuery] int count = 10)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||
if (app == null) return AppNotFound();
|
||||
|
||||
var stats = (await _appService.GetItemStats(app)).ToList();
|
||||
var max = Math.Min(count, stats.Count - offset);
|
||||
var items = stats.GetRange(offset, max);
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
private IActionResult AppNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
||||
|
@ -446,7 +446,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Name = request.Name ?? $"Refund {invoice.Id}",
|
||||
Description = request.Description,
|
||||
StoreId = storeId,
|
||||
PayoutMethodIds = new[] { payoutMethodId },
|
||||
PayoutMethods = new[] { payoutMethodId },
|
||||
};
|
||||
|
||||
if (request.RefundVariant != RefundVariant.Custom)
|
||||
|
@ -94,6 +94,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> GetNotificationSettings()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
return NotFound();
|
||||
var model = GetNotificationSettingsData(user);
|
||||
return Ok(model);
|
||||
}
|
||||
@ -103,6 +105,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> UpdateNotificationSettings(UpdateNotificationSettingsRequest request)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
return NotFound();
|
||||
if (request.Disabled.Contains("all"))
|
||||
{
|
||||
user.DisabledNotifications = "all";
|
||||
|
@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Name = factory.Processor,
|
||||
FriendlyName = factory.FriendlyName,
|
||||
PaymentMethods = factory.GetSupportedPayoutMethods().Select(id => id.ToString())
|
||||
PayoutMethods = factory.GetSupportedPayoutMethods().Select(id => id.ToString())
|
||||
.ToArray()
|
||||
}));
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.BOLT11Expiration), $"The BOLT11 expiration should be positive");
|
||||
}
|
||||
PayoutMethodId?[]? payoutMethods = null;
|
||||
if (request.PaymentMethods is { } payoutMethodsStr)
|
||||
if (request.PayoutMethods is { } payoutMethodsStr)
|
||||
{
|
||||
payoutMethods = payoutMethodsStr.Select(s =>
|
||||
{
|
||||
@ -144,13 +144,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
if (!supported.Contains(payoutMethods[i]))
|
||||
{
|
||||
request.AddModelError(paymentRequest => paymentRequest.PaymentMethods[i], "Invalid or unsupported payment method", this);
|
||||
request.AddModelError(paymentRequest => paymentRequest.PayoutMethods[i], "Invalid or unsupported payment method", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PaymentMethods), "This field is required");
|
||||
ModelState.AddModelError(nameof(request.PayoutMethods), "This field is required");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
@ -364,16 +364,17 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Id = p.Id,
|
||||
PullPaymentId = p.PullPaymentDataId,
|
||||
Date = p.Date,
|
||||
Amount = p.OriginalAmount,
|
||||
PaymentMethodAmount = p.Amount,
|
||||
OriginalCurrency = p.OriginalCurrency,
|
||||
OriginalAmount = p.OriginalAmount,
|
||||
PayoutCurrency = p.Currency,
|
||||
PayoutAmount = p.Amount,
|
||||
Revision = blob.Revision,
|
||||
State = p.State,
|
||||
PayoutMethodId = p.PayoutMethodId,
|
||||
PaymentProof = p.GetProofBlobJson(),
|
||||
Destination = blob.Destination,
|
||||
Metadata = blob.Metadata?? new JObject(),
|
||||
};
|
||||
model.Destination = blob.Destination;
|
||||
model.PayoutMethodId = p.PayoutMethodId;
|
||||
model.CryptoCode = p.Currency;
|
||||
model.PaymentProof = p.GetProofBlobJson();
|
||||
return model;
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ public class GreenfieldServerRolesController : ControllerBase
|
||||
[HttpGet("~/api/v1/server/roles")]
|
||||
public async Task<IActionResult> GetServerRoles()
|
||||
{
|
||||
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false, false)));
|
||||
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false)));
|
||||
}
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
{
|
||||
|
4
BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs
4
BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs
@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
PayoutMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
});
|
||||
|
||||
return Ok(configured.Select(ToModel).ToArray());
|
||||
@ -88,7 +88,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = new[] { pmi }
|
||||
PayoutMethods = new[] { pmi }
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
activeProcessor ??= new PayoutProcessorData();
|
||||
|
@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
PayoutMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
});
|
||||
|
||||
return Ok(configured.Select(ToModel).ToArray());
|
||||
@ -94,7 +94,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = new[] { payoutMethodId }
|
||||
PayoutMethods = new[] { payoutMethodId }
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
activeProcessor ??= new PayoutProcessorData();
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public partial class GreenfieldStoreOnChainPaymentMethodsController
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate")]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/generate")]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> GenerateOnChainWallet(string storeId,
|
||||
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
|
||||
|
@ -66,11 +66,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
protected JsonHttpException ErrorPaymentMethodNotConfigured()
|
||||
{
|
||||
return new JsonHttpException(this.CreateAPIError(404, "paymentmethod-not-configured", "The lightning node is not set up"));
|
||||
return new JsonHttpException(this.CreateAPIError(404, "paymentmethod-not-configured", "The payment method is not configured"));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/preview")]
|
||||
public IActionResult GetOnChainPaymentMethodPreview(
|
||||
string storeId,
|
||||
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
|
||||
@ -87,7 +87,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview")]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/preview")]
|
||||
public async Task<IActionResult> GetProposedOnChainPaymentMethodPreview(
|
||||
string storeId,
|
||||
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
|
||||
|
@ -95,10 +95,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet")]
|
||||
public async Task<IActionResult> ShowOnChainWalletOverview(string storeId, string cryptoCode)
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet")]
|
||||
public async Task<IActionResult> ShowOnChainWalletOverview(string storeId, string paymentMethodId)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
@ -115,10 +115,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feerate")]
|
||||
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null)
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/feerate")]
|
||||
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string paymentMethodId, int? blockTarget = null)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out _, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
@ -131,15 +131,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
|
||||
public async Task<IActionResult> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode,
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/address")]
|
||||
public async Task<IActionResult> GetOnChainWalletReceiveAddress(string storeId, string paymentMethodId,
|
||||
bool forceGenerate = false)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var kpi = await _walletReceiveService.GetOrGenerate(new WalletId(storeId, cryptoCode), forceGenerate);
|
||||
var kpi = await _walletReceiveService.GetOrGenerate(new WalletId(storeId, network.CryptoCode), forceGenerate);
|
||||
if (kpi is null)
|
||||
{
|
||||
return BadRequest();
|
||||
@ -151,7 +151,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey,
|
||||
Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
|
||||
new { cryptoCode })));
|
||||
new { network.CryptoCode })));
|
||||
}
|
||||
|
||||
return Ok(new OnChainWalletAddressData()
|
||||
@ -163,28 +163,28 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
|
||||
public async Task<IActionResult> UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode)
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/address")]
|
||||
public async Task<IActionResult> UnReserveOnChainWalletReceiveAddress(string storeId, string paymentMethodId)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out _,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out _, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var addr = await _walletReceiveService.UnReserveAddress(new WalletId(storeId, cryptoCode));
|
||||
var addr = await _walletReceiveService.UnReserveAddress(new WalletId(storeId, network.CryptoCode));
|
||||
if (addr is null)
|
||||
{
|
||||
return this.CreateAPIError("no-reserved-address",
|
||||
$"There was no reserved address for {cryptoCode} on this store.");
|
||||
$"There was no reserved address for {network.CryptoCode} on this store.");
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/transactions")]
|
||||
public async Task<IActionResult> ShowOnChainWalletTransactions(
|
||||
string storeId,
|
||||
string cryptoCode,
|
||||
string paymentMethodId,
|
||||
[FromQuery] TransactionStatus[]? statusFilter = null,
|
||||
[FromQuery] string? labelFilter = null,
|
||||
[FromQuery] int skip = 0,
|
||||
@ -192,12 +192,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, (string[]?)null);
|
||||
|
||||
var preFiltering = true;
|
||||
@ -238,11 +238,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}")]
|
||||
public async Task<IActionResult> GetOnChainWalletTransaction(string storeId, string cryptoCode,
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/transactions/{transactionId}")]
|
||||
public async Task<IActionResult> GetOnChainWalletTransaction(string storeId, string paymentMethodId,
|
||||
string transactionId)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
@ -253,7 +253,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(404, "transaction-not-found", "The transaction was not found.");
|
||||
}
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
var walletTransactionsInfoAsync =
|
||||
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] { transactionId })).Values
|
||||
.FirstOrDefault();
|
||||
@ -263,16 +263,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPatch(
|
||||
"~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}")]
|
||||
"~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/transactions/{transactionId}")]
|
||||
public async Task<IActionResult> PatchOnChainWalletTransaction(
|
||||
string storeId,
|
||||
string cryptoCode,
|
||||
string paymentMethodId,
|
||||
string transactionId,
|
||||
[FromBody] PatchOnChainTransactionRequest request,
|
||||
bool force = false
|
||||
)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
@ -283,7 +283,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(404, "transaction-not-found", "The transaction was not found.");
|
||||
}
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
var txObjectId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
|
||||
|
||||
if (request.Comment != null)
|
||||
@ -305,20 +305,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos")]
|
||||
public async Task<IActionResult> GetOnChainWalletUTXOs(string storeId, string cryptoCode)
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/utxos")]
|
||||
public async Task<IActionResult> GetOnChainWalletUTXOs(string storeId, string paymentMethodId)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId,
|
||||
utxos.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray());
|
||||
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
|
||||
var pmi = PaymentMethodId.Parse(paymentMethodId);
|
||||
return Ok(utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1);
|
||||
@ -347,17 +347,17 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions")]
|
||||
public async Task<IActionResult> CreateOnChainTransaction(string storeId, string cryptoCode,
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/transactions")]
|
||||
public async Task<IActionResult> CreateOnChainTransaction(string storeId, string paymentMethodId,
|
||||
[FromBody] CreateOnChainTransactionRequest request)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
if (network.ReadonlyWallet)
|
||||
{
|
||||
return this.CreateAPIError(503, "not-available",
|
||||
$"{cryptoCode} sending services are not currently available");
|
||||
$"{network.CryptoCode} sending services are not currently available");
|
||||
}
|
||||
|
||||
//This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation.
|
||||
@ -387,7 +387,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient(cryptoCode);
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient(network);
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
||||
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation, request.ExcludeUnconfirmed);
|
||||
@ -552,7 +552,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (!derivationScheme.IsHotWallet || signingKeyStr is null)
|
||||
{
|
||||
return this.CreateAPIError(503, "not-available",
|
||||
$"{cryptoCode} sending services are not currently available");
|
||||
$"{network.CryptoCode} sending services are not currently available");
|
||||
}
|
||||
|
||||
var signingKey = ExtKey.Parse(signingKeyStr, network.NBitcoinNetwork);
|
||||
@ -599,12 +599,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
payjoinPSBT.Finalize();
|
||||
var payjoinTransaction = payjoinPSBT.ExtractTransaction();
|
||||
var hash = payjoinTransaction.GetHash();
|
||||
await this._walletRepository.AddWalletTransactionAttachment(new WalletId(Store.Id, cryptoCode),
|
||||
await this._walletRepository.AddWalletTransactionAttachment(new WalletId(Store.Id, network.CryptoCode),
|
||||
hash, Attachment.Payjoin());
|
||||
broadcastResult = await explorerClient.BroadcastAsync(payjoinTransaction);
|
||||
if (broadcastResult.Success)
|
||||
{
|
||||
return await GetOnChainWalletTransaction(storeId, cryptoCode, hash.ToString());
|
||||
return await GetOnChainWalletTransaction(storeId, paymentMethodId, hash.ToString());
|
||||
}
|
||||
}
|
||||
catch (PayjoinException)
|
||||
@ -621,7 +621,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
broadcastResult = await explorerClient.BroadcastAsync(transaction);
|
||||
if (broadcastResult.Success)
|
||||
{
|
||||
return await GetOnChainWalletTransaction(storeId, cryptoCode, transactionHash.ToString());
|
||||
return await GetOnChainWalletTransaction(storeId, paymentMethodId, transactionHash.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -629,9 +629,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/objects")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetOnChainWalletObjects(string storeId, string cryptoCode, string? type = null, [FromQuery(Name = "ids")] string[]? ids = null, bool? includeNeighbourData = null)
|
||||
public async Task<IActionResult> GetOnChainWalletObjects(string storeId, string paymentMethodId, string? type = null, [FromQuery(Name = "ids")] string[]? ids = null, bool? includeNeighbourData = null)
|
||||
{
|
||||
if (ids?.Length is 0 && !Request.Query.ContainsKey("ids"))
|
||||
ids = null;
|
||||
@ -639,28 +639,34 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(ids), "If ids is specified, type should be specified");
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network, out var actionResult))
|
||||
return actionResult;
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
return Ok((await _walletRepository.GetWalletObjects(new(walletId, type, ids) { IncludeNeighbours = includeNeighbourData ?? true })).Select(kv => kv.Value).Select(ToModel).ToArray());
|
||||
}
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/objects/{objectType}/{objectId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetOnChainWalletObject(string storeId, string cryptoCode,
|
||||
public async Task<IActionResult> GetOnChainWalletObject(string storeId, string paymentMethodId,
|
||||
string objectType, string objectId,
|
||||
bool? includeNeighbourData = null)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network, out var actionResult))
|
||||
return actionResult;
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
var wo = await _walletRepository.GetWalletObject(new(walletId, objectType, objectId), includeNeighbourData ?? true);
|
||||
if (wo is null)
|
||||
return WalletObjectNotFound();
|
||||
return Ok(ToModel(wo));
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}")]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/objects/{objectType}/{objectId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> RemoveOnChainWalletObject(string storeId, string cryptoCode,
|
||||
public async Task<IActionResult> RemoveOnChainWalletObject(string storeId, string paymentMethodId,
|
||||
string objectType, string objectId)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network, out var actionResult))
|
||||
return actionResult;
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
if (await _walletRepository.RemoveWalletObjects(new WalletObjectId(walletId, objectType, objectId)))
|
||||
return Ok();
|
||||
else
|
||||
@ -672,11 +678,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(404, "wallet-object-not-found", "This wallet object's can't be found");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects")]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/objects")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode,
|
||||
public async Task<IActionResult> AddOrUpdateOnChainWalletObject(string storeId,
|
||||
string paymentMethodId,
|
||||
[FromBody] AddOnChainWalletObjectRequest request)
|
||||
{
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network, out var actionResult))
|
||||
return actionResult;
|
||||
if (request?.Type is null)
|
||||
ModelState.AddModelError(nameof(request.Type), "Type is required");
|
||||
if (request?.Id is null)
|
||||
@ -684,13 +693,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
|
||||
try
|
||||
{
|
||||
await _walletRepository.SetWalletObject(
|
||||
new WalletObjectId(walletId, request!.Type, request.Id), request.Data);
|
||||
return await GetOnChainWalletObject(storeId, cryptoCode, request!.Type, request.Id);
|
||||
return await GetOnChainWalletObject(storeId, network.CryptoCode, request!.Type, request.Id);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
@ -698,12 +707,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}/links")]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/objects/{objectType}/{objectId}/links")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> AddOrUpdateOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
public async Task<IActionResult> AddOrUpdateOnChainWalletLinks(string storeId, string paymentMethodId,
|
||||
string objectType, string objectId,
|
||||
[FromBody] AddOnChainWalletObjectLinkRequest request)
|
||||
{
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network, out var actionResult))
|
||||
return actionResult;
|
||||
if (request?.Type is null)
|
||||
ModelState.AddModelError(nameof(request.Type), "Type is required");
|
||||
if (request?.Id is null)
|
||||
@ -711,7 +722,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
try
|
||||
{
|
||||
await _walletRepository.SetWalletObjectLink(
|
||||
@ -726,13 +737,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}/links/{linkType}/{linkId}")]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/objects/{objectType}/{objectId}/links/{linkType}/{linkId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> RemoveOnChainWalletLink(string storeId, string cryptoCode,
|
||||
public async Task<IActionResult> RemoveOnChainWalletLink(string storeId, string paymentMethodId,
|
||||
string objectType, string objectId,
|
||||
string linkType, string linkId)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network, out var actionResult))
|
||||
return actionResult;
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
if (await _walletRepository.RemoveWalletObjectLink(
|
||||
new WalletObjectId(walletId, objectType, objectId),
|
||||
new WalletObjectId(walletId, linkType, linkId)))
|
||||
@ -769,31 +782,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return await _authorizationService.CanUseHotWallet(PoliciesSettings, User);
|
||||
}
|
||||
|
||||
private bool IsInvalidWalletRequest(string cryptoCode, [MaybeNullWhen(true)] out BTCPayNetwork network,
|
||||
private bool IsInvalidWalletRequest(string paymentMethodId, [MaybeNullWhen(true)] out BTCPayNetwork network,
|
||||
[MaybeNullWhen(true)] out DerivationSchemeSettings derivationScheme,
|
||||
[MaybeNullWhen(false)] out IActionResult actionResult)
|
||||
{
|
||||
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
|
||||
derivationScheme = null;
|
||||
if (!_handlers.TryGetValue(pmi, out var handler))
|
||||
{
|
||||
throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode",
|
||||
"This crypto code isn't set up in this BTCPay Server instance"));
|
||||
}
|
||||
network = ((IHasNetwork)handler).Network;
|
||||
|
||||
if (!network.WalletSupported || !_btcPayWalletProvider.IsAvailable(network))
|
||||
{
|
||||
actionResult = this.CreateAPIError(503, "not-available",
|
||||
$"{cryptoCode} services are not currently available");
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out network, out actionResult))
|
||||
return true;
|
||||
}
|
||||
|
||||
derivationScheme = GetDerivationSchemeSettings(cryptoCode);
|
||||
derivationScheme = GetDerivationSchemeSettings(network.CryptoCode);
|
||||
if (derivationScheme?.AccountDerivation is null)
|
||||
{
|
||||
actionResult = this.CreateAPIError(503, "not-available",
|
||||
$"{cryptoCode} doesn't have any derivation scheme set");
|
||||
$"{network.CryptoCode} doesn't have any derivation scheme set");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -801,6 +802,28 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsInvalidWalletRequest(string paymentMethodId, [MaybeNullWhen(true)] out BTCPayNetwork network,
|
||||
[MaybeNullWhen(false)] out IActionResult actionResult)
|
||||
{
|
||||
if (!PaymentMethodId.TryParse(paymentMethodId, out var pmi)
|
||||
|| !_handlers.TryGetValue(pmi, out var handler)
|
||||
|| handler is not IHasNetwork { Network: { WalletSupported: true } })
|
||||
{
|
||||
throw new JsonHttpException(this.CreateAPIError(404, "unknown-paymentMethodId",
|
||||
"This payment method doesn't exists or doesn't offer wallet services"));
|
||||
}
|
||||
network = ((IHasNetwork)handler).Network;
|
||||
|
||||
if (!_btcPayWalletProvider.IsAvailable(network))
|
||||
{
|
||||
actionResult = this.CreateAPIError(503, "not-available",
|
||||
$"{pmi} services are not currently available");
|
||||
return true;
|
||||
}
|
||||
actionResult = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private DerivationSchemeSettings? GetDerivationSchemeSettings(string cryptoCode)
|
||||
{
|
||||
return Store.GetPaymentMethodConfig<DerivationSchemeSettings>(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), _handlers);
|
||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Name = datas.Key,
|
||||
FriendlyName = _factories.FirstOrDefault(factory => factory.Processor == datas.Key)?.FriendlyName,
|
||||
PaymentMethods = datas.Select(data => data.PayoutMethodId).ToArray()
|
||||
PayoutMethods = datas.Select(data => data.PayoutMethodId).ToArray()
|
||||
});
|
||||
return Ok(configured);
|
||||
|
||||
@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { processor },
|
||||
PayoutMethodIds = new[] { PayoutMethodId.Parse(paymentMethod) }
|
||||
PayoutMethods = new[] { PayoutMethodId.Parse(paymentMethod) }
|
||||
})).FirstOrDefault();
|
||||
if (matched is null)
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var store = HttpContext.GetStoreData();
|
||||
return store == null
|
||||
? StoreNotFound()
|
||||
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false, false)));
|
||||
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false)));
|
||||
}
|
||||
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield,
|
||||
Policy = Policies.CanModifyStoreWebhooks)]
|
||||
Policy = Policies.CanModifyWebhooks)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreWebhooksController : ControllerBase
|
||||
{
|
||||
|
@ -175,6 +175,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Website = data.StoreWebsite,
|
||||
Archived = data.Archived,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
ApplyBrandColorToBackend = storeBlob.ApplyBrandColorToBackend,
|
||||
CssUrl = storeBlob.CssUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.CssUrl),
|
||||
LogoUrl = storeBlob.LogoUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.LogoUrl),
|
||||
PaymentSoundUrl = storeBlob.PaymentSoundUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.PaymentSoundUrl),
|
||||
@ -255,6 +256,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.PaymentTolerance = restModel.PaymentTolerance;
|
||||
blob.PayJoinEnabled = restModel.PayJoinEnabled;
|
||||
blob.BrandColor = restModel.BrandColor;
|
||||
blob.ApplyBrandColorToBackend = restModel.ApplyBrandColorToBackend;
|
||||
blob.LogoUrl = restModel.LogoUrl is null ? null : UnresolvedUri.Create(restModel.LogoUrl);
|
||||
blob.CssUrl = restModel.CssUrl is null ? null : UnresolvedUri.Create(restModel.CssUrl);
|
||||
blob.PaymentSoundUrl = restModel.PaymentSoundUrl is null ? null : UnresolvedUri.Create(restModel.PaymentSoundUrl);
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Controllers.GreenField;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -1142,6 +1143,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await GetController<GreenfieldAppsController>().GetAllApps());
|
||||
}
|
||||
|
||||
public override async Task<AppSalesStats> GetAppSales(string appId, int numberOfDays = 7, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<AppSalesStats>(
|
||||
await GetController<GreenfieldAppsController>().GetAppSales(appId, numberOfDays));
|
||||
}
|
||||
|
||||
public override async Task<List<AppItemStats>> GetAppTopItems(string appId, int offset = 0, int count = 10, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<List<AppItemStats>>(
|
||||
await GetController<GreenfieldAppsController>().GetAppTopItems(appId, offset, count));
|
||||
}
|
||||
|
||||
public override async Task DeleteApp(string appId, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteApp(appId));
|
||||
|
@ -16,6 +16,7 @@ using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -706,7 +707,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet("/login/forgot-password")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ForgotPassword()
|
||||
public ActionResult ForgotPassword()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
@ -717,7 +718,8 @@ namespace BTCPayServer.Controllers
|
||||
[RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>();
|
||||
if (ModelState.IsValid && settings?.IsComplete() is true)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (!UserService.TryCanLogin(user, out _))
|
||||
@ -739,7 +741,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet("/login/forgot-password/confirm")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ForgotPasswordConfirmation()
|
||||
public ActionResult ForgotPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
@ -812,7 +814,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByInvitationTokenAsync(userId, Uri.UnescapeDataString(code));
|
||||
var user = await _userManager.FindByInvitationTokenAsync<ApplicationUser>(userId, Uri.UnescapeDataString(code));
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
@ -827,6 +829,9 @@ namespace BTCPayServer.Controllers
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
|
||||
// unset used token
|
||||
await _userManager.UnsetInvitationTokenAsync<ApplicationUser>(user.Id);
|
||||
|
||||
if (requiresEmailConfirmation)
|
||||
{
|
||||
return await RedirectToConfirmEmail(user);
|
||||
|
@ -199,16 +199,13 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var receipt = InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, i.ReceiptOptions);
|
||||
|
||||
if (receipt.Enabled is not true)
|
||||
{
|
||||
if (i.RedirectURL is not null)
|
||||
{
|
||||
return Redirect(i.RedirectURL.ToString());
|
||||
}
|
||||
return NotFound();
|
||||
|
||||
return i.RedirectURL is not null
|
||||
? Redirect(i.RedirectURL.ToString())
|
||||
: NotFound();
|
||||
}
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new InvoiceReceiptViewModel
|
||||
{
|
||||
@ -416,7 +413,7 @@ namespace BTCPayServer.Controllers
|
||||
createPullPayment = new CreatePullPayment
|
||||
{
|
||||
Name = $"Refund {invoice.Id}",
|
||||
PayoutMethodIds = new[] { pmi },
|
||||
PayoutMethods = new[] { pmi },
|
||||
StoreId = invoice.StoreId,
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
|
||||
};
|
||||
@ -579,10 +576,10 @@ namespace BTCPayServer.Controllers
|
||||
PaymentMethodRaw = data,
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PaymentMethod = paymentMethodId.ToString(),
|
||||
TotalDue = _displayFormatter.Currency(accounting.TotalDue, data.Currency),
|
||||
Due = hasPayment ? _displayFormatter.Currency(accounting.Due, data.Currency) : null,
|
||||
Paid = hasPayment ? _displayFormatter.Currency(accounting.PaymentMethodPaid, data.Currency) : null,
|
||||
Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, data.Currency) : null,
|
||||
TotalDue = _displayFormatter.Currency(accounting.TotalDue, data.Currency, divisibility: data.Divisibility),
|
||||
Due = hasPayment ? _displayFormatter.Currency(accounting.Due, data.Currency, divisibility: data.Divisibility) : null,
|
||||
Paid = hasPayment ? _displayFormatter.Currency(accounting.PaymentMethodPaid, data.Currency, divisibility: data.Divisibility) : null,
|
||||
Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, data.Currency, divisibility: data.Divisibility) : null,
|
||||
Address = data.Destination
|
||||
};
|
||||
}).ToList(),
|
||||
@ -652,9 +649,7 @@ namespace BTCPayServer.Controllers
|
||||
if (derivationScheme is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
|
||||
var bumpableAddresses = (await GetAddresses(selectedItems))
|
||||
.Where(p => p.GetPaymentMethodId() == btc)
|
||||
.Select(p => p.GetAddress()).ToHashSet();
|
||||
var bumpableAddresses = await GetAddresses(btc, selectedItems);
|
||||
var utxos = await explorer.GetUTXOsAsync(derivationScheme);
|
||||
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray();
|
||||
var parameters = new MultiValueDictionary<string, string>();
|
||||
@ -676,10 +671,10 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListInvoices), new { storeId });
|
||||
}
|
||||
|
||||
private async Task<AddressInvoiceData[]> GetAddresses(string[] selectedItems)
|
||||
private async Task<HashSet<string>> GetAddresses(PaymentMethodId paymentMethodId, string[] selectedItems)
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
return await ctx.AddressInvoices.Where(i => selectedItems.Contains(i.InvoiceDataId)).ToArrayAsync();
|
||||
return new HashSet<string>(await ctx.AddressInvoices.Where(i => selectedItems.Contains(i.InvoiceDataId) && i.PaymentMethodId == paymentMethodId.ToString()).Select(i => i.Address).ToArrayAsync());
|
||||
}
|
||||
|
||||
[HttpGet("i/{invoiceId}")]
|
||||
@ -699,7 +694,16 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
{
|
||||
// see if the invoice actually exists and is in a state for which we do not display the checkout
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
var store = invoice != null ? await _StoreRepository.GetStoreByInvoiceId(invoice.Id) : null;
|
||||
var receipt = invoice != null && store != null ? InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, invoice.ReceiptOptions) : null;
|
||||
var redirectUrl = invoice?.RedirectURL?.ToString();
|
||||
return receipt?.Enabled is true
|
||||
? RedirectToAction(nameof(InvoiceReceipt), new { invoiceId })
|
||||
: !string.IsNullOrEmpty(redirectUrl) ? Redirect(redirectUrl) : NotFound();
|
||||
}
|
||||
|
||||
if (view == "modal")
|
||||
model.IsModal = true;
|
||||
@ -870,6 +874,13 @@ namespace BTCPayServer.Controllers
|
||||
_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension);
|
||||
return extension?.Image ?? "";
|
||||
}
|
||||
|
||||
// Show the "Common divisibility" rather than the payment method disibility.
|
||||
// For example, BTC has commonly 8 digits, but on lightning it has 11. In this case, pick 8.
|
||||
if (this._CurrencyNameTable.GetCurrencyData(prompt.Currency, false)?.Divisibility is not int divisibility)
|
||||
divisibility = prompt.Divisibility;
|
||||
|
||||
string ShowMoney(decimal value) => MoneyExtensions.ShowMoney(value, divisibility);
|
||||
var model = new PaymentModel
|
||||
{
|
||||
Activated = prompt.Activated,
|
||||
@ -887,10 +898,11 @@ namespace BTCPayServer.Controllers
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(paymentMethodId)),
|
||||
BtcAddress = prompt.Destination,
|
||||
BtcDue = accounting.ShowMoney(accounting.Due),
|
||||
BtcPaid = accounting.ShowMoney(accounting.Paid),
|
||||
BtcDue = ShowMoney(accounting.Due),
|
||||
BtcPaid = ShowMoney(accounting.Paid),
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
OrderAmount = accounting.ShowMoney(accounting.TotalDue - accounting.PaymentMethodFee),
|
||||
// The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible.
|
||||
OrderAmount = ShowMoney(accounting.TotalDue - (prompt.PaymentMethodFee - prompt.TweakFee)),
|
||||
IsUnsetTopUp = invoice.IsUnsetTopUp(),
|
||||
CustomerEmail = invoice.Metadata.BuyerEmail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
@ -912,17 +924,9 @@ namespace BTCPayServer.Controllers
|
||||
NetworkFeeMode.Never => 0,
|
||||
_ => throw new NotImplementedException()
|
||||
},
|
||||
RequiredConfirmations = invoice.SpeedPolicy switch
|
||||
{
|
||||
SpeedPolicy.HighSpeed => 0,
|
||||
SpeedPolicy.MediumSpeed => 1,
|
||||
SpeedPolicy.LowMediumSpeed => 2,
|
||||
SpeedPolicy.LowSpeed => 6,
|
||||
_ => null
|
||||
},
|
||||
ReceivedConfirmations = handler is BitcoinLikePaymentHandler bh ? invoice.GetAllBitcoinPaymentData(bh, false).FirstOrDefault()?.ConfirmationCount : null,
|
||||
Status = invoice.Status.ToString(),
|
||||
NetworkFee = prompt.PaymentMethodFee,
|
||||
// The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible.
|
||||
NetworkFee = prompt.PaymentMethodFee - prompt.TweakFee,
|
||||
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.PaymentMethodId).Concat(new[] { prompt.PaymentMethodId }).Distinct().Count() > 1,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentPrompts()
|
||||
|
@ -513,8 +513,8 @@ namespace BTCPayServer.Controllers
|
||||
{Policies.CanDeleteUser, ("Delete user", "Allows deleting the user to whom it is assigned. Admin users can delete any user without this permission.")},
|
||||
{Policies.CanModifyStoreSettings, ("Modify your stores", "Allows managing invoices on all your stores and modify their settings.")},
|
||||
{$"{Policies.CanModifyStoreSettings}:", ("Manage selected stores", "Allows managing invoices on the selected stores and modify their settings.")},
|
||||
{Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "Allows modifying the webhooks of all your stores.")},
|
||||
{$"{Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "Allows modifying the webhooks of the selected stores.")},
|
||||
{Policies.CanModifyWebhooks, ("Modify stores webhooks", "Allows modifying the webhooks of all your stores.")},
|
||||
{$"{Policies.CanModifyWebhooks}:", ("Modify selected stores' webhooks", "Allows modifying the webhooks of the selected stores.")},
|
||||
{Policies.CanViewStoreSettings, ("View your stores", "Allows viewing stores settings.")},
|
||||
{$"{Policies.CanViewStoreSettings}:", ("View your stores", "Allows viewing the selected stores' settings.")},
|
||||
{Policies.CanViewReports, ("View your reports", "Allows viewing reports.")},
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Controllers
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
var roles = await _StoreRepository.GetStoreRoles(null, true);
|
||||
var roles = await _StoreRepository.GetStoreRoles(null);
|
||||
var defaultRole = (await _StoreRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
@ -88,11 +88,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
prop.Value = "OK";
|
||||
}
|
||||
viewModel.Translations = Translations.CreateFromJson(jobj.ToString()).ToTextFormat();
|
||||
viewModel.Translations = Translations.CreateFromJson(jobj.ToString()).ToJsonFormat();
|
||||
}
|
||||
|
||||
|
||||
if (!Translations.TryCreateFromText(viewModel.Translations, out var translations))
|
||||
if (!Translations.TryCreateFromJson(viewModel.Translations, out var translations))
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.Translations), "Syntax error");
|
||||
return View(viewModel);
|
||||
|
@ -58,18 +58,28 @@ namespace BTCPayServer.Controllers
|
||||
.Skip(model.Skip)
|
||||
.Take(model.Count)
|
||||
.ToListAsync())
|
||||
.Select(u => new UsersViewModel.UserViewModel
|
||||
.Select(u =>
|
||||
{
|
||||
Name = u.GetBlob()?.Name,
|
||||
ImageUrl = u.GetBlob()?.ImageUrl,
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
||||
Approved = u.RequiresApproval ? u.Approved : null,
|
||||
Created = u.Created,
|
||||
Roles = u.UserRoles.Select(role => role.RoleId),
|
||||
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime,
|
||||
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
|
||||
var blob = u.GetBlob();
|
||||
return new UsersViewModel.UserViewModel
|
||||
{
|
||||
Name = blob?.Name,
|
||||
ImageUrl = blob?.ImageUrl,
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
InvitationUrl =
|
||||
string.IsNullOrEmpty(blob?.InvitationToken)
|
||||
? null
|
||||
: _linkGenerator.InvitationLink(u.Id, blob.InvitationToken, Request.Scheme,
|
||||
Request.Host, Request.PathBase),
|
||||
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
||||
Approved = u.RequiresApproval ? u.Approved : null,
|
||||
Created = u.Created,
|
||||
Roles = u.UserRoles.Select(role => role.RoleId),
|
||||
Disabled = u.LockoutEnabled && u.LockoutEnd != null &&
|
||||
DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime,
|
||||
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
return View(model);
|
||||
@ -88,6 +98,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Name = blob?.Name,
|
||||
InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null : _linkGenerator.InvitationLink(user.Id, blob.InvitationToken, Request.Scheme, Request.Host, Request.PathBase),
|
||||
ImageUrl = string.IsNullOrEmpty(blob?.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)),
|
||||
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
|
||||
Approved = user.RequiresApproval ? user.Approved : null,
|
||||
@ -199,17 +210,47 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(User), new { userId });
|
||||
}
|
||||
|
||||
[HttpGet("server/users/new")]
|
||||
public IActionResult CreateUser()
|
||||
[HttpGet("server/users/{userId}/reset-password")]
|
||||
public async Task<IActionResult> ResetUserPassword(string userId)
|
||||
{
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
return View();
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View(new ResetUserPasswordFromAdmin { Email = user.Email });
|
||||
}
|
||||
|
||||
[HttpPost("server/users/{userId}/reset-password")]
|
||||
public async Task<IActionResult> ResetUserPassword(string userId, ResetUserPasswordFromAdmin model)
|
||||
{
|
||||
|
||||
var user = await _UserManager.FindByEmailAsync(model.Email);
|
||||
if (user == null || user.Id != userId)
|
||||
return NotFound();
|
||||
|
||||
var result = await _UserManager.ResetPasswordAsync(user, await _UserManager.GeneratePasswordResetTokenAsync(user), model.Password);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = result.Succeeded ? StatusMessageModel.StatusSeverity.Success : StatusMessageModel.StatusSeverity.Error,
|
||||
Message = result.Succeeded ? "Password successfully set" : "An error occurred while resetting user password"
|
||||
});
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
[HttpGet("server/users/new")]
|
||||
public async Task<IActionResult> CreateUser()
|
||||
{
|
||||
await PrepareCreateUserViewData();
|
||||
var vm = new RegisterFromAdminViewModel
|
||||
{
|
||||
SendInvitationEmail = ViewData["CanSendEmail"] is true
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("server/users/new")]
|
||||
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
|
||||
{
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
await PrepareCreateUserViewData();
|
||||
if (!_Options.CheatMode)
|
||||
model.IsAdmin = false;
|
||||
if (ModelState.IsValid)
|
||||
@ -236,6 +277,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var tcs = new TaskCompletionSource<Uri>();
|
||||
var currentUser = await _UserManager.GetUserAsync(HttpContext.User);
|
||||
var sendEmail = model.SendInvitationEmail && ViewData["CanSendEmail"] is true;
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
@ -243,23 +285,23 @@ namespace BTCPayServer.Controllers
|
||||
Kind = UserRegisteredEventKind.Invite,
|
||||
User = user,
|
||||
InvitedByUser = currentUser,
|
||||
SendInvitationEmail = sendEmail,
|
||||
Admin = model.IsAdmin,
|
||||
CallbackUrlGenerated = tcs
|
||||
});
|
||||
|
||||
var callbackUrl = await tcs.Task;
|
||||
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
var info = settings.IsComplete()
|
||||
? "An invitation email has been sent.<br/>You may alternatively"
|
||||
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
|
||||
var info = sendEmail
|
||||
? "An invitation email has been sent. You may alternatively"
|
||||
: "An invitation email has not been sent. You need to";
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
AllowDismiss = false,
|
||||
Html = $"Account successfully created. {info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
|
||||
Html = $"Account successfully created. {info} share this link with them:<br/>{callbackUrl}"
|
||||
});
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
return RedirectToAction(nameof(User), new { userId = user.Id });
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
@ -391,6 +433,31 @@ namespace BTCPayServer.Controllers
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
private async Task PrepareCreateUserViewData()
|
||||
{
|
||||
var emailSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
ViewData["CanSendEmail"] = emailSettings.IsComplete();
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
}
|
||||
}
|
||||
|
||||
public class ResetUserPasswordFromAdmin
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Password")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
public class RegisterFromAdminViewModel
|
||||
@ -415,5 +482,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Display(Name = "Email confirmed?")]
|
||||
public bool EmailConfirmed { get; set; }
|
||||
|
||||
[Display(Name = "Send invitation email")]
|
||||
public bool SendInvitationEmail { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
|
||||
Amount = model.Amount,
|
||||
Currency = model.Currency,
|
||||
StoreId = storeId,
|
||||
PayoutMethodIds = selectedPaymentMethodIds,
|
||||
PayoutMethods = selectedPaymentMethodIds,
|
||||
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
|
||||
AutoApproveClaims = model.AutoApproveClaims
|
||||
});
|
||||
@ -586,7 +586,7 @@ namespace BTCPayServer.Controllers
|
||||
private async Task<bool> HasPayoutProcessor(string storeId, PayoutMethodId payoutMethodId)
|
||||
{
|
||||
var processors = await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PayoutMethodIds = [payoutMethodId] });
|
||||
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PayoutMethods = [payoutMethodId] });
|
||||
return _payoutProcessorFactories.Any(factory => factory.GetSupportedPayoutMethods().Contains(payoutMethodId)) && processors.Any();
|
||||
}
|
||||
private async Task<bool> HasPayoutProcessor(string storeId, string payoutMethodId)
|
||||
|
@ -538,12 +538,6 @@ public partial class UIStoresController
|
||||
}
|
||||
}
|
||||
|
||||
if (store.SpeedPolicy != vm.SpeedPolicy)
|
||||
{
|
||||
store.SpeedPolicy = vm.SpeedPolicy;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
store.SetPaymentMethodConfig(handler, derivation);
|
||||
@ -599,6 +593,12 @@ public partial class UIStoresController
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (store.SpeedPolicy != vm.SpeedPolicy)
|
||||
{
|
||||
store.SpeedPolicy = vm.SpeedPolicy;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _storeRepo.UpdateStore(store);
|
||||
|
@ -21,7 +21,7 @@ public partial class UIStoresController
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
var roles = await storeRepository.GetStoreRoles(storeId, true);
|
||||
var roles = await storeRepository.GetStoreRoles(storeId);
|
||||
var defaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
@ -34,6 +34,7 @@ public partial class UIStoresController
|
||||
LogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.LogoUrl),
|
||||
CssUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.CssUrl),
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
ApplyBrandColorToBackend = storeBlob.ApplyBrandColorToBackend,
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
|
||||
PaymentTolerance = storeBlob.PaymentTolerance,
|
||||
@ -75,10 +76,11 @@ public partial class UIStoresController
|
||||
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
|
||||
if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.BrandColor), "Invalid color");
|
||||
ModelState.AddModelError(nameof(model.BrandColor), "The brand color needs to be a valid hex color code");
|
||||
return View(model);
|
||||
}
|
||||
blob.BrandColor = model.BrandColor;
|
||||
blob.ApplyBrandColorToBackend = model.ApplyBrandColorToBackend && !string.IsNullOrEmpty(model.BrandColor);
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
|
@ -14,7 +14,6 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
@ -34,16 +33,13 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Client;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -330,7 +326,7 @@ namespace BTCPayServer.Controllers
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var store = GetCurrentStore();
|
||||
var address = _walletReceiveService.Get(walletId)?.Address;
|
||||
var address = (await _walletReceiveService.GetOrGenerate(walletId)).Address;
|
||||
var allowedPayjoin = paymentMethod.IsHotWallet && store.GetStoreBlob().PayJoinEnabled;
|
||||
var bip21 = network.GenerateBIP21(address?.ToString(), null);
|
||||
if (allowedPayjoin)
|
||||
@ -354,7 +350,7 @@ namespace BTCPayServer.Controllers
|
||||
Address = address?.ToString(),
|
||||
CryptoImage = GetImage(network),
|
||||
PaymentLink = bip21.ToString(),
|
||||
ReturnUrl = returnUrl ?? HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath,
|
||||
ReturnUrl = returnUrl,
|
||||
SelectedLabels = labels ?? Array.Empty<string>()
|
||||
});
|
||||
}
|
||||
@ -373,18 +369,6 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
switch (command)
|
||||
{
|
||||
case "unreserve-current-address":
|
||||
var address = await _walletReceiveService.UnReserveAddress(walletId);
|
||||
if (!string.IsNullOrEmpty(address))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
AllowDismiss = true,
|
||||
Message = $"Address {address} was unreserved.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "generate-new-address":
|
||||
await _walletReceiveService.GetOrGenerate(walletId, true);
|
||||
break;
|
||||
|
@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public static class AddressInvoiceDataExtensions
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
public static string GetAddress(this AddressInvoiceData addressInvoiceData)
|
||||
{
|
||||
if (addressInvoiceData.Address == null)
|
||||
return null;
|
||||
var index = addressInvoiceData.Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return addressInvoiceData.Address;
|
||||
return addressInvoiceData.Address.Substring(0, index);
|
||||
}
|
||||
public static PaymentMethodId GetPaymentMethodId(this AddressInvoiceData addressInvoiceData)
|
||||
{
|
||||
if (addressInvoiceData.Address == null)
|
||||
return null;
|
||||
var index = addressInvoiceData.Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
// Legacy AddressInvoiceData does not have the paymentMethodId attached to the Address
|
||||
if (index == -1)
|
||||
return PaymentMethodId.Parse("BTC");
|
||||
/////////////////////////
|
||||
return PaymentMethodId.TryParse(addressInvoiceData.Address.Substring(index + 1));
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ namespace BTCPayServer.Data
|
||||
#nullable enable
|
||||
public static PayoutMethodId? GetClosestPayoutMethodId(this InvoiceData invoice, IEnumerable<PayoutMethodId> pmids)
|
||||
{
|
||||
var paymentMethodIds = invoice.Payments.Select(o => o.GetPaymentMethodId()).ToArray();
|
||||
var paymentMethodIds = invoice.Payments.Select(o => PaymentMethodId.Parse(o.PaymentMethodId)).ToArray();
|
||||
if (paymentMethodIds.Length == 0)
|
||||
paymentMethodIds = invoice.GetBlob().GetPaymentPrompts().Select(p => p.PaymentMethodId).ToArray();
|
||||
return PaymentMethodId.GetSimilarities(pmids, paymentMethodIds)
|
||||
@ -73,7 +73,7 @@ namespace BTCPayServer.Data
|
||||
entity.Status = state.Status;
|
||||
if (invoiceData.AddressInvoices != null)
|
||||
{
|
||||
entity.AvailableAddressHashes = invoiceData.AddressInvoices.Select(a => a.GetAddress() + a.GetPaymentMethodId()).ToHashSet();
|
||||
entity.Addresses = invoiceData.AddressInvoices.Select(a => (PaymentMethodId.Parse(a.PaymentMethodId), a.Address)).ToHashSet();
|
||||
}
|
||||
if (invoiceData.Refunds != null)
|
||||
{
|
||||
|
@ -34,20 +34,16 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
public static PaymentData SetBlob(this PaymentData paymentData, PaymentMethodId paymentMethodId, PaymentBlob blob)
|
||||
{
|
||||
paymentData.Type = paymentMethodId.ToString();
|
||||
paymentData.PaymentMethodId = paymentMethodId.ToString();
|
||||
paymentData.Blob2 = JToken.FromObject(blob, InvoiceDataExtensions.DefaultSerializer).ToString(Newtonsoft.Json.Formatting.None);
|
||||
return paymentData;
|
||||
}
|
||||
public static PaymentMethodId GetPaymentMethodId(this PaymentData paymentData)
|
||||
{
|
||||
return PaymentMethodId.Parse(paymentData.Type);
|
||||
}
|
||||
public static PaymentEntity GetBlob(this PaymentData paymentData)
|
||||
{
|
||||
var entity = JToken.Parse(paymentData.Blob2).ToObject<PaymentEntity>(InvoiceDataExtensions.DefaultSerializer) ?? throw new FormatException($"Invalid {nameof(PaymentEntity)}");
|
||||
entity.Status = paymentData.Status!.Value;
|
||||
entity.Currency = paymentData.Currency;
|
||||
entity.PaymentMethodId = GetPaymentMethodId(paymentData);
|
||||
entity.PaymentMethodId = PaymentMethodId.Parse(paymentData.PaymentMethodId);
|
||||
entity.Value = paymentData.Amount!.Value;
|
||||
entity.Id = paymentData.Id;
|
||||
entity.ReceivedTime = paymentData.Created!.Value;
|
||||
|
@ -319,23 +319,34 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
string message = null;
|
||||
if (result.Result == PayResult.Ok)
|
||||
{
|
||||
message = result.Details?.TotalAmount != null
|
||||
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}"
|
||||
: null;
|
||||
payoutData.State = PayoutState.Completed;
|
||||
try
|
||||
payoutData.State = result.Details?.Status switch
|
||||
{
|
||||
var payment = await lightningClient.GetPayment(bolt11PaymentRequest.PaymentHash.ToString(), cancellationToken);
|
||||
proofBlob.Preimage = payment.Preimage;
|
||||
}
|
||||
catch (Exception)
|
||||
LightningPaymentStatus.Pending => PayoutState.InProgress,
|
||||
_ => PayoutState.Completed,
|
||||
};
|
||||
if (payoutData.State == PayoutState.Completed)
|
||||
{
|
||||
// ignored
|
||||
message = result.Details?.TotalAmount != null
|
||||
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}"
|
||||
: null;
|
||||
try
|
||||
{
|
||||
var payment = await lightningClient.GetPayment(bolt11PaymentRequest.PaymentHash.ToString(),
|
||||
cancellationToken);
|
||||
proofBlob.Preimage = payment.Preimage;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (result.Result == PayResult.Unknown)
|
||||
{
|
||||
payoutData.State = PayoutState.InProgress;
|
||||
}
|
||||
if (payoutData.State == PayoutState.InProgress)
|
||||
{
|
||||
message = "The payment has been initiated but is still in-flight.";
|
||||
}
|
||||
|
||||
|
@ -191,6 +191,8 @@ namespace BTCPayServer.Data
|
||||
|
||||
public List<UIStoresController.StoreEmailRule> EmailRules { get; set; }
|
||||
public string BrandColor { get; set; }
|
||||
public bool ApplyBrandColorToBackend { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
|
||||
public UnresolvedUri LogoUrl { get; set; }
|
||||
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
|
||||
|
@ -1,15 +0,0 @@
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceStopWatchedEvent : IHasInvoiceId
|
||||
{
|
||||
public InvoiceStopWatchedEvent(string invoiceId)
|
||||
{
|
||||
this.InvoiceId = invoiceId;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} is not monitored anymore.";
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ public class UserRegisteredEvent
|
||||
public UserRegisteredEventKind Kind { get; set; } = UserRegisteredEventKind.Registration;
|
||||
public Uri RequestUri { get; set; }
|
||||
public ApplicationUser InvitedByUser { get; set; }
|
||||
public bool SendInvitationEmail { get; set; }
|
||||
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||
}
|
||||
|
||||
|
@ -323,8 +323,8 @@ namespace BTCPayServer
|
||||
public static IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(this InvoiceEntity invoice, BitcoinLikePaymentHandler handler, bool accountedOnly)
|
||||
{
|
||||
return invoice.GetPayments(accountedOnly)
|
||||
.Where(p => p.PaymentMethodId == handler.PaymentMethodId)
|
||||
.Select(p => handler.ParsePaymentDetails(p.Details));
|
||||
.Select(p => p.GetDetails<BitcoinLikePaymentData>(handler))
|
||||
.Where(p => p is not null);
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, bool includeOffchain = false, CancellationToken cts = default(CancellationToken))
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using MimeKit;
|
||||
@ -45,6 +46,7 @@ namespace BTCPayServer.HostedServices
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
private readonly Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
public const string NamedClient = "bitpay-ipn";
|
||||
public BitpayIPNSender(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
@ -53,6 +55,7 @@ namespace BTCPayServer.HostedServices
|
||||
InvoiceRepository invoiceRepository,
|
||||
StoreRepository storeRepository,
|
||||
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
EmailSenderFactory emailSenderFactory)
|
||||
{
|
||||
_Client = httpClientFactory.CreateClient(NamedClient);
|
||||
@ -62,11 +65,12 @@ namespace BTCPayServer.HostedServices
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_bitpayExtensions = bitpayExtensions;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification, bool sendMail)
|
||||
{
|
||||
var dto = invoice.EntityToDTO(_bitpayExtensions);
|
||||
var dto = invoice.EntityToDTO(_bitpayExtensions, _currencyNameTable);
|
||||
var notification = new InvoicePaymentNotificationEventWrapper()
|
||||
{
|
||||
Data = new InvoicePaymentNotification()
|
||||
|
@ -29,7 +29,7 @@ public abstract class BlobMigratorHostedService<TEntity> : IHostedService
|
||||
public bool Complete { get; set; }
|
||||
}
|
||||
Task? _Migrating;
|
||||
TaskCompletionSource _Cts = new TaskCompletionSource();
|
||||
CancellationTokenSource? _Cts;
|
||||
public BlobMigratorHostedService(
|
||||
ILogger logs,
|
||||
ISettingsRepository settingsRepository,
|
||||
@ -46,7 +46,8 @@ public abstract class BlobMigratorHostedService<TEntity> : IHostedService
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Migrating = Migrate(cancellationToken);
|
||||
_Cts = new CancellationTokenSource();
|
||||
_Migrating = Migrate(_Cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public int BatchSize { get; set; } = 1000;
|
||||
@ -61,40 +62,63 @@ public abstract class BlobMigratorHostedService<TEntity> : IHostedService
|
||||
else
|
||||
Logs.LogInformation("Migrating from the beginning");
|
||||
|
||||
|
||||
int batchSize = BatchSize;
|
||||
retry:
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
retry:
|
||||
List<TEntity> entities;
|
||||
DateTimeOffset progress;
|
||||
await using (var ctx = ApplicationDbContextFactory.CreateContext())
|
||||
try
|
||||
{
|
||||
var query = GetQuery(ctx, settings?.Progress).Take(batchSize);
|
||||
entities = await query.ToListAsync(cancellationToken);
|
||||
if (entities.Count == 0)
|
||||
List<TEntity> entities;
|
||||
DateTimeOffset progress;
|
||||
await using (var ctx = ApplicationDbContextFactory.CreateContext(o => o.CommandTimeout((int)TimeSpan.FromDays(1.0).TotalSeconds)))
|
||||
{
|
||||
await SettingsRepository.UpdateSetting<Settings>(new Settings() { Complete = true }, SettingsKey);
|
||||
Logs.LogInformation("Migration completed");
|
||||
return;
|
||||
var query = GetQuery(ctx, settings?.Progress).Take(batchSize);
|
||||
entities = await query.ToListAsync(cancellationToken);
|
||||
if (entities.Count == 0)
|
||||
{
|
||||
var count = await GetQuery(ctx, null).CountAsync(cancellationToken);
|
||||
if (count != 0)
|
||||
{
|
||||
settings = new Settings() { Progress = null };
|
||||
Logs.LogWarning("Corruption detected, reindexing the table...");
|
||||
await Reindex(ctx, cancellationToken);
|
||||
goto retry;
|
||||
}
|
||||
await SettingsRepository.UpdateSetting<Settings>(new Settings() { Complete = true }, SettingsKey);
|
||||
Logs.LogInformation("Migration completed");
|
||||
await PostMigrationCleanup(ctx, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
progress = ProcessEntities(ctx, entities);
|
||||
await ctx.SaveChangesAsync();
|
||||
batchSize = BatchSize;
|
||||
}
|
||||
catch (Exception ex) when (ex is DbUpdateConcurrencyException or TimeoutException or OperationCanceledException)
|
||||
{
|
||||
batchSize /= 2;
|
||||
batchSize = Math.Max(1, batchSize);
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
progress = ProcessEntities(ctx, entities);
|
||||
await ctx.SaveChangesAsync();
|
||||
batchSize = BatchSize;
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
batchSize /= 2;
|
||||
batchSize = Math.Max(1, batchSize);
|
||||
goto retry;
|
||||
}
|
||||
settings = new Settings() { Progress = progress };
|
||||
await SettingsRepository.UpdateSetting<Settings>(settings, SettingsKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.LogError(ex, "Error while migrating");
|
||||
goto retry;
|
||||
}
|
||||
settings = new Settings() { Progress = progress };
|
||||
await SettingsRepository.UpdateSetting<Settings>(settings, SettingsKey);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task PostMigrationCleanup(ApplicationDbContext ctx, CancellationToken cancellationToken);
|
||||
|
||||
protected abstract Task Reindex(ApplicationDbContext ctx, CancellationToken cancellationToken);
|
||||
protected abstract IQueryable<TEntity> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress);
|
||||
protected abstract DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<TEntity> entities);
|
||||
public async Task ResetMigration()
|
||||
@ -108,11 +132,7 @@ retry:
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.TrySetCanceled();
|
||||
return (_Migrating ?? Task.CompletedTask).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Logs.LogError(t.Exception, "Error while migrating");
|
||||
});
|
||||
_Cts?.Cancel();
|
||||
return _Migrating ?? Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using AngleSharp.Dom;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Dapper;
|
||||
using Google.Apis.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@ -22,7 +23,7 @@ namespace BTCPayServer.HostedServices;
|
||||
|
||||
public class InvoiceBlobMigratorHostedService : BlobMigratorHostedService<InvoiceData>
|
||||
{
|
||||
|
||||
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
|
||||
public InvoiceBlobMigratorHostedService(
|
||||
@ -42,9 +43,13 @@ public class InvoiceBlobMigratorHostedService : BlobMigratorHostedService<Invoic
|
||||
ctx.Invoices.Include(o => o.Payments).Where(i => i.Currency == null);
|
||||
return query.OrderByDescending(i => i.Created);
|
||||
}
|
||||
protected override Task Reindex(ApplicationDbContext ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
return ctx.Database.ExecuteSqlRawAsync("REINDEX INDEX \"IX_Invoices_Created\";REINDEX INDEX \"PK_Invoices\";", cancellationToken);
|
||||
}
|
||||
protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<InvoiceData> invoices)
|
||||
{
|
||||
// Those clean up the JSON blobs, and mark entities as modified
|
||||
// Those clean up the JSON blobs
|
||||
foreach (var inv in invoices)
|
||||
{
|
||||
var blob = inv.GetBlob();
|
||||
@ -66,16 +71,21 @@ public class InvoiceBlobMigratorHostedService : BlobMigratorHostedService<Invoic
|
||||
paymentEntity.Details = JToken.FromObject(handler.ParsePaymentDetails(paymentEntity.Details), handler.Serializer);
|
||||
}
|
||||
pay.SetBlob(paymentEntity);
|
||||
|
||||
if (pay.PaymentMethodId != pay.MigratedPaymentMethodId)
|
||||
{
|
||||
ctx.Entry(pay).State = EntityState.Added;
|
||||
ctx.Payments.Remove(new PaymentData() { Id = pay.Id, PaymentMethodId = pay.MigratedPaymentMethodId });
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var entry in ctx.ChangeTracker.Entries<InvoiceData>())
|
||||
{
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
foreach (var entry in ctx.ChangeTracker.Entries<PaymentData>())
|
||||
{
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
return invoices[^1].Created;
|
||||
}
|
||||
|
||||
protected override async Task PostMigrationCleanup(ApplicationDbContext ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
Logs.LogInformation("Post-migration VACUUM (ANALYZE)");
|
||||
await ctx.Database.ExecuteSqlRawAsync("VACUUM (ANALYZE) \"Invoices\"", cancellationToken);
|
||||
Logs.LogInformation("Post-migration VACUUM (ANALYZE) finished");
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
Subscribe<InvoiceDataChangedEvent>();
|
||||
Subscribe<InvoiceStopWatchedEvent>();
|
||||
Subscribe<InvoiceIPNEvent>();
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
@ -191,9 +192,9 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Wait(string invoiceId)
|
||||
private async Task Wait(string invoiceId) => await Wait(await _invoiceRepository.GetInvoice(invoiceId));
|
||||
private async Task Wait(InvoiceEntity invoice)
|
||||
{
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId);
|
||||
try
|
||||
{
|
||||
// add 1 second to ensure watch won't trigger moments before invoice expires
|
||||
@ -202,7 +203,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.Delay(delay, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
Watch(invoice.Id);
|
||||
|
||||
// add 1 second to ensure watch won't trigger moments before monitoring expires
|
||||
delay = invoice.MonitoringExpiration.AddSeconds(1) - DateTimeOffset.UtcNow;
|
||||
@ -210,7 +211,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.Delay(delay, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
Watch(invoice.Id);
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{ }
|
||||
@ -255,8 +256,17 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private async Task WaitPendingInvoices()
|
||||
{
|
||||
await Task.WhenAll((await _invoiceRepository.GetPendingInvoiceIds())
|
||||
.Select(id => Wait(id)).ToArray());
|
||||
await Task.WhenAll((await GetPendingInvoices(_Cts.Token))
|
||||
.Select(i => Wait(i)).ToArray());
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity[]> GetPendingInvoices(CancellationToken cancellationToken)
|
||||
{
|
||||
using var ctx = _invoiceRepository.DbContextFactory.CreateContext();
|
||||
var rows = await ctx.Invoices.Where(i => Data.InvoiceData.IsPending(i.Status))
|
||||
.Select(o => o).ToArrayAsync(cancellationToken);
|
||||
var invoices = rows.Select(_invoiceRepository.ToEntity).ToArray();
|
||||
return invoices;
|
||||
}
|
||||
|
||||
async Task StartLoop(CancellationToken cancellation)
|
||||
@ -292,24 +302,6 @@ namespace BTCPayServer.HostedServices
|
||||
_eventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == InvoiceStatus.Settled ||
|
||||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
var extendInvoiceMonitoring = await UpdateConfirmationCount(invoice);
|
||||
|
||||
// we extend monitor time if we haven't reached max confirmation count
|
||||
// say user used low fee and we only got 3 confirmations right before it's time to remove
|
||||
if (extendInvoiceMonitoring)
|
||||
{
|
||||
await _invoiceRepository.ExtendInvoiceMonitor(invoice.Id);
|
||||
}
|
||||
else if (await _invoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
{
|
||||
_eventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateContext.Events.Count == 0)
|
||||
break;
|
||||
}
|
||||
@ -324,48 +316,6 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move that in the NBXplorerListener
|
||||
private async Task<bool> UpdateConfirmationCount(InvoiceEntity invoice)
|
||||
{
|
||||
bool extendInvoiceMonitoring = false;
|
||||
var updateConfirmationCountIfNeeded = invoice
|
||||
.GetPayments(true)
|
||||
.Select<PaymentEntity, Task<PaymentEntity>>(async payment =>
|
||||
{
|
||||
if (!_handlers.TryGetValue(payment.PaymentMethodId, out var h) || h is not Payments.Bitcoin.BitcoinLikePaymentHandler handler)
|
||||
return null;
|
||||
|
||||
var onChainPaymentData = handler.ParsePaymentDetails(payment.Details);
|
||||
var network = handler.Network;
|
||||
// Do update if confirmation count in the paymentData is not up to date
|
||||
if (onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation)
|
||||
{
|
||||
var client = _explorerClientProvider.GetExplorerClient(payment.Currency);
|
||||
var transactionResult = client is null ? null : await client.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
|
||||
var confirmationCount = transactionResult?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.Status = NBXplorerListener.IsSettled(invoice, onChainPaymentData) ? PaymentStatus.Settled : PaymentStatus.Processing;
|
||||
payment.SetDetails(handler, onChainPaymentData);
|
||||
|
||||
// we want to extend invoice monitoring until we reach max confirmations on all onchain payment methods
|
||||
if (confirmationCount < network.MaxTrackedConfirmation)
|
||||
extendInvoiceMonitoring = true;
|
||||
|
||||
return payment;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(updateConfirmationCountIfNeeded);
|
||||
var updatedPaymentData = updateConfirmationCountIfNeeded.Where(a => a.Result != null).Select(a => a.Result).ToList();
|
||||
if (updatedPaymentData.Count > 0)
|
||||
{
|
||||
await _paymentService.UpdatePayments(updatedPaymentData);
|
||||
}
|
||||
|
||||
return extendInvoiceMonitoring;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Cts == null)
|
||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.HostedServices
|
||||
public string Description { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public PayoutMethodId[] PayoutMethodIds { get; set; }
|
||||
public PayoutMethodId[] PayoutMethods { get; set; }
|
||||
public bool AutoApproveClaims { get; set; }
|
||||
public TimeSpan? BOLT11Expiration { get; set; }
|
||||
}
|
||||
@ -119,7 +119,7 @@ namespace BTCPayServer.HostedServices
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PayoutMethodIds = request.PaymentMethods.Select(p => PayoutMethodId.Parse(p)).ToArray(),
|
||||
PayoutMethods = request.PayoutMethods.Select(p => PayoutMethodId.Parse(p)).ToArray(),
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
}
|
||||
@ -143,7 +143,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Name = create.Name ?? string.Empty,
|
||||
Description = create.Description ?? string.Empty,
|
||||
SupportedPayoutMethods = create.PayoutMethodIds,
|
||||
SupportedPayoutMethods = create.PayoutMethods,
|
||||
AutoApproveClaims = create.AutoApproveClaims,
|
||||
View = new PullPaymentBlob.PullPaymentView
|
||||
{
|
||||
|
@ -73,11 +73,12 @@ public class UserEventHostedService(
|
||||
emailSender = await emailSenderFactory.GetEmailSender();
|
||||
if (isInvite)
|
||||
{
|
||||
code = await userManager.GenerateInvitationTokenAsync(user);
|
||||
code = await userManager.GenerateInvitationTokenAsync<ApplicationUser>(user.Id);
|
||||
callbackUrl = generator.InvitationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
|
||||
emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl);
|
||||
if (ev.SendInvitationEmail)
|
||||
emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl);
|
||||
}
|
||||
else if (requiresEmailConfirmation)
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user