Compare commits
149 Commits
v2.0.0-bet
...
v2.0.3
Author | SHA1 | Date | |
---|---|---|---|
558f45a48c | |||
6a76167637 | |||
f56ad3408c | |||
8e688b7f28 | |||
6d737ce119 | |||
2075e16767 | |||
2c4b1798ad | |||
b040f78f70 | |||
c221cb7ccb | |||
8a51e21310 | |||
31b73f0d82 | |||
777748d79e | |||
d0f97e85d2 | |||
2eff7523c3 | |||
12d4803c5c | |||
fdbee350b8 | |||
78f47516b8 | |||
088c0c7f85 | |||
809e475e29 | |||
8b776ed9e5 | |||
984475b9d0 | |||
894f68f2ae | |||
796d8d4406 | |||
7ac6cad18c | |||
b3abc8f161 | |||
e80296fa85 | |||
d0779b88e0 | |||
acd7765159 | |||
0a51031334 | |||
0f7c0341c5 | |||
d40669c7bd | |||
fe8360e870 | |||
72fd2ec424 | |||
7e7d4086cd | |||
f5d26a1555 | |||
540fb6c9f6 | |||
12681eda36 | |||
a05dbef337 | |||
a8581510e5 | |||
bbba7551b7 | |||
a129114603 | |||
392ec623c0 | |||
641bdcff31 | |||
b3945d758a | |||
4272ea97db | |||
5e438b84e1 | |||
ff79a31066 | |||
225264a283 | |||
8a5a160645 | |||
5cbadc09f9 | |||
7aa87d397e | |||
693eceb80f | |||
7d8fc14159 | |||
4687bb95cb | |||
e3ec07da76 | |||
910801d305 | |||
5ad0b128aa | |||
5cbeea4fb3 | |||
a6e18736d6 | |||
373b90e3b5 | |||
92f9b226fe | |||
0ac6553840 | |||
41a2241ae1 | |||
9bb1a5b80a | |||
0e59107eee | |||
c9fe68b812 | |||
e7b9688602 | |||
a962e60de9 | |||
e5611f9165 | |||
540ad13265 | |||
2849426092 | |||
c4a2b4e975 | |||
d508f5dc09 | |||
81ce8b0469 | |||
5a3a661e91 | |||
bb5c6bd68d | |||
9dfabeab52 | |||
ad07330bf1 | |||
74011e50e3 | |||
3dfdbf544a | |||
4bf0b79c2a | |||
cc0ea0b3f8 | |||
62d765125d | |||
b5b45d9a27 | |||
8e098710c1 | |||
6dfb369b55 | |||
817522ff97 | |||
8b5b90d247 | |||
b670097592 | |||
7b6a115adc | |||
77fba4aee3 | |||
7e1712c8cd | |||
b7affb1d34 | |||
d7fd90c4c3 | |||
1d94782463 | |||
c7a05c3f09 | |||
2dc58a82b7 | |||
755dbbab00 | |||
b470fe22f1 | |||
5b2560ddf7 | |||
be429c527c | |||
65fd537200 | |||
6e43c7f06f | |||
5867b5c000 | |||
05887cf8b0 | |||
c43721d489 | |||
0bf75d52d7 | |||
c35af2dc69 | |||
73a9835a27 | |||
87ab15f754 | |||
6bc608c081 | |||
cbea1d8691 | |||
58f21a69aa | |||
426c5b9a24 | |||
511e90efd1 | |||
bc7b856654 | |||
d50d2f9ca0 | |||
1b53defab3 | |||
3e612921f3 | |||
ec51d43490 | |||
80dc5028f7 | |||
2329c4a75f | |||
ae76cc1ca2 | |||
e4f79f046a | |||
622d837ea1 | |||
c0aa9a8bd4 | |||
9b1052f023 | |||
c77c2f8bd6 | |||
402eaa8f12 | |||
212e8c3654 | |||
7c77b16517 | |||
e5bb0bcba3 | |||
ca4a7d8771 | |||
663f97265a | |||
b91f3048ef | |||
4fe0bf1236 | |||
dd35af3c55 | |||
68f24e47cd | |||
968223a953 | |||
2f287874e3 | |||
c35e7406cd | |||
34b2cca492 | |||
e1bfc04451 | |||
ef0ba7b0c4 | |||
0a2d8880ba | |||
8dcd7e6966 | |||
b744fd6167 | |||
5bcc5c919a | |||
01e12329e9 |
.gitignore
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Client.csprojBTCPayServerClient.Apps.csBTCPayServerClient.Lightning.Internal.csBTCPayServerClient.Lightning.Store.csBTCPayServerClient.OnChainWallet.csBTCPayServerClient.StorePayoutProcessors.csBTCPayServerClient.cs
Models
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
AltcoinTests
FastTests.csGreenfieldAPITests.csPOSTests.csSeleniumTester.csSeleniumTests.csTestAccount.csTestData
ThirdPartyTests.csUnitTest1.csUtilitiesTests.csdocker-compose.altcoins.ymldocker-compose.ymlBTCPayServer
BTCPayServer.csprojProgram.csResourceTracker.csShowQR.cshtmlTemplateEditor.cshtmlVaultElements.cshtmlWalletId.cs_ViewImports.cshtml
Blazor
ColorPalette.csComponents
AppSales
AppTopItems
InvoiceStatus
LabelManager
MainNav
Pager
StoreLightningBalance
StoreLightningServices
StoreNumbers
StoreRecentInvoices
StoreRecentTransactions
StoreSelector
StoreWalletBalance
ThemeSwitch
WalletNav
Controllers
GreenField
GreenfieldAppsController.csGreenfieldInvoiceController.csGreenfieldLightningNodeApiController.Internal.csGreenfieldLightningNodeApiController.Store.csGreenfieldLightningNodeApiController.csGreenfieldNotificationsController.csGreenfieldPullPaymentController.csGreenfieldStoreAutomatedLightningPayoutProcessorsController.csGreenfieldStoreOnChainWalletsController.csGreenfieldStoresController.csGreenfieldUsersController.csLocalBTCPayServerClient.cs
UIAccountController.csUIAppsController.csUIBoltcardController.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLAuthController.csUILNURLController.csUIManageController.APIKeys.csUIManageController.Notifications.csUIManageController.csUINotificationsController.csUIPaymentRequestController.csUIPublicController.csUIPublicLightningNodeInfoController.csUIPullPaymentController.Boltcard.csUIPullPaymentController.csUIServerController.Plugins.csUIServerController.Roles.csUIServerController.Storage.csUIServerController.Translations.csUIServerController.Users.csUIServerController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Dashboard.csUIStoresController.Email.csUIStoresController.Integrations.csUIStoresController.LightningLike.csUIStoresController.Onchain.csUIStoresController.Rates.csUIStoresController.Roles.csUIStoresController.Settings.csUIStoresController.Tokens.csUIStoresController.Users.csUIStoresController.csUIUserStoresController.csUIWalletsController.PSBT.csUIWalletsController.csData/Payouts
DerivationSchemeParser.csEvents
Extensions.csExtensions
Fido2
Filters
Forms
HostedServices
EventHostedServiceBase.csInvoiceBlobMigratorHostedService.csPluginUpdateFetcher.csPullPaymentHostedService.csTransactionLabelMarkerHostedService.cs
Hosting
Models
InvoicingModels
ManageViewModels
NotificationViewModels
PaymentRequestViewModels
ServerViewModels
StoreViewModels
CheckoutAppearanceViewModel.csCreateStoreViewModel.csLightningNodeViewModel.csStoreDerivationScheme.csTokensViewModel.csWalletSettingsViewModel.cs
ViewPullPaymentModel.csWalletViewModels
PaymentRequest
Payments
Bitcoin
BitcoinCheckoutModelExtension.csBitcoinLikePaymentData.csBitcoinLikePaymentHandler.csBitcoinPaymentMethodViewExtension.csBitcoinPaymentPromptDetails.csNBXplorerListener.cs
ICheckoutModelExtension.csIPaymentMethodHandler.csIPaymentMethodViewExtension.csLNURLPay
Lightning
LNCheckoutModelExtension.csLightningLikePaymentData.csLightningLikePaymentHandler.csLightningPaymentMethodViewExtension.csLightningPendingPayoutListener.cs
PaymentMethodId.csPayoutProcessors
BaseAutomatedPayoutProcessor.cs
Lightning
LightningAutomatedPayoutBlob.csLightningAutomatedPayoutProcessor.csLightningAutomatedPayoutSenderFactory.csUILightningAutomatedPayoutProcessorsController.cs
OnChain
OnChainAutomatedPayoutProcessor.csOnChainAutomatedPayoutSenderFactory.csUIOnChainAutomatedPayoutProcessorsController.cs
UIPayoutProcessorsController.csPlugins
Altcoins
Crowdfund
NFC
PayButton
PluginBuilderClient.csPluginManager.csPluginService.csPointOfSale
Shopify
Services
Altcoins
Monero
Payments
MoneroCheckoutModelExtension.csMoneroLikePaymentData.csMoneroLikePaymentMethodHandler.csMoneroPaymentModelExtension.cs
Services
UI
Zcash
Apps
Invoices
LightningHistogramService.csLocalizerService.csMigrationSettings.csNotifications/Blobs
ExternalPayoutTransactionNotification.csInviteAcceptedNotification.csInvoiceEventNotification.csNewUserRequiresApprovalNotification.csNewVersionNotification.csPayoutNotification.cs
PrettyNameProvider.csTranslations.Default.csTranslations.csWallets
Storage/Services
Views
Shared
Bitcoin
BitcoinLikeMethodCheckout.cshtmlBitcoinLikeMethodCheckoutNoScript.cshtmlNBXSyncSummary.cshtmlViewBitcoinLikePaymentData.cshtml
CameraScanner.cshtmlConfirmModal.cshtmlCrowdfund
EmailsBody.cshtmlEmailsTest.cshtmlError.cshtmlLNURL
LayoutHeadStoreBranding.cshtmlLightning
LightningLikeMethodCheckout.cshtmlLightningLikeMethodCheckoutNoScript.cshtmlViewLightningLikePaymentData.cshtml
ListRoles.cshtmlLocalhostBrowserSupport.cshtmlMonero
NFC
NotificationEmailWarning.cshtmlPayButton
PointOfSale
Public
Cart.cshtmlLight.cshtmlMinimalLight.cshtmlPrint.cshtmlRecentTransactions.cshtmlStatic.cshtmlVueLight.cshtml
UpdatePointOfSale.cshtmlZcash
_BTCPaySupporters.cshtml_Footer.cshtml_Layout.cshtml_LayoutSignedOut.cshtml_StatusMessage.cshtmlUIAccount
CheatPermissions.cshtmlForgotPassword.cshtmlForgotPasswordConfirmation.cshtmlLockout.cshtmlLogin.cshtmlLoginWith2fa.cshtmlLoginWithFido2.cshtmlLoginWithLNURLAuth.cshtmlLoginWithRecoveryCode.cshtmlRegister.cshtmlSecondaryLogin.cshtmlSignedOut.cshtml
UIApps
UIError
UIFido2
UIForms
UIHome
UIInvoice
Checkout-Cheating.cshtmlCheckout-Testing.cshtmlCheckout.cshtmlCheckoutNoScript.cshtmlCreateInvoice.cshtmlInvoice.cshtmlInvoiceReceipt.cshtmlInvoiceReceiptPrint.cshtmlInvoiceStatusChangePartial.cshtmlListInvoices.cshtmlListInvoicesPaymentsPartial.cshtml_RefundModal.cshtml
UILNURL
UILNURLAuth
UILightningAutomatedPayoutProcessors
UILightningLikePayout
UIManage
APIKeys.cshtmlAddApiKey.cshtmlAuthorizeAPIKey.cshtmlChangePassword.cshtmlConfirmAPIKey.cshtmlEnableAuthenticator.cshtmlGenerateRecoveryCodes.cshtmlIndex.cshtmlLoginCodes.cshtmlNotificationSettings.cshtmlSetPassword.cshtmlTwoFactorAuthentication.cshtml
UIMoneroLikeStore
UINotifications
UIOnChainAutomatedPayoutProcessors
UIPaymentRequest
UIPayoutProcessors
UIPublic
UIPublicLightningNodeInfo
UIPullPayment
UIReports
UIServer
Branding.cshtmlCLightningRestServices.cshtmlConfiguratorService.cshtmlCreateTemporaryFileUrl.cshtmlCreateUser.cshtmlDynamicDnsService.cshtmlDynamicDnsServices.cshtmlEditAmazonS3StorageProvider.cshtmlEditAzureBlobStorageStorageProvider.cshtmlEditDictionary.cshtmlEditFileSystemStorageProvider.cshtmlEditGoogleCloudStorageStorageProvider.cshtmlEmails.cshtmlFiles.cshtmlLightningChargeServices.cshtmlLightningWalletServices.cshtmlListDictionaries.cshtmlListPlugins.cshtmlListStores.cshtmlListUsers.cshtmlLndSeedBackup.cshtmlLndServices.cshtmlLogs.cshtmlMaintenance.cshtmlP2PService.cshtmlPolicies.cshtmlRPCService.cshtmlResetUserPassword.cshtmlSSHService.cshtmlServices.cshtmlStorage.cshtmlUser.cshtml
UIShopify
UIStorePullPayments
UIStores
CheckoutAppearance.cshtmlCreateToken.cshtmlDashboard.cshtmlGeneralSettings.cshtmlGenerateWallet.cshtmlGenerateWalletOptions.cshtml
ImportWallet
ImportWalletOptions.cshtmlLightning.cshtmlLightningSettings.cshtmlListTokens.cshtmlModifyWebhook.cshtmlRates.cshtmlRequestPairing.cshtmlSetupLightningNode.cshtmlSetupWallet.cshtmlShowToken.cshtmlStoreEmailSettings.cshtmlStoreEmails.cshtmlStoreUsers.cshtmlTestWebhook.cshtmlWalletSettings.cshtmlWebhooks.cshtml_GenerateWalletForm.cshtml_LayoutWalletSetup.cshtmlUIUserStores
UIWallets
CoinSelection.cshtmlListWallets.cshtmlSignWithSeed.cshtmlWalletLabels.cshtmlWalletPSBT.cshtmlWalletPSBTCombine.cshtmlWalletPSBTDecoded.cshtmlWalletReceive.cshtmlWalletRescan.cshtmlWalletSend.cshtmlWalletSendVault.cshtmlWalletSigningOptions.cshtmlWalletTransactions.cshtml_PSBTInfo.cshtml_WalletTransactionsList.cshtml
UIZcashLikeStore
wwwroot
checkout
crowdfund
img
js
locales
am-ET.jsonar.jsonaz.jsonbg-BG.jsonbs-BA.jsonca-ES.json
checkout
bg-BG.jsoncs-CZ.jsonde-DE.jsonel-GR.jsonfa.jsonfi-FI.jsonhu-HU.jsonis-IS.jsonja-JP.jsonko.jsonnl-NL.jsonpl.jsonpt-BR.jsonro.jsonsk-SK.json
cs-CZ.jsonda-DK.jsonde-DE.jsonel-GR.jsonen.jsones-ES.jsonfa.jsonfi-FI.jsonfr-FR.jsonhe.jsonhi.jsonhr-HR.jsonhu-HU.jsonhy.jsonid.jsonis-IS.jsonit-IT.jsonja-JP.jsonka.jsonkk-KZ.jsonko.jsonlv.jsonnl-NL.jsonno.jsonnp-NP.jsonpl.jsonpt-BR.jsonpt-PT.jsonro.jsonru-RU.jsonsk-SK.jsonsl-SI.jsonsr.jsonsv.jsonth-TH.jsontr.jsonuk-UA.jsonvi-VN.jsonzh-SG.jsonzh-SP.jsonzh-TW.jsonzu.jsonmain
pos
swagger/v1
swagger.template.apps.jsonswagger.template.invoices.jsonswagger.template.jsonswagger.template.lightning.internal.jsonswagger.template.lightning.store.jsonswagger.template.pull-payments.jsonswagger.template.stores-wallet.on-chain.json
vendor/font-awesome
Build
Changelog.md
1
.gitignore
vendored
1
.gitignore
vendored
@ -266,6 +266,7 @@ paket-files/
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
.run
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
@ -8,6 +8,14 @@ namespace BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
public static class SetStatusMessageModelExtensions
|
||||
{
|
||||
public static void SetStatusSuccess(this ITempDataDictionary tempData, string statusMessage)
|
||||
{
|
||||
tempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = statusMessage
|
||||
});
|
||||
}
|
||||
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
|
||||
{
|
||||
if (statusMessage == null)
|
||||
@ -26,19 +34,14 @@ public static class SetStatusMessageModelExtensions
|
||||
tempData.TryGetValue("StatusMessageModel", out var model);
|
||||
if (successMessage != null || errorMessage != null)
|
||||
{
|
||||
var parsedModel = new StatusMessageModel();
|
||||
parsedModel.Message = (string)successMessage ?? (string)errorMessage;
|
||||
if (successMessage != null)
|
||||
var parsedModel = new StatusMessageModel
|
||||
{
|
||||
parsedModel.Severity = StatusMessageModel.StatusSeverity.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedModel.Severity = StatusMessageModel.StatusSeverity.Error;
|
||||
}
|
||||
Message = (string)successMessage ?? (string)errorMessage,
|
||||
Severity = successMessage != null ? StatusMessageModel.StatusSeverity.Success : StatusMessageModel.StatusSeverity.Error
|
||||
};
|
||||
return parsedModel;
|
||||
}
|
||||
else if (model != null && model is string str)
|
||||
if (model is string str)
|
||||
{
|
||||
return JObject.Parse(str).ToObject<StatusMessageModel>();
|
||||
}
|
||||
|
@ -14,14 +14,6 @@ namespace BTCPayServer.Abstractions.Models
|
||||
|
||||
public string SeverityCSS => ToString(Severity);
|
||||
|
||||
private void ParseNonJsonStatus(string s)
|
||||
{
|
||||
Message = s;
|
||||
Severity = s.StartsWith("Error", StringComparison.InvariantCultureIgnoreCase)
|
||||
? StatusSeverity.Error
|
||||
: StatusSeverity.Success;
|
||||
}
|
||||
|
||||
public static string ToString(StatusSeverity severity)
|
||||
{
|
||||
switch (severity)
|
||||
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Services
|
||||
{
|
||||
public class UIExtension : IUIExtension
|
||||
{
|
||||
[Obsolete("Use extension method BTCPayServer.Extensions.AddUIExtension(this IServiceCollection services, string location, string partialViewName) instead")]
|
||||
public UIExtension(string partial, string location)
|
||||
{
|
||||
Partial = partial;
|
||||
|
@ -16,7 +16,7 @@
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">2.0.1</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@ -31,7 +31,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.45" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -78,4 +78,14 @@ public partial class BTCPayServerClient
|
||||
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||
await SendHttpRequest($"api/v1/apps/{appId}", null, HttpMethod.Delete, token);
|
||||
}
|
||||
|
||||
public virtual async Task<FileData> UploadAppItemImage(string appId, string filePath, string mimeType, CancellationToken token = default)
|
||||
{
|
||||
return await UploadFileRequest<FileData>($"api/v1/apps/{appId}/image", filePath, mimeType, "file", HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteAppItemImage(string appId, string fileId, CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/apps/{appId}/image/{fileId}", null, HttpMethod.Delete, token);
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,13 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/server/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<HistogramData> GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = type == null ? null : new Dictionary<string, object> { { "type", type.ToString() } };
|
||||
return await SendHttpRequest<HistogramData>($"api/v1/server/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
@ -21,6 +21,13 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<HistogramData> GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = type == null ? null : new Dictionary<string, object> { { "type", type.ToString() } };
|
||||
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
@ -16,6 +16,14 @@ public partial class BTCPayServerClient
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<HistogramData> GetOnChainWalletHistogram(string storeId, string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = type == null ? null : new Dictionary<string, object> { { "type", type.ToString() } };
|
||||
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/histogram", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
@ -19,14 +19,14 @@ public partial class BTCPayServerClient
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payout-processors/{processor}/{paymentMethod}", null, HttpMethod.Delete, token);
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<LightningAutomatedPayoutSettings>> GetStoreLightningAutomatedPayoutProcessors(string storeId, string? paymentMethod = null, CancellationToken token = default)
|
||||
public virtual async Task<IEnumerable<LightningAutomatedPayoutSettings>> GetStoreLightningAutomatedPayoutProcessors(string storeId, string? payoutMethodId = null, CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<IEnumerable<LightningAutomatedPayoutSettings>>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory{(paymentMethod is null ? string.Empty : $"/{paymentMethod}")}", null, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<IEnumerable<LightningAutomatedPayoutSettings>>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory{(payoutMethodId is null ? string.Empty : $"/{payoutMethodId}")}", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningAutomatedPayoutSettings> UpdateStoreLightningAutomatedPayoutProcessors(string storeId, string paymentMethod, LightningAutomatedPayoutSettings request, CancellationToken token = default)
|
||||
public virtual async Task<LightningAutomatedPayoutSettings> UpdateStoreLightningAutomatedPayoutProcessors(string storeId, string payoutMethodId, LightningAutomatedPayoutSettings request, CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<LightningAutomatedPayoutSettings>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}", request, HttpMethod.Put, token);
|
||||
return await SendHttpRequest<LightningAutomatedPayoutSettings>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{payoutMethodId}", request, HttpMethod.Put, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainAutomatedPayoutSettings> UpdateStoreOnChainAutomatedPayoutProcessors(string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request, CancellationToken token = default)
|
||||
|
@ -156,7 +156,7 @@ public partial class BTCPayServerClient
|
||||
protected virtual async Task<T> UploadFileRequest<T>(string apiPath, string filePath, string mimeType, string formFieldName, HttpMethod method = null, CancellationToken token = default)
|
||||
{
|
||||
using MultipartFormDataContent multipartContent = new();
|
||||
var fileContent = new StreamContent(File.OpenRead(filePath));
|
||||
using var fileContent = new StreamContent(File.OpenRead(filePath));
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType);
|
||||
multipartContent.Add(fileContent, formFieldName, fileName);
|
||||
|
35
BTCPayServer.Client/Models/AppItem.cs
Normal file
35
BTCPayServer.Client/Models/AppItem.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public enum AppItemPriceType
|
||||
{
|
||||
Fixed,
|
||||
Topup,
|
||||
Minimum
|
||||
}
|
||||
|
||||
public class AppItem
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string[] Categories { get; set; }
|
||||
public string Image { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public AppItemPriceType PriceType { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? Price { get; set; }
|
||||
public string BuyButtonText { get; set; }
|
||||
public int? Inventory { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||
}
|
@ -37,7 +37,7 @@ public abstract class CrowdfundBaseData : AppBaseData
|
||||
|
||||
public class CrowdfundAppData : CrowdfundBaseData
|
||||
{
|
||||
public object? Perks { get; set; }
|
||||
public AppItem[]? Perks { get; set; }
|
||||
}
|
||||
|
||||
public class CrowdfundAppRequest : CrowdfundBaseData, IAppRequest
|
||||
|
30
BTCPayServer.Client/Models/HistogramData.cs
Normal file
30
BTCPayServer.Client/Models/HistogramData.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public enum HistogramType
|
||||
{
|
||||
Week,
|
||||
Month,
|
||||
YTD,
|
||||
Year,
|
||||
TwoYears,
|
||||
Day
|
||||
}
|
||||
|
||||
public class HistogramData
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public HistogramType Type { get; set; }
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public List<decimal> Series { get; set; }
|
||||
[JsonProperty(ItemConverterType = typeof(DateTimeToUnixTimeConverter))]
|
||||
public List<DateTimeOffset> Labels { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Balance { get; set; }
|
||||
}
|
@ -11,7 +11,6 @@ public class LightningAutomatedPayoutSettings
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan IntervalSeconds { get; set; }
|
||||
|
||||
public int? CancelPayoutAfterFailures { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
|
||||
|
@ -11,7 +11,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string Body { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public bool Seen { get; set; }
|
||||
public Uri Link { get; set; }
|
||||
public string Link { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset CreatedTime { get; set; }
|
||||
|
@ -1,8 +1,6 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
@ -31,7 +29,7 @@ public abstract class PointOfSaleBaseData : AppBaseData
|
||||
|
||||
public class PointOfSaleAppData : PointOfSaleBaseData
|
||||
{
|
||||
public object? Items { get; set; }
|
||||
public AppItem[]? Items { get; set; }
|
||||
}
|
||||
|
||||
public class PointOfSaleAppRequest : PointOfSaleBaseData, IAppRequest
|
||||
|
@ -4,10 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.4" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Altcoins\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -8,7 +8,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.24" />
|
||||
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.31" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
|
@ -137,9 +137,10 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
return paymentType switch
|
||||
{
|
||||
"BTCLike" => $"{cryptoCode}-CHAIN",
|
||||
"BTCLike" or "MoneroLike" or "ZcashLike" => $"{cryptoCode}-CHAIN",
|
||||
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
|
||||
"LNURLPAY" => $"{cryptoCode}-LNURL",
|
||||
|
||||
_ => throw new NotSupportedException("Unknown payment type " + paymentType)
|
||||
};
|
||||
}
|
||||
@ -147,6 +148,23 @@ namespace BTCPayServer.Data
|
||||
return $"{splitted[0]}-CHAIN";
|
||||
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
|
||||
}
|
||||
public static string TryMigratePaymentMethodId(string paymentMethodId)
|
||||
{
|
||||
var splitted = paymentMethodId.Split(new[] { '_', '-' });
|
||||
if (splitted is [var cryptoCode, var paymentType])
|
||||
{
|
||||
return paymentType switch
|
||||
{
|
||||
"BTCLike" or "MoneroLike" or "ZcashLike" => $"{cryptoCode}-CHAIN",
|
||||
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
|
||||
"LNURLPAY" => $"{cryptoCode}-LNURL",
|
||||
_ => paymentMethodId
|
||||
};
|
||||
}
|
||||
if (splitted.Length == 1)
|
||||
return $"{splitted[0]}-CHAIN";
|
||||
return paymentMethodId;
|
||||
}
|
||||
|
||||
// Make postgres happy
|
||||
public static string SanitizeJSON(string json) => json.Replace("\\u0000", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
@ -92,7 +92,7 @@ namespace BTCPayServer.Data
|
||||
blob.Remove("output");
|
||||
blob.Remove("outpoint");
|
||||
// Convert from sats to btc
|
||||
if (cryptoData["value"] is not (null or { Type: JTokenType.Null }))
|
||||
if (cryptoData["value"] is not (null or { Type: JTokenType.Null } or { Type: JTokenType.Object }))
|
||||
{
|
||||
var v = cryptoData["value"].Value<long>();
|
||||
Amount = (decimal)v / (decimal)Money.COIN;
|
||||
@ -103,7 +103,22 @@ namespace BTCPayServer.Data
|
||||
blob.ConvertNumberToString("paymentMethodFee");
|
||||
blob.Remove("networkFee");
|
||||
blob.RemoveIfNull("paymentMethodFee");
|
||||
}
|
||||
}
|
||||
// Liquid
|
||||
else if (cryptoData["value"] is { Type: JTokenType.Object })
|
||||
{
|
||||
var v = cryptoData["value"]["value"].Value<long>();
|
||||
var assetId = cryptoData["value"]["assetId"].Value<string>();
|
||||
divisibility = GetDivisibility(assetId) ?? 8;
|
||||
Amount = (decimal)v / (decimal)Math.Pow(10.0, divisibility);
|
||||
cryptoData.Remove("value");
|
||||
cryptoData["assetId"] = assetId;
|
||||
blob["paymentMethodFee"] = blob["networkFee"];
|
||||
blob.RemoveIfValue<decimal>("paymentMethodFee", 0.0m);
|
||||
blob.ConvertNumberToString("paymentMethodFee");
|
||||
blob.Remove("networkFee");
|
||||
blob.RemoveIfNull("paymentMethodFee");
|
||||
}
|
||||
// Convert from millisats to btc
|
||||
else if (cryptoData["amount"] is not (null or { Type: JTokenType.Null }))
|
||||
{
|
||||
@ -164,6 +179,17 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return true;
|
||||
}
|
||||
|
||||
private int? GetDivisibility(string assetId) =>
|
||||
assetId switch
|
||||
{
|
||||
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2" => 8,
|
||||
"aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf" => 2,
|
||||
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a" => 8,
|
||||
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d" => 8,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
[NotMapped]
|
||||
|
@ -1,11 +1,9 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -73,7 +71,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasColumnType("JSONB");
|
||||
.HasColumnType("jsonb");
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Proof)
|
||||
.HasColumnType("JSONB");
|
||||
|
@ -560,7 +560,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Blob")
|
||||
.HasColumnType("JSONB");
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.HasColumnType("text");
|
||||
|
@ -6,7 +6,7 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.45" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
@ -3,10 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
@ -25,7 +24,7 @@ using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
using WalletSettingsViewModel = BTCPayServer.Models.StoreViewModels.WalletSettingsViewModel;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@ -320,10 +319,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
|
||||
var checkout =
|
||||
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
|
||||
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id)
|
||||
.GetAwaiter().GetResult()).Value;
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
Assert.Single(checkout.AvailablePaymentMethods);
|
||||
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
|
||||
|
||||
//////////////////////
|
||||
|
||||
@ -337,7 +336,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
|
||||
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id)
|
||||
.GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal("Processing", checkout.Status);
|
||||
});
|
||||
@ -475,10 +474,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
|
||||
var checkout =
|
||||
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null)
|
||||
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null)
|
||||
.GetAwaiter().GetResult()).Value;
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("BTC", checkout.CryptoCode);
|
||||
Assert.Single(checkout.AvailablePaymentMethods);
|
||||
Assert.Equal("BTC", checkout.PaymentMethodCurrency);
|
||||
|
||||
Assert.Single(invoice.PaymentCodes);
|
||||
Assert.Single(invoice.SupportedTransactionCurrencies);
|
||||
@ -536,10 +535,10 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
|
||||
controller = tester.PayTester.GetController<UIInvoiceController>(null);
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
|
||||
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
|
||||
.GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal(2, checkout.AvailableCryptos.Count);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
Assert.Equal(2, checkout.AvailablePaymentMethods.Count);
|
||||
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
|
||||
|
||||
Assert.Equal(2, invoice.PaymentCodes.Count());
|
||||
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
|
||||
@ -759,39 +758,6 @@ noninventoryitem:
|
||||
AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
|
||||
}, 10000);
|
||||
|
||||
//test payment methods option
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "hello";
|
||||
vmpos.Currency = "BTC";
|
||||
vmpos.Template = @"
|
||||
btconly:
|
||||
price: 1.0
|
||||
title: good apple
|
||||
payment_methods:
|
||||
- BTC
|
||||
normal:
|
||||
price: 1.0";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
|
||||
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
|
||||
Assert.Single(btcOnlyInvoice.CryptoInfo);
|
||||
Assert.Equal("BTC",
|
||||
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
|
||||
Assert.Equal("BTC-CHAIN",
|
||||
btcOnlyInvoice.CryptoInfo.First().PaymentType);
|
||||
|
||||
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
|
||||
Assert.Contains(
|
||||
normalInvoice.CryptoInfo,
|
||||
s => "BTC-CHAIN" == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
|
||||
s.CryptoCode));
|
||||
|
||||
//test topup option
|
||||
vmpos.Template = @"
|
||||
a:
|
||||
@ -821,13 +787,13 @@ g:
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.DoesNotContain("custom", vmpos.Template);
|
||||
var items = AppService.Parse(vmpos.Template);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.PriceType == AppItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.PriceType == AppItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.PriceType == AppItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.PriceType == AppItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "e" && item.PriceType == AppItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.PriceType == AppItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.PriceType == AppItemPriceType.Topup);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
|
||||
|
@ -63,7 +63,6 @@ namespace BTCPayServer.Tests
|
||||
//no tether on our regtest, lets create it and set it
|
||||
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
|
||||
var lbtc = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("LBTC");
|
||||
var etb = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("ETB");
|
||||
var issueAssetResult = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
|
||||
tether.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString());
|
||||
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network)
|
||||
@ -71,15 +70,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(tether.AssetId, tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT").AssetId);
|
||||
Assert.Equal(tether.AssetId, ((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network).AssetId);
|
||||
|
||||
var issueAssetResult2 = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
|
||||
etb.AssetId = uint256.Parse(issueAssetResult2.Result["asset"].ToString());
|
||||
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("ETB").Network)
|
||||
.AssetId = etb.AssetId;
|
||||
|
||||
|
||||
user.RegisterDerivationScheme("LBTC");
|
||||
user.RegisterDerivationScheme("USDT");
|
||||
user.RegisterDerivationScheme("ETB");
|
||||
|
||||
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
|
||||
@ -109,11 +103,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT", StringComparison.InvariantCultureIgnoreCase)).Payments);
|
||||
});
|
||||
|
||||
//test precision based on https://github.com/ElementsProject/elements/issues/805#issuecomment-601277606
|
||||
var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21, etb.NBitcoinNetwork);
|
||||
//precision = 2, 1ETB = 0.00000100
|
||||
Assert.Equal(100, etbBip21.Amount.Satoshi);
|
||||
|
||||
|
||||
var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21, lbtc.NBitcoinNetwork);
|
||||
//precision = 8, 0.1 = 0.1
|
||||
|
@ -679,6 +679,26 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(utxo54, utxos[53]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResourceTrackerTest()
|
||||
{
|
||||
var tracker = new ResourceTracker<string>();
|
||||
var t1 = tracker.StartTracking();
|
||||
Assert.True(t1.TryTrack("1"));
|
||||
Assert.False(t1.TryTrack("1"));
|
||||
var t2 = tracker.StartTracking();
|
||||
Assert.True(t2.TryTrack("2"));
|
||||
Assert.False(t2.TryTrack("1"));
|
||||
Assert.True(t1.Contains("1"));
|
||||
Assert.True(t2.Contains("2"));
|
||||
Assert.True(tracker.Contains("1"));
|
||||
Assert.True(tracker.Contains("2"));
|
||||
t1.Dispose();
|
||||
Assert.False(tracker.Contains("1"));
|
||||
Assert.True(tracker.Contains("2"));
|
||||
Assert.True(t2.TryTrack("1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanAcceptInvoiceWithTolerance()
|
||||
{
|
||||
@ -848,6 +868,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<MultisigDerivationStrategy>(((P2WSHDerivationStrategy)strategyBase).Inner);
|
||||
Assert.Equal(expected, strategyBase.ToString());
|
||||
|
||||
foreach (var space in new[] { "\r\n", " ", "\t" })
|
||||
{
|
||||
var expectedWithNewLines = $"2-of-tpubDDXgATYzdQkHHhZZCMcNJj8BGDENvzMVou5v9NdxiP4rxDLj33nS233dGFW4htpVZSJ6zds9eVqAV9RyRHHiKtwQKX8eR4n4KN3Dwmj7A3h-{space}tpubDC8a54NFtQtMQAZ97VhoU9V6jVTvi9w4Y5SaAXJSBYETKg3AoX5CCKndznhPWxJUBToPCpT44s86QbKdGpKAnSjcMTGW4kE6UQ8vpBjcybW-tpubDChjnP9LXNrJp43biqjY7FH93wgRRNrNxB4Q8pH7PPRy8UPcH2S6V46WGVJ47zVGF7SyBJNCpnaogsFbsybVQckGtVhCkng3EtFn8qmxptS";
|
||||
strategyBase = parser.Parse(expectedWithNewLines);
|
||||
Assert.Equal(expected, strategyBase.ToString());
|
||||
}
|
||||
|
||||
var inner = (MultisigDerivationStrategy)((P2WSHDerivationStrategy)strategyBase).Inner;
|
||||
Assert.False(inner.IsLegacy);
|
||||
Assert.Equal(3, inner.Keys.Count);
|
||||
@ -864,7 +891,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
|
||||
|
||||
// Failure cases
|
||||
Assert.Throws<FormatException>(() => { parser.Parse("xpub 661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); }); // invalid format because of space
|
||||
Assert.Throws<FormatException>(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
|
||||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
|
||||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.Lightning;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
@ -317,6 +318,20 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(await client.GetFiles());
|
||||
storeData = await client.GetStore(store.Id);
|
||||
Assert.Null(storeData.LogoUrl);
|
||||
|
||||
// App Item Image
|
||||
var app = await client.CreatePointOfSaleApp(store.Id, new PointOfSaleAppRequest { AppName = "Test App" });
|
||||
await AssertValidationError(["file"],
|
||||
async () => await client.UploadAppItemImage(app.Id, filePath, "text/csv")
|
||||
);
|
||||
|
||||
var fileData = await client.UploadAppItemImage(app.Id, logoPath, "image/png");
|
||||
Assert.Equal("logo.png", fileData.OriginalName);
|
||||
files = await client.GetFiles();
|
||||
Assert.Single(files);
|
||||
|
||||
await client.DeleteAppItemImage(app.Id, fileData.Id);
|
||||
Assert.Empty(await client.GetFiles());
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -744,9 +759,9 @@ namespace BTCPayServer.Tests
|
||||
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 item1 = new AppItem { Id = "item1", Title = "Item 1", Price = 1, PriceType = AppItemPriceType.Fixed };
|
||||
var item2 = new AppItem { Id = "item2", Title = "Item 2", Price = 2, PriceType = AppItemPriceType.Fixed };
|
||||
var item3 = new AppItem { Id = "item3", Title = "Item 3", Price = 3, PriceType = AppItemPriceType.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" });
|
||||
@ -1399,10 +1414,17 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(0, card.Version);
|
||||
var card1keys = new[] { card.K0, card.K1, card.K2, card.K3, card.K4 };
|
||||
Assert.DoesNotContain(null, card1keys);
|
||||
|
||||
var card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
UID = uid
|
||||
});
|
||||
Assert.Equal(0, card2.Version);
|
||||
card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
UID = uid,
|
||||
OnExisting = OnExistingBehavior.UpdateVersion
|
||||
});
|
||||
Assert.Equal(1, card2.Version);
|
||||
Assert.StartsWith("lnurlw://", card2.LNURLW);
|
||||
Assert.EndsWith("/boltcard", card2.LNURLW);
|
||||
@ -2733,7 +2755,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.EndsWith($"/i/{newInvoice.Id}", newInvoice.CheckoutLink);
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
|
||||
var model = (PaymentModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
|
||||
var model = (CheckoutModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
|
||||
Assert.Equal("it-IT", model.DefaultLang);
|
||||
Assert.Equal("http://toto.com/lol", model.MerchantRefLink);
|
||||
|
||||
@ -2980,9 +3002,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// check list for store with paid invoice
|
||||
var merchantInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC");
|
||||
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
|
||||
Assert.NotEmpty(merchantInvoices);
|
||||
Assert.Empty(merchantPendingInvoices);
|
||||
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
|
||||
Assert.True(merchantPendingInvoices.Length < merchantInvoices.Length);
|
||||
Assert.All(merchantPendingInvoices, m => Assert.Equal(LightningInvoiceStatus.Unpaid, m.Status));
|
||||
// if the test ran too many times the invoice might be on a later page
|
||||
if (merchantInvoices.Length < 100)
|
||||
Assert.Contains(merchantInvoices, i => i.Id == merchantInvoice.Id);
|
||||
@ -2999,6 +3022,17 @@ namespace BTCPayServer.Tests
|
||||
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
// balance
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var balance = await client.GetLightningNodeBalance(user.StoreId, "BTC");
|
||||
var histogram = await client.GetLightningNodeHistogram(user.StoreId, "BTC");
|
||||
var localBalance = balance.OffchainBalance.Local.ToDecimal(LightMoneyUnit.BTC);
|
||||
Assert.Equal(histogram.Balance, histogram.Series.Last());
|
||||
Assert.Equal(localBalance, histogram.Balance);
|
||||
Assert.Equal(localBalance, histogram.Series.Last());
|
||||
});
|
||||
|
||||
// As admin, can use the internal node through our store.
|
||||
await user.MakeAdmin(true);
|
||||
@ -3021,6 +3055,10 @@ namespace BTCPayServer.Tests
|
||||
client = await guest.CreateClient(Policies.CanUseLightningNodeInStore);
|
||||
// Can use lightning node is only granted to store's owner
|
||||
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
|
||||
|
||||
// balance and histogram should not be accessible with view only clients
|
||||
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeBalance(user.StoreId, "BTC"));
|
||||
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeHistogram(user.StoreId, "BTC"));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 20 * 1000)]
|
||||
@ -3046,7 +3084,7 @@ namespace BTCPayServer.Tests
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = "USD",
|
||||
Amount = 100,
|
||||
Amount = 0.1m,
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions
|
||||
{
|
||||
PaymentMethods = new[] { "BTC-LN" },
|
||||
@ -3536,8 +3574,7 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
|
||||
Assert.Equal(0m, overview.Balance);
|
||||
|
||||
|
||||
|
||||
var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode);
|
||||
Assert.NotNull(fee.FeeRate);
|
||||
|
||||
@ -3583,6 +3620,17 @@ namespace BTCPayServer.Tests
|
||||
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
|
||||
Assert.Equal(0.01m, overview.Balance);
|
||||
|
||||
// histogram should not be accessible with view only clients
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.GetOnChainWalletHistogram(walletId.StoreId, walletId.CryptoCode);
|
||||
});
|
||||
var histogram = await client.GetOnChainWalletHistogram(walletId.StoreId, walletId.CryptoCode);
|
||||
Assert.Equal(histogram.Balance, histogram.Series.Last());
|
||||
Assert.Equal(0.01m, histogram.Balance);
|
||||
Assert.Equal(0.01m, histogram.Series.Last());
|
||||
Assert.Equal(0, histogram.Series.First());
|
||||
|
||||
//the simplest request:
|
||||
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
|
||||
var createTxRequest = new CreateOnChainTransactionRequest()
|
||||
@ -3776,7 +3824,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
|
||||
await tester.ExplorerNode.GenerateAsync(1);
|
||||
}, bevent => bevent.CryptoCode.Equals("BTC", StringComparison.Ordinal));
|
||||
}, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
|
||||
|
||||
Assert.Contains(
|
||||
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
|
||||
@ -4171,13 +4219,17 @@ namespace BTCPayServer.Tests
|
||||
await admin.GrantAccessAsync(true);
|
||||
|
||||
var adminClient = await admin.CreateClient(Policies.Unrestricted);
|
||||
admin.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
|
||||
admin.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
var payoutAmount = LightMoney.Satoshis(1000);
|
||||
var inv = await tester.MerchantLnd.Client.CreateInvoice(payoutAmount, "Donation to merchant", TimeSpan.FromHours(1), default);
|
||||
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
|
||||
Assert.Equal(PayResult.Ok, resp.Result);
|
||||
|
||||
var ppService = tester.PayTester.GetService<HostedServices.PullPaymentHostedService>();
|
||||
var serializers = tester.PayTester.GetService<BTCPayNetworkJsonSerializerSettings>();
|
||||
var store = tester.PayTester.GetService<StoreRepository>();
|
||||
var dbContextFactory = tester.PayTester.GetService<Data.ApplicationDbContextFactory>();
|
||||
|
||||
Assert.True(await store.InternalNodePayoutAuthorized(admin.StoreId));
|
||||
Assert.False(await store.InternalNodePayoutAuthorized("blah"));
|
||||
await admin.MakeAdmin(false);
|
||||
@ -4201,8 +4253,8 @@ namespace BTCPayServer.Tests
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var payoutC =
|
||||
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
|
||||
Assert.Equal(PayoutState.Completed, payoutC.State);
|
||||
(await adminClient.GetStorePayouts(admin.StoreId, false)).SingleOrDefault(data => data.Id == payout.Id);
|
||||
Assert.Equal(PayoutState.Completed, payoutC?.State);
|
||||
});
|
||||
|
||||
payout = await adminClient.CreatePayout(admin.StoreId,
|
||||
@ -4244,6 +4296,36 @@ namespace BTCPayServer.Tests
|
||||
Destination = customerInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(payout2.OriginalAmount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
|
||||
// Checking if we can disable a payout...
|
||||
var allLNPayouts = await ppService.GetPayouts(new ()
|
||||
{
|
||||
PayoutIds = new[] { payout2.Id },
|
||||
Processor = LightningAutomatedPayoutSenderFactory.ProcessorName
|
||||
});
|
||||
Assert.NotEmpty(allLNPayouts);
|
||||
var b = JsonConvert.DeserializeObject<Data.PayoutBlob>(allLNPayouts[0].Blob);
|
||||
b.DisableProcessor(LightningAutomatedPayoutSenderFactory.ProcessorName);
|
||||
Assert.Equal(1, b.IncrementErrorCount());
|
||||
Assert.Equal(2, b.IncrementErrorCount());
|
||||
allLNPayouts[0].Blob = JsonConvert.SerializeObject(b);
|
||||
Assert.Equal(3, JsonConvert.DeserializeObject<Data.PayoutBlob>(allLNPayouts[0].Blob).IncrementErrorCount());
|
||||
using var ctx = dbContextFactory.CreateContext();
|
||||
var p = ctx.Payouts.Find(allLNPayouts[0].Id);
|
||||
p.Blob = allLNPayouts[0].Blob;
|
||||
await ctx.SaveChangesAsync();
|
||||
var allLNPayouts2 = await ppService.GetPayouts(new()
|
||||
{
|
||||
PayoutIds = new[] { payout2.Id },
|
||||
Processor = LightningAutomatedPayoutSenderFactory.ProcessorName
|
||||
});
|
||||
Assert.DoesNotContain(allLNPayouts[0].Id, allLNPayouts2.Select(a => a.Id));
|
||||
allLNPayouts2 = await ppService.GetPayouts(new()
|
||||
{
|
||||
PayoutIds = new[] { payout2.Id },
|
||||
Processor = "hello"
|
||||
});
|
||||
Assert.Contains(allLNPayouts[0].Id, allLNPayouts2.Select(a => a.Id));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Hosting;
|
||||
@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static BTCPayServer.Tests.UnitTest1;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -76,9 +78,8 @@ fruit tea:
|
||||
Assert.Null( parsedDefault[0].BuyButtonText);
|
||||
Assert.Equal( "~/img/pos-sample/green-tea.jpg" ,parsedDefault[0].Image);
|
||||
Assert.Equal( 1 ,parsedDefault[0].Price);
|
||||
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Fixed ,parsedDefault[0].PriceType);
|
||||
Assert.Equal( AppItemPriceType.Fixed ,parsedDefault[0].PriceType);
|
||||
Assert.Null( parsedDefault[0].AdditionalData);
|
||||
Assert.Null( parsedDefault[0].PaymentMethods);
|
||||
|
||||
|
||||
Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title);
|
||||
@ -87,9 +88,8 @@ fruit tea:
|
||||
Assert.Null( parsedDefault[4].BuyButtonText);
|
||||
Assert.Equal( "~/img/pos-sample/herbal-tea.jpg" ,parsedDefault[4].Image);
|
||||
Assert.Equal( 1.8m ,parsedDefault[4].Price);
|
||||
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Minimum ,parsedDefault[4].PriceType);
|
||||
Assert.Equal( AppItemPriceType.Minimum ,parsedDefault[4].PriceType);
|
||||
Assert.Null( parsedDefault[4].AdditionalData);
|
||||
Assert.Null( parsedDefault[4].PaymentMethods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -183,15 +183,17 @@ retry:
|
||||
Driver.AssertNoError();
|
||||
CreatedUser = usr;
|
||||
Password = "123456";
|
||||
IsAdmin = isAdmin;
|
||||
return usr;
|
||||
}
|
||||
string CreatedUser;
|
||||
|
||||
public string Password { get; private set; }
|
||||
public bool IsAdmin { get; private set; }
|
||||
|
||||
public TestAccount AsTestAccount()
|
||||
{
|
||||
return new TestAccount(Server) { RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser } };
|
||||
return new TestAccount(Server) { StoreId = StoreId, Email = CreatedUser, Password = Password, RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser }, IsAdmin = IsAdmin };
|
||||
}
|
||||
|
||||
public (string storeName, string storeId) CreateNewStore(bool keepId = true)
|
||||
@ -641,7 +643,15 @@ retry:
|
||||
GoToUrl(url);
|
||||
Assert.DoesNotMatch("404 - Page not found</h", Driver.PageSource);
|
||||
if (shouldHaveAccess)
|
||||
{
|
||||
Assert.DoesNotMatch("- Denied</h", Driver.PageSource);
|
||||
// check associated link is active if present
|
||||
var sidebarLink = Driver.FindElements(By.CssSelector($"#mainNav a[href=\"{url}\"]")).FirstOrDefault();
|
||||
if (sidebarLink != null)
|
||||
{
|
||||
Assert.Contains("active", sidebarLink.GetAttribute("class"));
|
||||
}
|
||||
}
|
||||
else
|
||||
Assert.Contains("- Denied</h", Driver.PageSource);
|
||||
}
|
||||
|
@ -2487,7 +2487,16 @@ namespace BTCPayServer.Tests
|
||||
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
// Oops!
|
||||
Assert.Equal("The request has been approved. The sender needs to send the payment manually. (Or activate the lightning automated payment processor)", response.Reason);
|
||||
var account = await s.AsTestAccount().CreateClient();
|
||||
await account.UpdateStoreLightningAutomatedPayoutProcessors(s.StoreId, "BTC-LN", new()
|
||||
{
|
||||
ProcessNewPayoutsInstantly = true,
|
||||
IntervalSeconds = TimeSpan.FromSeconds(60)
|
||||
});
|
||||
// Now it should process to complete
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains(bolt2.BOLT11, s.Driver.PageSource);
|
||||
@ -2577,7 +2586,9 @@ namespace BTCPayServer.Tests
|
||||
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
|
||||
TestUtils.Eventually(() =>
|
||||
// Nope, you need to approve the claim automatically
|
||||
Assert.Equal("The request has been recorded, but still need to be approved before execution.", response.Reason);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains(bolt2.BOLT11, s.Driver.PageSource);
|
||||
@ -3264,6 +3275,7 @@ namespace BTCPayServer.Tests
|
||||
public async Task CanUseLNAddress()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.DeleteStore = false;
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
@ -3416,7 +3428,13 @@ namespace BTCPayServer.Tests
|
||||
var succ = JsonConvert.DeserializeObject<LNURLPayRequest.LNURLPayRequestCallbackResponse>(str);
|
||||
Assert.NotNull(succ.Pr);
|
||||
Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
|
||||
await s.Server.CustomerLightningD.Pay(succ.Pr);
|
||||
}
|
||||
|
||||
// Can we find our comment and address in the payment list?
|
||||
s.GoToInvoices();
|
||||
var source = s.Driver.PageSource;
|
||||
Assert.Contains(lnUsername, source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -3765,7 +3783,7 @@ retry:
|
||||
(_, string posId) = s.CreateApp("PointOfSale");
|
||||
(_, string crowdfundId) = s.CreateApp("Crowdfund");
|
||||
|
||||
string GetStorePath(string subPath) => $"/stores/{storeId}/{subPath}";
|
||||
string GetStorePath(string subPath) => $"/stores/{storeId}" + (string.IsNullOrEmpty(subPath) ? "" : $"/{subPath}");
|
||||
|
||||
// Owner access
|
||||
s.AssertPageAccess(true, GetStorePath(""));
|
||||
|
@ -609,9 +609,7 @@ retry:
|
||||
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
|
||||
var method = methods.First(m => m.PaymentMethodId == $"{cryptoCode}-LN");
|
||||
var bolt11 = method.Destination;
|
||||
TestLogs.LogInformation("PAYING");
|
||||
await parent.CustomerLightningD.Pay(bolt11);
|
||||
TestLogs.LogInformation("PAID");
|
||||
await WaitInvoicePaid(invoiceId);
|
||||
}
|
||||
|
||||
|
@ -727,5 +727,39 @@
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "payment",
|
||||
"input": {
|
||||
"output": null,
|
||||
"version": 1,
|
||||
"outpoint": null,
|
||||
"accounted": true,
|
||||
"cryptoCode": "XMR",
|
||||
"networkFee": 0.0000000019,
|
||||
"receivedTimeMs": 1705500405468,
|
||||
"cryptoPaymentData": "{\"Amount\":62700000000,\"Address\":\"85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t\",\"SubaddressIndex\":23,\"SubaccountIndex\":0,\"BlockHeight\":3063946,\"ConfirmationCount\":10,\"TransactionId\":\"cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06\",\"LockTime\":0}",
|
||||
"cryptoPaymentDataType": "MoneroLike"
|
||||
},
|
||||
"expected": {
|
||||
"divisibility": 12,
|
||||
"destination": "85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t",
|
||||
"details": {
|
||||
"blockHeight": 3063946,
|
||||
"confirmationCount": 10,
|
||||
"lockTime": 0,
|
||||
"subaccountIndex": 0,
|
||||
"subaddressIndex": 23,
|
||||
"transactionId": "cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06"
|
||||
},
|
||||
"version": 2
|
||||
},
|
||||
"expectedProperties": {
|
||||
"Amount": "0.0627",
|
||||
"PaymentMethodId": "XMR-CHAIN",
|
||||
"Currency": "XMR",
|
||||
"Status": "Settled",
|
||||
"Accounted": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -1,5 +1,5 @@
|
||||
Id,Blob,Created,ExceptionStatus,Status,StoreDataId,Archived,Blob2
|
||||
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Q7RqoHLngK9svM4MgRyi9y,\x1f8b080000000000000ac454cb76da400cfd97595302b1c1981d6012124c0e8f104e12b2f043b627d833663c767073f2655df493fa0bd50049714fbae9262b1f4ba3ab2be94abf7efc7c25d4275d3233e65b3eb2593836b362a24fc27949cd92d44826b9802bf544cbc2e5602097fdb0bde8d8d71be1fa416fe566d7936dbfbcb08362b17dbe6cefc6fdd5d0c7402e7c102a90e5718c3829803fe531f54ad26dd4887024906ebb65b6eb9a6e187adbd0b4965e2394159c7a704b1374375b5a4733b596765e23b04b290651ce4e7dbaae7c3ea43ca3b2e7fb02b24c71ed9b634b338ca56d19c3fb6075afcd4661b0ea5ce8fd17695ba14d91612ab89f7bf28a055c247b64d2c57648482cc83c44710104be539601f7e1bd94342a33ea3931e9064e9c810242c6a46bd4b12e2f1702982a922c171679ab11372fb111d5247bdb8d937c60ee0dc3c4a171c532e03993a2acd81e685af95fc87d2b4fa3a8ac861c1b73fe99b159314e23ce8e60481dbb6a39d2797f8103a5c5be868554f30b5595bb3477db56cf0da3209f0f56edd95668e360613dd38dbe192e69d1db049177399a6cee86a0bb0fdb967637fb6e981b5d1ff2596b62a5f6f578dc9b8e5bc5dd5d7bba3396cdb966c28cbacc8dac6c65e6c35973ec47e6c0729370feed313dcfa227f2091b0a6af4af6bd2bf1dac4977fd45d4d6e46dbf378ecc151f062ff80b3b0fd203d783fd2825a74c8049fc7f7cc29d802067bee3c6f021ad836972a20b013e15e0c9e5dc46f448ca34eb9e9d254e264130542926de5016d63d9e605eb9bb00b52c0d946680f1375cd200c5aba8605629724cc24e8c7bd8e3ca8228402c455cc943190359f721e175577a58c0e1599d8b10f379a24c253f88e6550d427dfeb5ebc7ea2720238e87e215151700ccf7af9b55eeffb3e108677f3103c44b38a378437124c38f0bf67ebd3a0d75bd22aa8eacba284770a5e3c3089c0227af0471f48c9c2cfae3859d04e683ffd7508fd281e2a0ac8ad26e790cc261ea5c35eb8db7df000000ffff0300d1083b8e01060000,2018-10-01 11:32:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
|
|
@ -9,3 +9,4 @@ dc130d025a4bd2e7ee83b7707d850e7c1a52872c222a5bc2e05d3357e79aa762-1,\x1f8b0800000
|
||||
afc39884e024cbb3a48ca997b7e878b199d85fe97f90f94471364cc866ba83ad-1,\x1f8b080000000000000374904d6ac3301085f73945d03a14c992f593655cb20aa5942cbd1949a362d258c191534cc8c9bae8917a854a2e0ddd7476ef3de6d3e87d7d7c5e17cb25b9e070ee624fd64bb62a7a4087dd05fdbe3b6231eb9ad5a21246ce698fe93d0e872d968c3ed032d59cc4319d62d7a7ec13f09a5b90d269e1b8544c88600c55014d1dbc36c632a5515b651418ed4058ee6c25906a91258740d90ca6e44e1e672eb5402b47efc3f23e13351a270cc7bc2aa407164ce0ca04e1a9ac38b34885e65c4aabd50f0f9c8b639fd067641a469c4d374ca7149be8cbc7c866df903ff6334c47ecd3232428e9b5254dec43371c21e5ea9a426bc95ae59e562d79d96cb398c12d79baf795bddc57b676f80a6eca32c0db196fffbcb39f4ebf97ecba0392c5ed1b0000ffff030018cc54a1b1010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
6f2b8513ebfdb14b41d4de235f050cd028400cb01fde700d72c6bedc2ad8af1e-5,\x1f8b08000000000000037490bf6ac3301087f73c45d01cc249966425635c3285528a472ffa732a26b5151c39c5843c59873e525fa192434397de22ddef83efa4fbfefcba2e964b72c1e1dc869e6c977495fb012db6177475db610e85908a6d24c04c7b8c1f6138ee313358432e369330c65368fb987242517ba79876d6a0952503578243ea0d80e5c0944b2708e159e1d0714eb931d43b34b4508219e941cc62200ff378f752c719c2a368a937942b445158855e6967b8b05c0a2e9934c0b59585960c0b55d272a3cabb4f5b1bc63ea24b4aafdfcf38a776984e3154c1e59f915d5d913ff18b9e3aece3938e3ad36b43aad0fb76e8744cbbabb2ae215b5835e475b74fb7388c989ae7c7b6325d677ec0376da7d4ce936fff0ca9a7d3ef330eed11c9e2f6030000ffff0300b09cf40daf010000,Q7RqoHLngK9svM4MgRyi9y,f,,
|
||||
3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61,\x1f8b08000000000000037490bd6ec3201485f73c45c41c5580f973c6b8ca14555595d10be65e222b8d891c9cca8af2641dfa487d8582ab585d7a27ce39f001e7fbf3ebb6582ec915fb4b1b3ab25eb255d63d3a6caf08fbf684d994525121a85653da61fc08fd718b39a34f340f9f9230c47368bb987c22256fb4f09e7bc79d914a000aea8c6bd083c1461bcfd2524241593a5f3a250aef8d2991c952a9c21630812999c9c3c4d55615cad37998b625131eb8e5c080496c941525958c97b2708c838286022ac5b56060f42fcf3a17862e222464ec079c4cd78fe718aa00f96364b3afc81ffbd58e27ece2b38d36a7b79a54a1f36d7fb2315557655a4dd64c30b1aac9db669b84b7ef174cea652e2c99a9b064edf060ddf8d873ffe7a2fd787e3c65d71e912cee3f000000ffff0300a991d8c4b2010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
26c879f3d27a894a62f8730c84205ac9dec38b7bbc0a11ccc0c196d1259b25aa-1,\x1f8b08000000000000036c90cf6ed4301087dfc5e7086c27f19fdccaa2b0b4dbaa452b964ab94cec314d378943e204d26a9f8c038fc42be02eea01099f3c9faddfcc7cbf7ffe7a260b8e53e37b52b0848c68b059d0ee9b0eafa78804cfa9d69aa954c884f418befbf158229282bea109f173187cd3075210009eeb9ae796696198a1c63046a136b256b5492d1a9d03a72aa346a64e099e81d2122c77a9d4467141193d1ff237758e99fddcb6090163fcdc07b4a408e38c0931e33a04bff1364e4176eff61bf2ca6e61edb00fef21407c7aae88f1bd6bc60e425c70f3925291422415196b176f0eda096305d68e384d9154e4f3e5ae94878b61f1eeca680fdf862ffb3be5ecc5aacb9b0f87fce89e9aedfd63d64cf869fbb46c1fe67ab854dd74d5fd58efef4ad61c9c6bacce1eafa1bbc1beac486c70c4f516c2c3b9017d1b3da567bc403b6384715098260c1fedf98770944b8d1a2dcd585427a347ed7269a9a15c73953191096ab25a720e9866b904c6104daa98c9b9b0ff647325b253ac5bfc0a667d5dfaf43f65fb7578311a85ee9a2392d31f000000ffff03004a65a9b61e020000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
|
|
@ -345,7 +345,7 @@ retry:
|
||||
var fetcher = new RateFetcher(factory);
|
||||
|
||||
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
|
||||
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
|
||||
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD" })
|
||||
{
|
||||
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default).GetAwaiter().GetResult();
|
||||
Assert.NotNull(result.BidAsk);
|
||||
@ -488,10 +488,14 @@ retry:
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@{version}/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
// This test is flaky probably because of the CDN sending the wrong file's version in some regions.
|
||||
// https://app.circleci.com/pipelines/github/btcpayserver/btcpayserver/13750/workflows/44aaf31d-0057-4fd8-a5bb-1a2c47fc530f/jobs/42963
|
||||
// It works locally depending on where you live.
|
||||
|
||||
//actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
//version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
//expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
|
||||
//EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
|
||||
|
@ -648,7 +648,7 @@ namespace BTCPayServer.Tests
|
||||
var store2 = acc.GetController<UIStoresController>();
|
||||
await store2.Pair(pairingCode.ToString(), store2.CurrentStore.Id);
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey),
|
||||
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
|
||||
store2.TempData[WellKnownTempData.ErrorMessage].ToString(), StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout * 2)]
|
||||
@ -1587,14 +1587,14 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
var checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
|
||||
var checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<CheckoutModel>();
|
||||
Assert.Equal(lnMethod, checkout.PaymentMethodId);
|
||||
|
||||
// If we change store's default, it should change the checkout's default
|
||||
vm.DefaultPaymentMethod = btcMethod;
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
|
||||
.Result);
|
||||
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
|
||||
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<CheckoutModel>();
|
||||
Assert.Equal(btcMethod, checkout.PaymentMethodId);
|
||||
}
|
||||
|
||||
@ -1625,7 +1625,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// validate that invoice data model doesn't have lightning string initially
|
||||
var res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
|
||||
var paymentMethodFirst = Assert.IsType<PaymentModel>(
|
||||
var paymentMethodFirst = Assert.IsType<CheckoutModel>(
|
||||
Assert.IsType<ViewResult>(res).Model
|
||||
);
|
||||
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
|
||||
@ -1641,7 +1641,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// validate that QR code now has both onchain and offchain payment urls
|
||||
res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
|
||||
var paymentMethodUnified = Assert.IsType<PaymentModel>(
|
||||
var paymentMethodUnified = Assert.IsType<CheckoutModel>(
|
||||
Assert.IsType<ViewResult>(res).Model
|
||||
);
|
||||
Assert.StartsWith("bitcoin:bcrt", paymentMethodUnified.InvoiceBitcoinUrl);
|
||||
@ -1655,8 +1655,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Standard for all uppercase characters in QR codes is still not implemented in all wallets
|
||||
// But we're proceeding with BECH32 being uppercase
|
||||
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
|
||||
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
|
||||
Assert.Equal($"bitcoin:{paymentMethodUnified.Address}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
|
||||
Assert.Equal($"bitcoin:{paymentMethodUnified.Address.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
|
||||
|
||||
// Fallback lightning invoice should be uppercase inside the QR code, lowercase in payment URI
|
||||
var lightningFallback = paymentMethodUnified.InvoiceBitcoinUrl.Split(new[] { "&lightning=" }, StringSplitOptions.None)[1];
|
||||
@ -2909,6 +2909,11 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
Assert.True(await invoiceMigrator.IsComplete());
|
||||
});
|
||||
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var invoice = await invoiceRepo.GetInvoice("Q7RqoHLngK9svM4MgRyi9y");
|
||||
var p = invoice.Payments.First(p => p.Id == "26c879f3d27a894a62f8730c84205ac9dec38b7bbc0a11ccc0c196d1259b25aa-1");
|
||||
var details = p.GetDetails<BitcoinLikePaymentData>(handlers.GetBitcoinHandler("BTC"));
|
||||
Assert.Equal("6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d", details.AssetId.ToString());
|
||||
}
|
||||
|
||||
private static async Task RestartMigration(ServerTester tester)
|
||||
@ -3245,8 +3250,14 @@ namespace BTCPayServer.Tests
|
||||
var date2018 = new DateTimeOffset(2018, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
report = await GetReport(acc, new() { ViewName = "Payments", TimePeriod = new TimePeriod() { From = date2018, To = date2018 + TimeSpan.FromDays(365) } });
|
||||
var invoiceIdIndex = report.GetIndex("InvoiceId");
|
||||
var invoiceCurrencyAmountIndex = report.GetIndex("InvoiceCurrencyAmount");
|
||||
var rateIndex = report.GetIndex("Rate");
|
||||
var oldPaymentsCount = report.Data.Count(d => d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y");
|
||||
Assert.Equal(8, oldPaymentsCount); // 10 payments, but 2 unaccounted
|
||||
Assert.Equal(9, oldPaymentsCount); // 11 payments, but 2 unaccounted
|
||||
Assert.Single(report.Data, d =>
|
||||
d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y" &&
|
||||
GetAmount(rateIndex, d) == 6596.35m &&
|
||||
GetAmount(invoiceCurrencyAmountIndex, d) == 1.18m);
|
||||
|
||||
var addr = await tester.ExplorerNode.GetNewAddressAsync();
|
||||
// Two invoices get refunded
|
||||
@ -3268,9 +3279,9 @@ namespace BTCPayServer.Tests
|
||||
var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id));
|
||||
Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]);
|
||||
Assert.Equal(currency, d[currencyIndex].Value<string>());
|
||||
Assert.Equal(completed, (((JObject)d[completedIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(awaiting, (((JObject)d[awaitingIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(limit, (((JObject)d[limitIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(completed, GetAmount(completedIndex, d));
|
||||
Assert.Equal(awaiting, GetAmount(awaitingIndex, d));
|
||||
Assert.Equal(limit, GetAmount(limitIndex, d));
|
||||
}
|
||||
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
@ -3291,6 +3302,12 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private static decimal GetAmount(int idx, JArray d)
|
||||
{
|
||||
var jobj = (JObject)d[idx];
|
||||
return Math.Round(jobj["v"].Value<decimal>(), jobj["d"].Value<int>());
|
||||
}
|
||||
|
||||
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
|
||||
{
|
||||
var controller = acc.GetController<UIReportsController>();
|
||||
|
@ -16,11 +16,13 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using ExchangeSharp;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Localization;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -349,6 +351,8 @@ retry:
|
||||
{
|
||||
defaultTranslatedKeys.Add(k);
|
||||
}
|
||||
|
||||
AddLocalizers(defaultTranslatedKeys, txt);
|
||||
}
|
||||
|
||||
// Go through all cshtml file, search for text-translate or ViewLocalizer usage
|
||||
@ -356,32 +360,24 @@ retry:
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var engine = tester.PayTester.GetService<RazorProjectEngine>();
|
||||
foreach (var file in soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories))
|
||||
var files = soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories)
|
||||
.Union(soldir.EnumerateFiles("*.razor", SearchOption.AllDirectories));
|
||||
foreach (var file in files)
|
||||
{
|
||||
var filePath = file.FullName;
|
||||
var txt = File.ReadAllText(file.FullName);
|
||||
foreach (string localizer in new[] { "ViewLocalizer", "StringLocalizer" })
|
||||
{
|
||||
if (txt.Contains(localizer))
|
||||
{
|
||||
var matches = Regex.Matches(txt, localizer + "\\[\"(.*?)\"[\\],]");
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
defaultTranslatedKeys.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
AddLocalizers(defaultTranslatedKeys, txt);
|
||||
|
||||
filePath = filePath.Replace(Path.Combine(soldir.FullName, "BTCPayServer"), "/");
|
||||
var item = engine.FileSystem.GetItem(filePath);
|
||||
|
||||
|
||||
var node = (DocumentIntermediateNode)engine.Process(item).Items[typeof(DocumentIntermediateNode)];
|
||||
var w = new TranslatedKeyNodeWalker(defaultTranslatedKeys, txt);
|
||||
w.Visit(node);
|
||||
}
|
||||
|
||||
}
|
||||
defaultTranslatedKeys = defaultTranslatedKeys.Select(d => d.Trim()).Distinct().OrderBy(o => o).ToList();
|
||||
defaultTranslatedKeys = defaultTranslatedKeys.Select(d => d.Trim().Replace("\r\n", "\n")).Distinct().OrderBy(o => o).ToList();
|
||||
JObject obj = new JObject();
|
||||
foreach (var v in defaultTranslatedKeys)
|
||||
{
|
||||
@ -397,6 +393,24 @@ retry:
|
||||
content += defaultTranslation.Substring(endIdx);
|
||||
File.WriteAllText(path, content);
|
||||
}
|
||||
|
||||
private static void AddLocalizers(List<string> defaultTranslatedKeys, string txt)
|
||||
{
|
||||
foreach (string localizer in new[] { "ViewLocalizer", "StringLocalizer" })
|
||||
{
|
||||
if (txt.Contains(localizer))
|
||||
{
|
||||
var matches = Regex.Matches(txt, localizer + "\\[\"(.*?)\"[\\],]");
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var k = match.Groups[1].Value;
|
||||
k = k.Replace("\\", "");
|
||||
defaultTranslatedKeys.Add(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DisplayNameWalker : CSharpSyntaxWalker
|
||||
{
|
||||
public List<string> Keys = new List<string>();
|
||||
|
@ -98,7 +98,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.4
|
||||
image: nicolasdorier/nbxplorer:2.5.12
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -162,7 +162,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v24.05
|
||||
image: btcpayserver/lightning:v24.08.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -190,7 +190,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v24.05
|
||||
image: btcpayserver/lightning:v24.08.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -227,7 +227,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
image: btcpayserver/lnd:v0.18.3-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -262,7 +262,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
image: btcpayserver/lnd:v0.18.3-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -95,7 +95,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.4
|
||||
image: nicolasdorier/nbxplorer:2.5.12
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -148,7 +148,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v24.05
|
||||
image: btcpayserver/lightning:v24.08.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -176,7 +176,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v24.05
|
||||
image: btcpayserver/lightning:v24.08.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -213,7 +213,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
image: btcpayserver/lnd:v0.18.3-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -250,7 +250,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
image: btcpayserver/lnd:v0.18.3-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -37,11 +37,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.23" />
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.24" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.0" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.7" />
|
||||
<PackageReference Include="CsvHelper" Version="32.0.3" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
|
@ -4,10 +4,12 @@
|
||||
@using BTCPayServer.Services.Notifications;
|
||||
@using Microsoft.AspNetCore.Identity;
|
||||
@using Microsoft.AspNetCore.Routing;
|
||||
@using Microsoft.Extensions.Localization
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider _AuthenticationStateProvider
|
||||
@inject NotificationManager _NotificationManager
|
||||
@inject UserManager<ApplicationUser> _UserManager
|
||||
@inject IStringLocalizer StringLocalizer
|
||||
@inject IJSRuntime _JSRuntime
|
||||
@inject LinkGenerator _LinkGenerator
|
||||
@inject BTCPayServerOptions _BTCPayServerOptions
|
||||
@ -16,13 +18,13 @@
|
||||
<div id="Notifications">
|
||||
@if (UnseenCount == "0")
|
||||
{
|
||||
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
|
||||
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="@StringLocalizer["Notifications"]">
|
||||
<Icon Symbol="nav-notifications" />
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
|
||||
<button id="NotificationsHandle" class="mainMenuButton" title="@StringLocalizer["Notifications"]" type="button" data-bs-toggle="dropdown">
|
||||
<Icon Symbol="nav-notifications" />
|
||||
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
|
||||
</button>
|
||||
@ -31,8 +33,8 @@
|
||||
{
|
||||
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
||||
<h5 class="m-0">Notifications</h5>
|
||||
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen">Mark all as seen</a>
|
||||
<h5 class="m-0" text-translate="true">Notifications</h5>
|
||||
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen" text-translate="true">Mark all as seen</a>
|
||||
</div>
|
||||
<div id="NotificationsList" v-pre>
|
||||
@foreach (var n in Last5)
|
||||
@ -54,7 +56,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<a href="@NotificationsUrl">View all</a>
|
||||
<a href="@NotificationsUrl" text-translate="true">View all</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
@using Microsoft.AspNetCore.Http
|
||||
|
||||
@inject IHttpContextAccessor HttpContextAccessor;
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
|
||||
@if (Users?.Any() is true)
|
||||
{
|
||||
<div @attributes="Attrs" class="@CssClass">
|
||||
<label for="SignedInUser" class="form-label">Signed in user</label>
|
||||
<label for="SignedInUser" class="form-label" text-translate="true">Signed in user</label>
|
||||
<select id="SignedInUser" class="form-select" value="@_userId" @onchange="@(e => _userId = e.Value?.ToString())">
|
||||
<option value="">None, just open the URL</option>
|
||||
<option value="" text-translate="true">None, just open the URL</option>
|
||||
@foreach (var u in Users)
|
||||
{
|
||||
<option value="@u.Key">@u.Value</option>
|
||||
|
@ -5,11 +5,13 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Microsoft.AspNetCore.Mvc
|
||||
@using Microsoft.AspNetCore.Routing
|
||||
@using Microsoft.Extensions.Localization
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject UserManager<ApplicationUser> UserManager;
|
||||
@inject UserLoginCodeService UserLoginCodeService;
|
||||
@inject LinkGenerator LinkGenerator;
|
||||
@inject IHttpContextAccessor HttpContextAccessor;
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
@inject UserLoginCodeService UserLoginCodeService
|
||||
@inject LinkGenerator LinkGenerator
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
@inject IStringLocalizer StringLocalizer
|
||||
@implements IDisposable
|
||||
|
||||
@if (!string.IsNullOrEmpty(_data))
|
||||
@ -18,7 +20,7 @@
|
||||
<div class="qr-container mb-2">
|
||||
<QrCode Data="@_data" Size="Size"/>
|
||||
</div>
|
||||
<p class="text-center text-muted mb-1" id="progress">Valid for @_seconds seconds</p>
|
||||
<p class="text-center text-muted mb-1" id="progress">@StringLocalizer["Valid for {0} seconds", _seconds]</p>
|
||||
<div class="progress only-for-js" data-bs-toggle="tooltip" data-bs-placement="top">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated @(Percent < 15 ? "bg-warning" : null)" role="progressbar" style="width:@Percent%" id="progressbar"></div>
|
||||
</div>
|
||||
|
@ -110,5 +110,10 @@ namespace BTCPayServer
|
||||
{
|
||||
return ColorTranslator.FromHtml(html);
|
||||
}
|
||||
|
||||
public string ToHtml(Color color)
|
||||
{
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,5 @@
|
||||
using System;
|
||||
using System.Security.AccessControl;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
|
@ -2,23 +2,30 @@
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Plugins.Crowdfund
|
||||
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
||||
@{
|
||||
var label = Model.AppType == CrowdfundAppType.AppType ? "Contributions" : "Sales";
|
||||
}
|
||||
|
||||
<div id="AppSales-@Model.Id" class="widget app-sales">
|
||||
<header class="mb-3">
|
||||
<h3>@Model.Name @label</h3>
|
||||
<h3>
|
||||
@Model.Name
|
||||
@if (Model.AppType == CrowdfundAppType.AppType)
|
||||
{
|
||||
<span text-translate="true">Contributions</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span text-translate="true">Sales</span>
|
||||
}
|
||||
</h3>
|
||||
@if (!string.IsNullOrEmpty(Model.AppUrl))
|
||||
{
|
||||
<a href="@Model.AppUrl">Manage</a>
|
||||
<a href="@Model.AppUrl" text-translate="true">Manage</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<span class="visually-hidden" text-translate="true">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/Components/AppSales/Default.cshtml.js" asp-append-version="true"></script>
|
||||
@ -39,7 +46,15 @@
|
||||
{
|
||||
<header class="mb-3">
|
||||
<span>
|
||||
<span class="sales-count">@Model.SalesCount</span> Total @label
|
||||
<span class="sales-count">@Model.SalesCount</span>
|
||||
@if (Model.AppType == CrowdfundAppType.AppType)
|
||||
{
|
||||
<span text-translate="true">Total Contributions</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span text-translate="true">Total Sales</span>
|
||||
}
|
||||
</span>
|
||||
<div class="btn-group only-for-js" role="group" aria-label="Filter">
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.Id" id="AppSalesPeriodWeek-@Model.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>
|
||||
|
@ -42,11 +42,19 @@ if (!window.appSales) {
|
||||
render(data, period);
|
||||
}
|
||||
};
|
||||
|
||||
function addEventListeners() {
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
}
|
||||
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
if (document.readyState === "loading") {
|
||||
window.addEventListener("DOMContentLoaded", addEventListeners);
|
||||
} else {
|
||||
addEventListeners();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,18 +1,24 @@
|
||||
@using BTCPayServer.Plugins.Crowdfund
|
||||
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
|
||||
@{
|
||||
var label = Model.AppType == CrowdfundAppType.AppType ? "contribution" : "sale";
|
||||
}
|
||||
|
||||
<div id="AppTopItems-@Model.Id" class="widget app-top-items">
|
||||
<header class="mb-3">
|
||||
<h3>Top @(Model.AppType == CrowdfundAppType.AppType ? "Perks" : "Items")</h3>
|
||||
<h3>
|
||||
@if (Model.AppType == CrowdfundAppType.AppType)
|
||||
{
|
||||
<span text-translate="true">Top Perks</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span text-translate="true">Top Items</span>
|
||||
}
|
||||
</h3>
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<span class="visually-hidden" text-translate="true">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/Components/AppTopItems/Default.cshtml.js" asp-append-version="true"></script>
|
||||
@ -45,7 +51,31 @@
|
||||
@entry.Title
|
||||
</span>
|
||||
<span class="app-item-value" data-sensitive>
|
||||
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
|
||||
<span class="text-muted">
|
||||
@entry.SalesCount
|
||||
@if (Model.AppType == CrowdfundAppType.AppType)
|
||||
{
|
||||
if (entry.SalesCount == 1)
|
||||
{
|
||||
<span text-translate="true">contribution</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span text-translate="true">contributions</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry.SalesCount == 1)
|
||||
{
|
||||
<span text-translate="true">sale</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span text-translate="true">sales</span>
|
||||
}
|
||||
},
|
||||
</span>
|
||||
@entry.TotalFormatted
|
||||
</span>
|
||||
</div>
|
||||
@ -55,7 +85,14 @@
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
No @($"{label}s") have been made yet.
|
||||
@if (Model.AppType == CrowdfundAppType.AppType)
|
||||
{
|
||||
<span text-translate="true">No contributions have been made yet.</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span text-translate="true">No sales have been made yet.</span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
@using BTCPayServer.Payments
|
||||
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
|
||||
@inject Dictionary<PaymentMethodId, IPaymentModelExtension> Extensions
|
||||
@inject Dictionary<PaymentMethodId, ICheckoutModelExtension> Extensions
|
||||
|
||||
@{
|
||||
var state = Model.State.ToString();
|
||||
@ -10,7 +10,7 @@
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
@if (Model.IsArchived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
<span class="badge bg-warning" text-translate="true">archived</span>
|
||||
}
|
||||
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
|
||||
@if (canMark)
|
||||
@ -21,13 +21,13 @@
|
||||
<div class="dropdown-menu">
|
||||
@if (Model.State.CanMarkInvalid())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid">
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid" text-translate="true">
|
||||
Mark as invalid
|
||||
</button>
|
||||
}
|
||||
@if (Model.State.CanMarkComplete())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled">
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled" text-translate="true">
|
||||
Mark as settled
|
||||
</button>
|
||||
}
|
||||
@ -62,6 +62,6 @@
|
||||
}
|
||||
@if (Model.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">Refund</span>
|
||||
<span class="badge bg-warning" text-translate="true">Refund</span>
|
||||
}
|
||||
</div>
|
||||
|
@ -11,7 +11,7 @@
|
||||
walletId = Model.WalletObjectId.WalletId
|
||||
}): string.Empty;
|
||||
}
|
||||
<input id="@elementId" placeholder="Select labels" autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
|
||||
<input id="@elementId" placeholder=@StringLocalizer["Select labels"] autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
|
||||
class="only-for-js form-control label-manager ts-wrapper @(Model.DisplayInline ? "ts-inline" : "")"
|
||||
data-fetch-url="@fetchUrl"
|
||||
data-update-url="@updateUrl"
|
||||
|
@ -45,31 +45,31 @@
|
||||
</li>
|
||||
@if (ViewData.IsPageActive([StoreNavPages.General, StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Roles, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails, StoreNavPages.Forms]))
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Model.Store.Id" text-translate="true">Rates</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@Model.Store.Id" text-translate="true">Checkout Appearance</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@Model.Store.Id" text-translate="true">Access Tokens</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Users)" asp-controller="UIStores" asp-action="StoreUsers" asp-route-storeId="@Model.Store.Id" text-translate="true">Users</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Roles))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Roles)" asp-controller="UIStores" asp-action="ListRoles" asp-route-storeId="@Model.Store.Id" text-translate="true">Roles</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@Model.Store.Id" text-translate="true">Webhooks</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.PayoutProcessors))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.PayoutProcessors)" asp-controller="UIPayoutProcessors" asp-action="ConfigureStorePayoutProcessors" asp-route-storeId="@Model.Store.Id" text-translate="true">Payout Processors</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Emails))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Emails)" asp-controller="UIStores" asp-action="StoreEmailSettings" asp-route-storeId="@Model.Store.Id" text-translate="true">Emails</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Forms))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Forms)" asp-controller="UIForms" asp-action="FormsList" asp-route-storeId="@Model.Store.Id" text-translate="true">Forms</a>
|
||||
</li>
|
||||
}
|
||||
@ -106,14 +106,19 @@
|
||||
</li>
|
||||
@if (ViewData.IsCategoryActive(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsPageActive([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsPageActive([StoreNavPages.OnchainSettings], categoryId))
|
||||
{
|
||||
@if (!scheme.ReadonlyWallet)
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId" text-translate="true">Send</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId">Send</a>
|
||||
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId" text-translate="true">Receive</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId">Receive</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Settings" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Settings, scheme.WalletId.ToString()) @ViewData.ActivePageClass(StoreNavPages.OnchainSettings, categoryId)" asp-area="" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.WalletId.CryptoCode" asp-route-storeId="@scheme.WalletId.StoreId">Settings</a>
|
||||
<a id="WalletNav-Settings" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Settings, scheme.WalletId.ToString()) @ViewData.ActivePageClass(StoreNavPages.OnchainSettings, categoryId)" asp-area="" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.WalletId.CryptoCode" asp-route-storeId="@scheme.WalletId.StoreId" text-translate="true">Settings</a>
|
||||
</li>
|
||||
<vc:ui-extension-point location="wallet-nav" model="@Model" />
|
||||
}
|
||||
@ -143,7 +148,7 @@
|
||||
@if (ViewData.IsPageActive([StoreNavPages.Lightning, StoreNavPages.LightningSettings], $"{Model.Store.Id}-{scheme.CryptoCode}"))
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode">Settings</a>
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode" text-translate="true">Settings</a>
|
||||
</li>
|
||||
<vc:ui-extension-point location="lightning-nav" model="@Model"/>
|
||||
}
|
||||
@ -322,8 +327,9 @@
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Files" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Files)" asp-action="Files" text-translate="true">Files</a>
|
||||
</li>
|
||||
|
||||
<vc:ui-extension-point location="server-nav" model="@Model"/>
|
||||
}
|
||||
<vc:ui-extension-point location="server-nav" model="@Model"/>
|
||||
<li class="nav-item dropup">
|
||||
<a class="nav-link @ViewData.ActivePageClass(ManageNavPages.Index)" role="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" id="Nav-Account">
|
||||
<vc:icon symbol="nav-account"/>
|
||||
|
@ -1,3 +1,5 @@
|
||||
@using System.Web
|
||||
@using BTCPayServer.TagHelpers
|
||||
@model BasePagingViewModel
|
||||
|
||||
@{
|
||||
@ -13,7 +15,7 @@
|
||||
@if (Model.Skip > 0)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">Prev</a>
|
||||
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)" text-translate="true">Prev</a>
|
||||
</li>
|
||||
}
|
||||
<li class="page-item disabled">
|
||||
@ -35,7 +37,7 @@
|
||||
@if ((Model.Total is null && Model.CurrentPageCount >= Model.Count) || (Model.Total is not null && Model.Total.Value > Model.Skip + Model.Count))
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="@NavigatePages(1, Model.Count)">Next</a>
|
||||
<a class="page-link" href="@NavigatePages(1, Model.Count)" text-translate="true">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@ -45,7 +47,7 @@
|
||||
{
|
||||
<ul class="pagination ms-auto">
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page Size</span>
|
||||
<span class="page-link" text-translate="true">Page Size</span>
|
||||
</li>
|
||||
@foreach (var pageSize in pageSizeOptions)
|
||||
{
|
||||
@ -85,10 +87,26 @@
|
||||
{
|
||||
// merge both, preferring the `query` properties in case of duplicate keys
|
||||
query = query.Concat(Model.PaginationQuery)
|
||||
.Where(e => e.Value != null)
|
||||
.GroupBy(e => e.Key)
|
||||
.ToDictionary(g => g.Key, g => g.First().Value);
|
||||
}
|
||||
|
||||
return Url.Action(null, query);
|
||||
return ReplaceQueryParameters(query);
|
||||
}
|
||||
|
||||
string ReplaceQueryParameters(Dictionary<string, object> query)
|
||||
{
|
||||
var uri = new Uri(ViewContext.HttpContext.Request.GetCurrentUrlWithQueryString());
|
||||
var queryParams = HttpUtility.ParseQueryString(uri.Query);
|
||||
foreach (var (key, value) in query)
|
||||
{
|
||||
if (value != null) queryParams[key] = value?.ToString();
|
||||
}
|
||||
var uriBuilder = new UriBuilder(uri)
|
||||
{
|
||||
Query = queryParams.ToString()!
|
||||
};
|
||||
return uriBuilder.ToString();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -1,11 +1,15 @@
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
|
||||
@if(!Model.InitialRendering && Model.Balance == null)
|
||||
@if (!Model.InitialRendering && Model.Balance == null)
|
||||
{
|
||||
return;
|
||||
return;
|
||||
}
|
||||
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
|
||||
<div id="StoreLightningBalance-@Model.StoreId" class="widget store-lightning-balance">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||
<h6>Lightning Balance</h6>
|
||||
<h6 text-translate="true">Lightning Balance</h6>
|
||||
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
|
||||
{
|
||||
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
|
||||
@ -29,7 +33,7 @@
|
||||
<div class="d-flex align-items-baseline gap-1">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> in channels
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> in channels", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -41,7 +45,7 @@
|
||||
@Model.Balance.OffchainBalance.Opening
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> opening channels
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> opening channels", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@ -52,7 +56,7 @@
|
||||
@Model.Balance.OffchainBalance.Local
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> local balance
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> local balance", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@ -63,7 +67,7 @@
|
||||
@Model.Balance.OffchainBalance.Remote
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> remote balance
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> remote balance", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@ -74,7 +78,7 @@
|
||||
@Model.Balance.OffchainBalance.Closing
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> closing channels
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> closing channels", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@ -87,7 +91,7 @@
|
||||
<div class="d-flex align-items-baseline gap-1">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> on-chain
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> on-chain", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
<div class="balance-details collapse" id="balanceDetailsOnchain">
|
||||
@ -98,7 +102,7 @@
|
||||
@Model.Balance.OnchainBalance.Confirmed
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> confirmed
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> confirmed", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@ -109,7 +113,7 @@
|
||||
@Model.Balance.OnchainBalance.Unconfirmed
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> unconfirmed
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> unconfirmed", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@ -120,7 +124,7 @@
|
||||
@Model.Balance.OnchainBalance.Reserved
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> reserved
|
||||
@ViewLocalizer["<span class=\"currency\">{0}</span> reserved", Model.CryptoCode]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@ -128,62 +132,53 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 @(Model.Series != null ? "my-3" : "mt-3")">
|
||||
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
|
||||
{
|
||||
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
|
||||
<vc:icon symbol="caret-down" />
|
||||
<span class="ms-1" text-translate="true">Details</span>
|
||||
</button>
|
||||
}
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
|
||||
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodWeek-@Model.StoreId" value="@HistogramType.Week" @(Model.Type == HistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreLightningBalancePeriodWeek-@Model.StoreId">1W</label>
|
||||
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodMonth-@Model.StoreId" value="@HistogramType.Month" @(Model.Type == HistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreLightningBalancePeriodMonth-@Model.StoreId">1M</label>
|
||||
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodYear-@Model.StoreId" value="@HistogramType.Year" @(Model.Type == HistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreLightningBalancePeriodYear-@Model.StoreId">1Y</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 mt-3 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
|
||||
<vc:icon symbol="caret-down"/>
|
||||
<span class="ms-1">Details</span>
|
||||
</button>
|
||||
<div class="ct-chart"></div>
|
||||
<template>
|
||||
@Safe.Json(Model)
|
||||
</template>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<span class="visually-hidden" text-translate="true">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/Components/StoreLightningBalance/Default.cshtml.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("LightningBalance", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const url = @Safe.Json(Model.DataUrl);
|
||||
const storeId = @Safe.Json(Model.StoreId);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreLightningBalance-${storeId}`).outerHTML = await response.text();
|
||||
const data = document.querySelector(`#StoreLightningBalance-${storeId} template`);
|
||||
if (data) window.storeLightningBalance.dataLoaded(JSON.parse(data.innerHTML));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
||||
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
||||
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
||||
const id = `StoreLightningBalance-${storeId}`;
|
||||
|
||||
const render = rate => {
|
||||
const currency = rate ? defaultCurrency : cryptoCode;
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
|
||||
: value
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
delegate('change', `#${id} .currency-toggle input`, async e => {
|
||||
const { target } = e;
|
||||
if (target.value === defaultCurrency) {
|
||||
const rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
|
||||
if (rate) render(rate);
|
||||
} else {
|
||||
render(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -0,0 +1,94 @@
|
||||
if (!window.storeLightningBalance) {
|
||||
window.storeLightningBalance = {
|
||||
dataLoaded (model) {
|
||||
const { storeId, cryptoCode, defaultCurrency, currencyData: { divisibility } } = model;
|
||||
const id = `StoreLightningBalance-${storeId}`;
|
||||
const valueTransform = value => rate ? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility) : value
|
||||
const labelCount = 6
|
||||
const tooltip = Chartist.plugins.tooltip2({
|
||||
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -16
|
||||
},
|
||||
valueTransformFunction(value, label) {
|
||||
return valueTransform(value) + ' ' + (rate ? defaultCurrency : cryptoCode)
|
||||
}
|
||||
})
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
const dateFormatter = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric' })
|
||||
const chartOpts = {
|
||||
fullWidth: true,
|
||||
showArea: true,
|
||||
axisY: {
|
||||
showLabel: false,
|
||||
offset: 0
|
||||
},
|
||||
plugins: [tooltip]
|
||||
};
|
||||
const baseUrl = model.dataUrl;
|
||||
let data = model;
|
||||
let rate = null;
|
||||
|
||||
const render = data => {
|
||||
let { series, labels } = data;
|
||||
const currency = rate ? defaultCurrency : cryptoCode;
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = valueTransform(value)
|
||||
});
|
||||
if (!series) return;
|
||||
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
const renderOpts = Object.assign({}, chartOpts, { low, axisX: {
|
||||
labelInterpolationFnc(date, i) {
|
||||
return i % labelEvery == 0 ? dateFormatter.format(new Date(date)) : null
|
||||
}
|
||||
} });
|
||||
const pointCount = series.length;
|
||||
const labelEvery = pointCount / labelCount;
|
||||
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels: labels,
|
||||
series: [series]
|
||||
}, renderOpts);
|
||||
};
|
||||
|
||||
const update = async type => {
|
||||
const url = `${baseUrl}/${type}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
render(data);
|
||||
}
|
||||
};
|
||||
|
||||
render(data);
|
||||
|
||||
function addEventListeners() {
|
||||
delegate('change', `#${id} [name="StoreLightningBalancePeriod-${storeId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
})
|
||||
delegate('change', `#${id} .currency-toggle input`, async e => {
|
||||
const { target } = e;
|
||||
if (target.value === defaultCurrency) {
|
||||
rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
|
||||
if (rate) render(data);
|
||||
} else {
|
||||
rate = null;
|
||||
render(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
window.addEventListener("DOMContentLoaded", addEventListeners);
|
||||
} else {
|
||||
addEventListeners();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
@ -18,11 +16,14 @@ using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreLightningBalance;
|
||||
|
||||
public class StoreLightningBalance : ViewComponent
|
||||
{
|
||||
private const HistogramType DefaultType = HistogramType.Week;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||
@ -32,6 +33,7 @@ public class StoreLightningBalance : ViewComponent
|
||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly LightningHistogramService _lnHistogramService;
|
||||
|
||||
public StoreLightningBalance(
|
||||
StoreRepository storeRepo,
|
||||
@ -42,7 +44,8 @@ public class StoreLightningBalance : ViewComponent
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions,
|
||||
IAuthorizationService authorizationService,
|
||||
PaymentMethodHandlerDictionary handlers)
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
LightningHistogramService lnHistogramService)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_currencies = currencies;
|
||||
@ -53,31 +56,32 @@ public class StoreLightningBalance : ViewComponent
|
||||
_handlers = handlers;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_lnHistogramService = lnHistogramService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreLightningBalanceViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
|
||||
{
|
||||
if (vm.Store == null)
|
||||
throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
|
||||
var vm = new StoreLightningBalanceViewModel
|
||||
{
|
||||
StoreId = store.Id,
|
||||
CryptoCode = cryptoCode,
|
||||
InitialRendering = initialRendering,
|
||||
DefaultCurrency = defaultCurrency,
|
||||
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
|
||||
DataUrl = Url.Action("LightningBalanceDashboard", "UIStores", new { storeId = store.Id, cryptoCode })
|
||||
};
|
||||
|
||||
vm.DefaultCurrency = vm.Store.GetStoreBlob().DefaultCurrency;
|
||||
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
try
|
||||
{
|
||||
var lightningClient = await GetLightningClient(vm.Store, vm.CryptoCode);
|
||||
if (lightningClient == null)
|
||||
{
|
||||
vm.InitialRendering = false;
|
||||
return View(vm);
|
||||
}
|
||||
var lightningClient = await GetLightningClient(store, vm.CryptoCode);
|
||||
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
var balance = await lightningClient.GetBalance();
|
||||
// balance
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var balance = await lightningClient.GetBalance(cts.Token);
|
||||
vm.Balance = balance;
|
||||
vm.TotalOnchain = balance.OnchainBalance != null
|
||||
? (balance.OnchainBalance.Confirmed ?? 0L) + (balance.OnchainBalance.Reserved ?? 0L) +
|
||||
@ -87,8 +91,16 @@ public class StoreLightningBalance : ViewComponent
|
||||
? (balance.OffchainBalance.Opening ?? 0) + (balance.OffchainBalance.Local ?? 0) +
|
||||
(balance.OffchainBalance.Closing ?? 0)
|
||||
: null;
|
||||
|
||||
// histogram
|
||||
var data = await _lnHistogramService.GetHistogram(lightningClient, DefaultType, cts.Token);
|
||||
if (data != null)
|
||||
{
|
||||
vm.Type = data.Type;
|
||||
vm.Series = data.Series;
|
||||
vm.Labels = data.Labels;
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex) when (ex is NotImplementedException or NotSupportedException)
|
||||
{
|
||||
// not all implementations support balance fetching
|
||||
@ -102,7 +114,7 @@ public class StoreLightningBalance : ViewComponent
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode )
|
||||
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
|
||||
|
@ -1,4 +1,6 @@
|
||||
using BTCPayServer.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
@ -7,13 +9,17 @@ namespace BTCPayServer.Components.StoreLightningBalance;
|
||||
|
||||
public class StoreLightningBalanceViewModel
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public Money TotalOnchain { get; set; }
|
||||
public LightMoney TotalOffchain { get; set; }
|
||||
public LightningNodeBalance Balance { get; set; }
|
||||
public string ProblemDescription { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
public bool InitialRendering { get; set; } = true;
|
||||
public HistogramType Type { get; set; }
|
||||
public IList<DateTimeOffset> Labels { get; set; }
|
||||
public IList<decimal> Series { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
}
|
||||
|
@ -2,16 +2,17 @@
|
||||
|
||||
@if (Model.Services != null && Model.Services.Any())
|
||||
{
|
||||
<div id="StoreLightningServices-@Model.Store.Id" class="widget store-lightning-services">
|
||||
<div id="StoreLightningServices-@Model.StoreId" class="widget store-lightning-services">
|
||||
<header class="mb-4">
|
||||
<h6>Lightning Services</h6>
|
||||
<h6 text-translate="true">Lightning Services</h6>
|
||||
<a
|
||||
asp-controller="UIPublicLightningNodeInfo"
|
||||
asp-action="ShowLightningNodeInfo"app-top-items
|
||||
asp-route-cryptoCode="@Model.CryptoCode"
|
||||
asp-route-storeId="@Model.Store.Id"
|
||||
asp-route-storeId="@Model.StoreId"
|
||||
target="_blank"
|
||||
id="PublicNodeInfo">
|
||||
id="PublicNodeInfo"
|
||||
text-translate="true">
|
||||
Node Info
|
||||
</a>
|
||||
</header>
|
||||
|
@ -2,15 +2,16 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@ -20,24 +21,38 @@ public class StoreLightningServices : ViewComponent
|
||||
{
|
||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
|
||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||
|
||||
public StoreLightningServices(
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
IAuthorizationService authorizationService,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions)
|
||||
{
|
||||
_networkProvider = networkProvider;
|
||||
_btcpayServerOptions = btcpayServerOptions;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_externalServiceOptions = externalServiceOptions;
|
||||
_authorizationService = authorizationService;
|
||||
_handlers = handlers;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(StoreLightningServicesViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode)
|
||||
{
|
||||
if (vm.Store == null)
|
||||
throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
var vm = new StoreLightningServicesViewModel { StoreId = store.Id, CryptoCode = cryptoCode };
|
||||
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
|
||||
var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, _handlers);
|
||||
if (existing?.IsInternalNode is true && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out _))
|
||||
{
|
||||
var result = await _authorizationService.AuthorizeAsync(HttpContext.User, null, new PolicyRequirement(Policies.CanUseInternalLightningNode));
|
||||
vm.LightningNodeType = result.Succeeded ? LightningNodeType.Internal : null;
|
||||
}
|
||||
|
||||
if (vm.LightningNodeType != LightningNodeType.Internal)
|
||||
return View(vm);
|
||||
if (!User.IsInRole(Roles.ServerAdmin))
|
||||
|
@ -8,8 +8,8 @@ namespace BTCPayServer.Components.StoreLightningServices;
|
||||
|
||||
public class StoreLightningServicesViewModel
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public LightningNodeType LightningNodeType { get; set; }
|
||||
public LightningNodeType? LightningNodeType { get; set; }
|
||||
public List<AdditionalServiceViewModel> Services { get; set; }
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
@using BTCPayServer.Client
|
||||
@model BTCPayServer.Components.StoreNumbers.StoreNumbersViewModel
|
||||
|
||||
<div class="widget store-numbers" id="StoreNumbers-@Model.Store.Id">
|
||||
<div class="widget store-numbers" id="StoreNumbers-@Model.StoreId">
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<span class="visually-hidden" text-translate="true">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("StoreNumbers", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const url = @Safe.Json(Url.Action("StoreNumbers", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.StoreId);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreNumbers-${storeId}`).outerHTML = await response.text();
|
||||
@ -24,24 +24,24 @@
|
||||
{
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Paid invoices in the last @Model.TimeframeDays days</h6>
|
||||
<h6 text-translate="true">@ViewLocalizer["Paid invoices in the last {0} days", Model.TimeframeDays]</h6>
|
||||
@if (Model.PaidInvoices > 0)
|
||||
{
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanViewInvoices">View All</a>
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" permission="@Policies.CanViewInvoices" text-translate="true">View All</a>
|
||||
}
|
||||
</header>
|
||||
<div class="h3">@Model.PaidInvoices</div>
|
||||
</div>
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Payouts Pending</h6>
|
||||
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanManagePullPayments">Manage</a>
|
||||
<h6 text-translate="true">Payouts Pending</h6>
|
||||
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.StoreId" permission="@Policies.CanManagePullPayments" text-translate="true">Manage</a>
|
||||
</header>
|
||||
<div class="h3">@Model.PayoutsPending</div>
|
||||
</div>
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Refunds Issued</h6>
|
||||
<h6 text-translate="true">Refunds Issued</h6>
|
||||
</header>
|
||||
<div class="h3">@Model.RefundsIssued</div>
|
||||
</div>
|
||||
|
@ -1,19 +1,12 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Components.StoreRecentTransactions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreNumbers;
|
||||
@ -34,14 +27,15 @@ public class StoreNumbers : ViewComponent
|
||||
_invoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreNumbersViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
|
||||
{
|
||||
if (vm.Store == null)
|
||||
throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
|
||||
vm.WalletId = new WalletId(vm.Store.Id, vm.CryptoCode);
|
||||
var vm = new StoreNumbersViewModel
|
||||
{
|
||||
StoreId = store.Id,
|
||||
CryptoCode = cryptoCode,
|
||||
InitialRendering = initialRendering,
|
||||
WalletId = new WalletId(store.Id, cryptoCode)
|
||||
};
|
||||
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
@ -50,12 +44,12 @@ public class StoreNumbers : ViewComponent
|
||||
var offset = DateTimeOffset.Now.AddDays(-vm.TimeframeDays).ToUniversalTime();
|
||||
|
||||
vm.PaidInvoices = await _invoiceRepository.GetInvoiceCount(
|
||||
new InvoiceQuery { StoreId = new [] { vm.Store.Id }, StartDate = offset, Status = new [] { "paid", "confirmed" } });
|
||||
new InvoiceQuery { StoreId = [store.Id], StartDate = offset, Status = ["paid", "confirmed"] });
|
||||
vm.PayoutsPending = await ctx.Payouts
|
||||
.Where(p => p.PullPaymentData.StoreId == vm.Store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
|
||||
.Where(p => p.PullPaymentData.StoreId == store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
|
||||
.CountAsync();
|
||||
vm.RefundsIssued = await ctx.Invoices
|
||||
.Where(i => i.StoreData.Id == vm.Store.Id && !i.Archived && i.Created >= offset)
|
||||
.Where(i => i.StoreData.Id == store.Id && !i.Archived && i.Created >= offset)
|
||||
.SelectMany(i => i.Refunds)
|
||||
.CountAsync();
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreNumbers;
|
||||
|
||||
public class StoreNumbersViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public int PayoutsPending { get; set; }
|
||||
public int TimeframeDays { get; set; } = 7;
|
||||
|
@ -1,28 +1,27 @@
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
|
||||
|
||||
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
|
||||
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.StoreId">
|
||||
<header>
|
||||
<h3>Recent Invoices</h3>
|
||||
<h3 text-translate="true">Recent Invoices</h3>
|
||||
@if (Model.Invoices.Any())
|
||||
{
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id">View All</a>
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" text-translate="true">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<span class="visually-hidden" text-translate="true">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.StoreId);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreRecentInvoices-${storeId}`).outerHTML = await response.text();
|
||||
@ -36,10 +35,10 @@
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="w-125px" text-translate="true">Date</th>
|
||||
<th class="text-nowrap" text-translate="true">Invoice Id</th>
|
||||
<th text-translate="true">Status</th>
|
||||
<th class="text-end" text-translate="true">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -65,10 +64,10 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary my-3">
|
||||
<p class="text-secondary my-3" text-translate="true">
|
||||
There are no recent invoices.
|
||||
</p>
|
||||
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.Store.Id" class="fw-semibold">
|
||||
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.StoreId" class="fw-semibold" text-translate="true">
|
||||
Create Invoice
|
||||
</a>
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
@ -9,7 +7,6 @@ using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
@ -35,12 +32,15 @@ public class StoreRecentInvoices : ViewComponent
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreRecentInvoicesViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
|
||||
{
|
||||
if (vm.Store == null)
|
||||
throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
var vm = new StoreRecentInvoicesViewModel
|
||||
{
|
||||
StoreId = store.Id,
|
||||
CryptoCode = cryptoCode,
|
||||
InitialRendering = initialRendering
|
||||
};
|
||||
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
@ -48,7 +48,7 @@ public class StoreRecentInvoices : ViewComponent
|
||||
var invoiceEntities = await _invoiceRepo.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
UserId = userId,
|
||||
StoreId = new[] { vm.Store.Id },
|
||||
StoreId = [store.Id],
|
||||
IncludeArchived = false,
|
||||
IncludeRefunds = true,
|
||||
Take = 5
|
||||
@ -68,7 +68,7 @@ public class StoreRecentInvoices : ViewComponent
|
||||
Details = new InvoiceDetailsModel
|
||||
{
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(false)
|
||||
Payments = invoice.GetPayments(false)
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
|
@ -5,7 +5,7 @@ namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
public class StoreRecentInvoicesViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public IList<StoreRecentInvoiceViewModel> Invoices { get; set; } = new List<StoreRecentInvoiceViewModel>();
|
||||
public bool InitialRendering { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
|
@ -2,25 +2,25 @@
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
|
||||
|
||||
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
|
||||
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.StoreId">
|
||||
<header>
|
||||
<h3>Recent Transactions</h3>
|
||||
<h3 text-translate="true">Recent Transactions</h3>
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId" text-translate="true">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<span class="visually-hidden" text-translate="true">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("RecentTransactions", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const url = @Safe.Json(Url.Action("RecentTransactions", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.StoreId);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreRecentTransactions-${storeId}`).outerHTML = await response.text();
|
||||
@ -34,10 +34,10 @@
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="w-125px" text-translate="true">Date</th>
|
||||
<th text-translate="true">Transaction</th>
|
||||
<th text-translate="true">Labels</th>
|
||||
<th class="text-end" text-translate="true">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -90,7 +90,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3 mb-0">
|
||||
<p class="text-secondary mt-3 mb-0" text-translate="true">
|
||||
There are no recent transactions.
|
||||
</p>
|
||||
}
|
||||
|
@ -1,27 +1,16 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Client;
|
||||
using static BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
@ -47,26 +36,27 @@ public class StoreRecentTransactions : ViewComponent
|
||||
_transactionLinkProviders = transactionLinkProviders;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreRecentTransactionsViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
|
||||
{
|
||||
if (vm.Store == null)
|
||||
throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
|
||||
vm.WalletId = new WalletId(vm.Store.Id, vm.CryptoCode);
|
||||
var vm = new StoreRecentTransactionsViewModel
|
||||
{
|
||||
StoreId = store.Id,
|
||||
CryptoCode = cryptoCode,
|
||||
InitialRendering = initialRendering,
|
||||
WalletId = new WalletId(store.Id, cryptoCode)
|
||||
};
|
||||
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
var derivationSettings = vm.Store.GetDerivationSchemeSettings(_handlers, vm.CryptoCode);
|
||||
var derivationSettings = store.GetDerivationSchemeSettings(_handlers, vm.CryptoCode);
|
||||
var transactions = new List<StoreRecentTransactionViewModel>();
|
||||
if (derivationSettings?.AccountDerivation is not null)
|
||||
{
|
||||
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(vm.CryptoCode);
|
||||
var network = ((IHasNetwork)_handlers[pmi]).Network;
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: this.HttpContext.RequestAborted);
|
||||
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: HttpContext.RequestAborted);
|
||||
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo(vm.WalletId, allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
|
||||
transactions = allTransactions
|
||||
|
@ -1,11 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
public class StoreRecentTransactionsViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public IList<StoreRecentTransactionViewModel> Transactions { get; set; } = new List<StoreRecentTransactionViewModel>();
|
||||
public WalletId WalletId { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
|
@ -17,9 +17,9 @@
|
||||
<small class="badge bg-warning rounded-pill ms-1 ms-sm-0" title="@type">@displayType</small>
|
||||
}
|
||||
}
|
||||
private static string StoreName(string title)
|
||||
private string StoreName(string title)
|
||||
{
|
||||
return string.IsNullOrEmpty(title) ? "Unnamed Store" : title;
|
||||
return string.IsNullOrEmpty(title) ? StringLocalizer["Unnamed Store"] : title;
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
@ -44,7 +44,7 @@ else
|
||||
{
|
||||
<vc:icon symbol="nav-store"/>
|
||||
}
|
||||
<span>@(Model.CurrentStoreId == null ? "Select Store" : Model.CurrentDisplayName)</span>
|
||||
<span>@(Model.CurrentStoreId == null ? StringLocalizer["Select Store"] : Model.CurrentDisplayName)</span>
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
<ul id="StoreSelectorMenu" class="dropdown-menu" aria-labelledby="StoreSelectorToggle">
|
||||
@ -58,15 +58,15 @@ else
|
||||
{
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
}
|
||||
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
|
||||
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Create)" id="StoreSelectorCreate" text-translate="true">Create Store</a></li>
|
||||
@if (Model.ArchivedCount > 0)
|
||||
{
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
|
||||
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Index)" id="StoreSelectorArchived">@(Model.ArchivedCount == 1 ? StringLocalizer["{0} Archived Store", Model.ArchivedCount] : StringLocalizer["{0} Archived Stores", Model.ArchivedCount])</a></li>
|
||||
}
|
||||
@*
|
||||
<li permission="@Policies.CanModifyServerSettings"><hr class="dropdown-divider"></li>
|
||||
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.ActivePageClass(ServerNavPages.Stores)" id="StoreSelectorAdminStores">Admin Store Overview</a></li>
|
||||
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.ActivePageClass(ServerNavPages.Stores)" id="StoreSelectorAdminStores" text-translate="true">Admin Store Overview</a></li>
|
||||
*@
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1,16 +1,11 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Components.StoreSelector
|
||||
{
|
||||
@ -38,13 +33,11 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
var archivedCount = stores.Count(s => s.Archived);
|
||||
var options = stores
|
||||
.Where(store => !store.Archived)
|
||||
.Select(store =>
|
||||
new StoreSelectorOption
|
||||
.Select(store => new StoreSelectorOption
|
||||
{
|
||||
Text = store.StoreName,
|
||||
Value = store.Id,
|
||||
Selected = store.Id == currentStore?.Id,
|
||||
Store = store
|
||||
Selected = store.Id == currentStore?.Id
|
||||
})
|
||||
.OrderBy(s => s.Text)
|
||||
.ToList();
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreSelector
|
||||
{
|
||||
@ -17,6 +16,5 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
public bool Selected { get; set; }
|
||||
public string Text { get; set; }
|
||||
public string Value { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
@using BTCPayServer.Services.Wallets
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.TagHelpers
|
||||
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
||||
@inject BTCPayNetworkProvider NetworkProvider
|
||||
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
||||
<div id="StoreWalletBalance-@Model.StoreId" class="widget store-wallet-balance">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||
<h6>Wallet Balance</h6>
|
||||
<h6 text-translate="true">Wallet Balance</h6>
|
||||
@if (Model.CryptoCode != Model.DefaultCurrency)
|
||||
{
|
||||
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
|
||||
@ -26,12 +26,12 @@
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodWeek-@Model.Store.Id" value="@WalletHistogramType.Week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodWeek-@Model.Store.Id">1W</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodMonth-@Model.Store.Id" value="@WalletHistogramType.Month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodMonth-@Model.Store.Id">1M</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodYear-@Model.Store.Id" value="@WalletHistogramType.Year" @(Model.Type == WalletHistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodYear-@Model.Store.Id">1Y</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodWeek-@Model.StoreId" value="@HistogramType.Week" @(Model.Type == HistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodWeek-@Model.StoreId">1W</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodMonth-@Model.StoreId" value="@HistogramType.Month" @(Model.Type == HistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodMonth-@Model.StoreId">1M</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodYear-@Model.StoreId" value="@HistogramType.Year" @(Model.Type == HistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodYear-@Model.StoreId">1Y</label>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
@ -39,10 +39,10 @@
|
||||
{
|
||||
<div class="ct-chart"></div>
|
||||
}
|
||||
else if (Model.Store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(Model.CryptoCode)) is null)
|
||||
else if (Model.MissingWalletConfig)
|
||||
{
|
||||
<p>
|
||||
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new {storeId = Model.Store.Id, cryptoCode = Model.CryptoCode})">configured a wallet</a>.
|
||||
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode })">configured a wallet</a>.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
@ -55,66 +55,65 @@
|
||||
}
|
||||
<script>
|
||||
(function () {
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const storeId = @Safe.Json(Model.StoreId);
|
||||
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
||||
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
||||
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
||||
let data = { series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) };
|
||||
let rate = null;
|
||||
|
||||
|
||||
const id = `StoreWalletBalance-${storeId}`;
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
||||
const valueTransform = value => rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
|
||||
: value
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = HistogramType.Week }));
|
||||
const valueTransform = value => rate ? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility) : value
|
||||
const labelCount = 6
|
||||
const tooltip = Chartist.plugins.tooltip2({
|
||||
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -16
|
||||
},
|
||||
valueTransformFunction(value, label) {
|
||||
return valueTransform(value) + ' ' + (rate ? defaultCurrency : cryptoCode)
|
||||
}
|
||||
})
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
const dateFormatter = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric' })
|
||||
const chartOpts = {
|
||||
fullWidth: true,
|
||||
showArea: true,
|
||||
axisY: {
|
||||
labelInterpolationFnc: valueTransform
|
||||
}
|
||||
showLabel: false,
|
||||
offset: 0
|
||||
},
|
||||
plugins: [tooltip]
|
||||
};
|
||||
|
||||
|
||||
const render = data => {
|
||||
let { series, labels } = data;
|
||||
const currency = rate ? defaultCurrency : cryptoCode;
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = valueTransform(value)
|
||||
});
|
||||
if (!series) return;
|
||||
|
||||
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
const tooltip = Chartist.plugins.tooltip2({
|
||||
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -16
|
||||
},
|
||||
valueTransformFunction: valueTransform
|
||||
})
|
||||
const renderOpts = Object.assign({}, chartOpts, { low, plugins: [tooltip] });
|
||||
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
const renderOpts = Object.assign({}, chartOpts, { low, axisX: {
|
||||
labelInterpolationFnc(date, i) {
|
||||
return i % labelEvery === 0 ? dateFormatter.format(new Date(date)) : null
|
||||
}
|
||||
} });
|
||||
const pointCount = series.length;
|
||||
const labelEvery = pointCount / labelCount;
|
||||
new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels: labels,
|
||||
series: [series]
|
||||
}, renderOpts);
|
||||
|
||||
// prevent y-axis labels from getting cut off
|
||||
window.setTimeout(() => {
|
||||
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
|
||||
if (yLabels) {
|
||||
const width = Math.max(...(yLabels.map(l => l.innerText.length * 7.5)));
|
||||
const opts = Object.assign({}, renderOpts, {
|
||||
axisY: Object.assign({}, renderOpts.axisY, { offset: width })
|
||||
});
|
||||
chart.update(null, opts);
|
||||
}
|
||||
}, 0)
|
||||
};
|
||||
|
||||
|
||||
const update = async type => {
|
||||
const url = baseUrl.replace(/\/week$/gi, `/${type}`);
|
||||
const response = await fetch(url);
|
||||
@ -123,10 +122,10 @@
|
||||
render(data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
render(data);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function addEventListeners() {
|
||||
delegate('change', `#${id} [name="StoreWalletBalancePeriod-${storeId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
@ -141,7 +140,13 @@
|
||||
render(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
window.addEventListener("DOMContentLoaded", addEventListeners);
|
||||
} else {
|
||||
addEventListeners();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
|
@ -1,29 +1,21 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Client;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||
|
||||
public class StoreWalletBalance : ViewComponent
|
||||
{
|
||||
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
|
||||
private const HistogramType DefaultType = HistogramType.Week;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
@ -57,7 +49,7 @@ public class StoreWalletBalance : ViewComponent
|
||||
|
||||
var vm = new StoreWalletBalanceViewModel
|
||||
{
|
||||
Store = store,
|
||||
StoreId = store.Id,
|
||||
CryptoCode = cryptoCode,
|
||||
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
|
||||
DefaultCurrency = defaultCurrency,
|
||||
@ -82,6 +74,10 @@ public class StoreWalletBalance : ViewComponent
|
||||
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token);
|
||||
vm.Balance = balance.Available.GetValue(network);
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.MissingWalletConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
|
@ -1,19 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||
|
||||
public class StoreWalletBalanceViewModel
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
public decimal? Balance { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public WalletHistogramType Type { get; set; }
|
||||
public IList<string> Labels { get; set; }
|
||||
public HistogramType Type { get; set; }
|
||||
public IList<DateTimeOffset> Labels { get; set; }
|
||||
public IList<decimal> Series { get; set; }
|
||||
public bool MissingWalletConfig { get; set; }
|
||||
}
|
||||
|
@ -3,8 +3,8 @@
|
||||
<div class="btcpay-theme-switch @Model.CssClass">
|
||||
<span class="btcpay-theme-switch-label" text-translate="true">Theme</span>
|
||||
<div class="btcpay-theme-switch-themes">
|
||||
<button type="button" title="System" data-theme="system"><vc:icon symbol="themes-system"/></button>
|
||||
<button type="button" title="Light" data-theme="light"><vc:icon symbol="themes-light"/></button>
|
||||
<button type="button" title="Dark" data-theme="dark"><vc:icon symbol="themes-dark"/></button>
|
||||
<button type="button" title="@StringLocalizer["System"]" data-theme="system"><vc:icon symbol="themes-system"/></button>
|
||||
<button type="button" title="@StringLocalizer["Light"]" data-theme="light"><vc:icon symbol="themes-light"/></button>
|
||||
<button type="button" title="@StringLocalizer["Dark"]" data-theme="dark"><vc:icon symbol="themes-dark"/></button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,27 +1,15 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Secp256k1;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BTCPayServer.Components.WalletNav
|
||||
{
|
||||
@ -33,6 +21,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly DefaultRulesCollection _defaultRules;
|
||||
private readonly RateFetcher _rateFetcher;
|
||||
private IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public WalletNav(
|
||||
BTCPayWalletProvider walletProvider,
|
||||
@ -40,6 +29,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
UIWalletsController walletsController,
|
||||
CurrencyNameTable currencies,
|
||||
DefaultRulesCollection defaultRules,
|
||||
IStringLocalizer stringLocalizer,
|
||||
RateFetcher rateFetcher)
|
||||
{
|
||||
_walletProvider = walletProvider;
|
||||
@ -48,6 +38,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
_currencies = currencies;
|
||||
_defaultRules = defaultRules;
|
||||
_rateFetcher = rateFetcher;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(WalletId walletId)
|
||||
@ -71,7 +62,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
Network = network,
|
||||
Balance = balance.ShowMoney(network),
|
||||
DefaultCurrency = defaultCurrency,
|
||||
Label = derivation?.Label ?? $"{store.StoreName} {walletId.CryptoCode} Wallet"
|
||||
Label = derivation?.Label ?? $"{store.StoreName} {StringLocalizer["{0} Wallet", walletId.CryptoCode]}"
|
||||
};
|
||||
|
||||
if (defaultCurrency != network.CryptoCode)
|
||||
|
@ -3,7 +3,9 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
@ -15,6 +17,7 @@ using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
@ -33,13 +36,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IFileService _fileService;
|
||||
|
||||
public GreenfieldAppsController(
|
||||
AppService appService,
|
||||
UriResolver uriResolver,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
CurrencyNameTable currencies,
|
||||
IFileService fileService,
|
||||
UserManager<ApplicationUser> userManager
|
||||
)
|
||||
{
|
||||
@ -47,6 +51,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_uriResolver = uriResolver;
|
||||
_storeRepository = storeRepository;
|
||||
_currencies = currencies;
|
||||
_fileService = fileService;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
@ -221,10 +226,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> DeleteApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
}
|
||||
if (app == null) return AppNotFound();
|
||||
|
||||
await _appService.DeleteApp(app);
|
||||
|
||||
@ -254,6 +256,57 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var items = stats.GetRange(offset, max);
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/apps/{appId}/image")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> UploadAppItemImage(string appId, IFormFile? file)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (app == null || userId == null) return AppNotFound();
|
||||
|
||||
UploadImageResultModel? upload = null;
|
||||
if (file is null)
|
||||
ModelState.AddModelError(nameof(file), "Invalid file");
|
||||
else
|
||||
{
|
||||
upload = await _fileService.UploadImage(file, userId, 500_000);
|
||||
if (!upload.Success)
|
||||
ModelState.AddModelError(nameof(file), upload.Response);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
try
|
||||
{
|
||||
var storedFile = upload!.StoredFile!;
|
||||
var fileData = new FileData
|
||||
{
|
||||
Id = storedFile.Id,
|
||||
UserId = storedFile.ApplicationUserId,
|
||||
Url = await _fileService.GetFileUrl(Request.GetAbsoluteRootUri(), storedFile.Id),
|
||||
OriginalName = storedFile.FileName,
|
||||
StorageName = storedFile.StorageFileName,
|
||||
CreatedAt = storedFile.Timestamp
|
||||
};
|
||||
return Ok(fileData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return this.CreateAPIError(404, "file-upload-failed", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/apps/{appId}/image/{fileId}")]
|
||||
public async Task<IActionResult> DeleteAppItemImage(string appId, string fileId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (app == null || userId == null) return AppNotFound();
|
||||
if (!string.IsNullOrEmpty(fileId)) await _fileService.RemoveFile(fileId, userId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private IActionResult AppNotFound()
|
||||
{
|
||||
@ -355,6 +408,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
||||
Enum.TryParse<PosViewType>(settings.DefaultView.ToString(), true, out var defaultView);
|
||||
var items = AppService.Parse(settings.Template);
|
||||
|
||||
return new PointOfSaleAppData
|
||||
{
|
||||
@ -382,16 +436,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
RedirectUrl = settings.RedirectUrl,
|
||||
Description = settings.Description,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
Items = JsonConvert.DeserializeObject(
|
||||
JsonConvert.SerializeObject(
|
||||
AppService.Parse(settings.Template),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver =
|
||||
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
}
|
||||
)
|
||||
)
|
||||
Items = items
|
||||
};
|
||||
}
|
||||
|
||||
@ -420,6 +465,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||
Enum.TryParse<CrowdfundResetEvery>(settings.ResetEvery.ToString(), true, out var resetEvery);
|
||||
var perks = AppService.Parse(settings.PerksTemplate);
|
||||
|
||||
return new CrowdfundAppData
|
||||
{
|
||||
@ -451,15 +497,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
Sounds = settings.Sounds,
|
||||
AnimationColors = settings.AnimationColors,
|
||||
Perks = JsonConvert.DeserializeObject(
|
||||
JsonConvert.SerializeObject(
|
||||
AppService.Parse(settings.PerksTemplate),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
}
|
||||
)
|
||||
)
|
||||
Perks = perks
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions;
|
||||
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly DefaultRulesCollection _defaultRules;
|
||||
|
||||
public LanguageService LanguageService { get; }
|
||||
@ -65,6 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions,
|
||||
PayoutMethodHandlerDictionary payoutHandlers,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
DefaultRulesCollection defaultRules)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
@ -79,6 +81,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_paymentLinkExtensions = paymentLinkExtensions;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_handlers = handlers;
|
||||
_networkProvider = networkProvider;
|
||||
_defaultRules = defaultRules;
|
||||
LanguageService = languageService;
|
||||
}
|
||||
@ -338,6 +341,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
PaymentPrompt? paymentPrompt = null;
|
||||
PayoutMethodId? payoutMethodId = null;
|
||||
if (request.PayoutMethodId is null)
|
||||
request.PayoutMethodId = invoice.GetDefaultPaymentMethodId(store, _networkProvider)?.ToString();
|
||||
|
||||
if (request.PayoutMethodId is not null && PayoutMethodId.TryParse(request.PayoutMethodId, out payoutMethodId))
|
||||
{
|
||||
var supported = _payoutHandlers.GetSupportedPayoutMethods(store);
|
||||
|
@ -1,13 +1,10 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -31,8 +28,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
PoliciesSettings policiesSettings, LightningClientFactoryService lightningClientFactory,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IAuthorizationService authorizationService,
|
||||
PaymentMethodHandlerDictionary handlers
|
||||
) : base(policiesSettings, authorizationService, handlers)
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
LightningHistogramService lnHistogramService
|
||||
) : base(policiesSettings, authorizationService, handlers, lnHistogramService)
|
||||
{
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
@ -55,6 +53,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return base.GetBalance(cryptoCode, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/histogram")]
|
||||
public override Task<IActionResult> GetHistogram(string cryptoCode, [FromQuery] HistogramType? type = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return base.GetHistogram(cryptoCode, type, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")]
|
||||
|
@ -1,8 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -34,7 +32,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
LightningClientFactoryService lightningClientFactory, PaymentMethodHandlerDictionary handlers,
|
||||
PoliciesSettings policiesSettings,
|
||||
IAuthorizationService authorizationService) : base(policiesSettings, authorizationService, handlers)
|
||||
IAuthorizationService authorizationService,
|
||||
LightningHistogramService lnHistogramService) : base(policiesSettings, authorizationService, handlers, lnHistogramService)
|
||||
{
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
@ -56,6 +55,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return base.GetBalance(cryptoCode, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram")]
|
||||
public override Task<IActionResult> GetHistogram(string cryptoCode, [FromQuery] HistogramType? type = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return base.GetHistogram(cryptoCode, type, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
@ -64,6 +70,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return base.ConnectToNode(cryptoCode, request, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
|
||||
@ -71,6 +78,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return base.GetChannels(cryptoCode, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
@ -32,15 +33,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly PoliciesSettings _policiesSettings;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly LightningHistogramService _lnHistogramService;
|
||||
|
||||
protected GreenfieldLightningNodeApiController(
|
||||
PoliciesSettings policiesSettings,
|
||||
IAuthorizationService authorizationService,
|
||||
PaymentMethodHandlerDictionary handlers)
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
LightningHistogramService lnHistogramService)
|
||||
{
|
||||
_policiesSettings = policiesSettings;
|
||||
_authorizationService = authorizationService;
|
||||
_handlers = handlers;
|
||||
_lnHistogramService = lnHistogramService;
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetInfo(string cryptoCode, CancellationToken cancellationToken = default)
|
||||
@ -86,6 +90,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
: null
|
||||
});
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetHistogram(string cryptoCode, HistogramType? type = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Enum.TryParse<HistogramType>(type.ToString(), true, out var histType);
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
var data = await _lnHistogramService.GetHistogram(lightningClient, histType, cancellationToken);
|
||||
if (data == null) return this.CreateAPIError(404, "histogram-not-found", "The lightning histogram was not found.");
|
||||
|
||||
return Ok(new HistogramData
|
||||
{
|
||||
Type = data.Type,
|
||||
Balance = data.Balance,
|
||||
Series = data.Series,
|
||||
Labels = data.Labels
|
||||
});
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
@ -135,7 +135,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Body = entity.Body,
|
||||
StoreId = entity.StoreId,
|
||||
Seen = entity.Seen,
|
||||
Link = string.IsNullOrEmpty(entity.ActionLink) ? null : new Uri(entity.ActionLink)
|
||||
Link = entity.ActionLink
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -131,26 +131,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.BOLT11Expiration), $"The BOLT11 expiration should be positive");
|
||||
}
|
||||
PayoutMethodId?[]? payoutMethods = null;
|
||||
if (request.PayoutMethods is { } payoutMethodsStr)
|
||||
|
||||
var supported = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
|
||||
request.PayoutMethods ??= supported.Select(s => s.ToString()).ToArray();
|
||||
for (int i = 0; i < request.PayoutMethods.Length; i++)
|
||||
{
|
||||
payoutMethods = payoutMethodsStr.Select(s =>
|
||||
var pmi = request.PayoutMethods[i] is string pm ? PayoutMethodId.TryParse(pm) : null;
|
||||
if (pmi is null || !supported.Contains(pmi))
|
||||
{
|
||||
PayoutMethodId.TryParse(s, out var pmi);
|
||||
return pmi;
|
||||
}).ToArray();
|
||||
var supported = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
|
||||
for (int i = 0; i < payoutMethods.Length; i++)
|
||||
{
|
||||
if (!supported.Contains(payoutMethods[i]))
|
||||
{
|
||||
request.AddModelError(paymentRequest => paymentRequest.PayoutMethods[i], "Invalid or unsupported payment method", this);
|
||||
}
|
||||
request.AddModelError(paymentRequest => paymentRequest.PayoutMethods[i], "Invalid or unsupported payment method", this);
|
||||
}
|
||||
}
|
||||
else
|
||||
if (request.PayoutMethods.Length is 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PayoutMethods), "This field is required");
|
||||
ModelState.AddModelError(nameof(request.PayoutMethods), "At least one payout method is required");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
@ -195,6 +189,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
BoltcardPICCData? picc = null;
|
||||
|
||||
// LNURLW is used by deeplinks
|
||||
if (request?.LNURLW is not null)
|
||||
@ -210,11 +205,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc)
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData o)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
picc = o;
|
||||
request.UID = picc.Uid;
|
||||
}
|
||||
|
||||
@ -236,8 +232,54 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
BoltcardKeys keys;
|
||||
int version;
|
||||
|
||||
var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, request.UID);
|
||||
if (request.OnExisting is OnExistingBehavior.UpdateVersion ||
|
||||
(request.OnExisting is null && registration?.PullPaymentId is null))
|
||||
{
|
||||
version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (registration?.PullPaymentId is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "This card isn't registered");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (pullPaymentId != registration.PullPaymentId)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "This card is registered on a different pull payment");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var ppId = registration.PullPaymentId;
|
||||
version = registration.Version;
|
||||
int retryCount = 0;
|
||||
retry:
|
||||
keys = issuerKey.CreatePullPaymentCardKey(request!.UID, version, ppId).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
// The server version may be higher than the card.
|
||||
// If that is the case, let's try a few versions until we find the right one
|
||||
// by checking c.
|
||||
if (request?.LNURLW is { } lnurlw &&
|
||||
ExtractC(lnurlw) is string c &&
|
||||
picc is not null)
|
||||
{
|
||||
if (!keys.AuthenticationKey.CheckSunMac(c, picc))
|
||||
{
|
||||
retryCount++;
|
||||
version--;
|
||||
if (version < 0 || retryCount > 5)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "Unable to get keys of this card, it might be caused by a version mismatch");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
@ -256,7 +298,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
private string? ExtractP(string? url)
|
||||
public static string? Extract(string? url, string param, int size)
|
||||
{
|
||||
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return null;
|
||||
@ -264,11 +306,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (num == -1)
|
||||
return null;
|
||||
string input = uri.AbsoluteUri.Substring(num);
|
||||
Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})");
|
||||
Match match = Regex.Match(input, param + "=([a-f0-9A-F]{" + size + "})");
|
||||
if (!match.Success)
|
||||
return null;
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
public static string? ExtractP(string? url) => Extract(url, "p", 32);
|
||||
public static string? ExtractC(string? url) => Extract(url, "c", 16);
|
||||
|
||||
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
|
||||
[AllowAnonymous]
|
||||
@ -407,23 +451,29 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount, payoutHandler.Currency, pp.Currency);
|
||||
if (amtError.error is not null)
|
||||
var amt = ClaimRequest.GetClaimedAmount(destination.destination, request.Amount, payoutHandler.Currency, pp.Currency);
|
||||
if (amt is ClaimRequest.ClaimedAmountResult.Error err)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), amtError.error );
|
||||
ModelState.AddModelError(nameof(request.Amount), err.Message);
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
request.Amount = amtError.amount;
|
||||
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
||||
else if (amt is ClaimRequest.ClaimedAmountResult.Success succ)
|
||||
{
|
||||
Destination = destination.destination,
|
||||
PullPaymentId = pullPaymentId,
|
||||
Value = request.Amount,
|
||||
PayoutMethodId = payoutMethodId,
|
||||
StoreId = pp.StoreId
|
||||
});
|
||||
|
||||
return HandleClaimResult(result);
|
||||
request.Amount = succ.Amount;
|
||||
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
||||
{
|
||||
Destination = destination.destination,
|
||||
PullPaymentId = pullPaymentId,
|
||||
ClaimedAmount = request.Amount,
|
||||
PayoutMethodId = payoutMethodId,
|
||||
StoreId = pp.StoreId
|
||||
});
|
||||
return HandleClaimResult(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Should never happen {amt}");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payouts")]
|
||||
@ -456,6 +506,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
|
||||
PullPaymentBlob? ppBlob = null;
|
||||
string? ppCurrency = null;
|
||||
if (request?.PullPaymentId is not null)
|
||||
{
|
||||
|
||||
@ -464,6 +515,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
ppBlob = pp.GetBlob();
|
||||
ppCurrency = pp.Currency;
|
||||
}
|
||||
var destination = await payoutHandler.ParseAndValidateClaimDestination(request!.Destination, ppBlob, default);
|
||||
if (destination.destination is null)
|
||||
@ -472,30 +524,37 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount);
|
||||
if (amtError.error is not null)
|
||||
var amt = ClaimRequest.GetClaimedAmount(destination.destination, request.Amount, payoutHandler.Currency, ppCurrency);
|
||||
if (amt is ClaimRequest.ClaimedAmountResult.Error err)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), amtError.error );
|
||||
ModelState.AddModelError(nameof(request.Amount), err.Message);
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
request.Amount = amtError.amount;
|
||||
if (request.Amount is { } v && (v < ppBlob?.MinimumClaim || v == 0.0m))
|
||||
else if (amt is ClaimRequest.ClaimedAmountResult.Success succ)
|
||||
{
|
||||
var minimumClaim = ppBlob?.MinimumClaim is decimal val ? val : 0.0m;
|
||||
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {minimumClaim})");
|
||||
return this.CreateValidationError(ModelState);
|
||||
request.Amount = succ.Amount;
|
||||
if (request.Amount is { } v && (v < ppBlob?.MinimumClaim || v == 0.0m))
|
||||
{
|
||||
var minimumClaim = ppBlob?.MinimumClaim is decimal val ? val : 0.0m;
|
||||
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {minimumClaim})");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
||||
{
|
||||
Destination = destination.destination,
|
||||
PullPaymentId = request.PullPaymentId,
|
||||
PreApprove = request.Approved,
|
||||
ClaimedAmount = request.Amount,
|
||||
PayoutMethodId = paymentMethodId,
|
||||
StoreId = storeId,
|
||||
Metadata = request.Metadata
|
||||
});
|
||||
return HandleClaimResult(result);
|
||||
}
|
||||
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
||||
else
|
||||
{
|
||||
Destination = destination.destination,
|
||||
PullPaymentId = request.PullPaymentId,
|
||||
PreApprove = request.Approved,
|
||||
Value = request.Amount,
|
||||
PayoutMethodId = paymentMethodId,
|
||||
StoreId = storeId,
|
||||
Metadata = request.Metadata
|
||||
});
|
||||
return HandleClaimResult(result);
|
||||
throw new NotSupportedException($"Should never happen {amt}");
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult HandleClaimResult(ClaimRequest.ClaimResponse result)
|
||||
|
2
BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs
2
BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs
@ -59,7 +59,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
PayoutMethodId = data.PayoutMethodId,
|
||||
IntervalSeconds = blob.Interval,
|
||||
CancelPayoutAfterFailures = blob.CancelPayoutAfterFailures,
|
||||
ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly
|
||||
};
|
||||
}
|
||||
@ -68,7 +67,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return new LightningAutomatedPayoutBlob() {
|
||||
Interval = data.IntervalSeconds,
|
||||
CancelPayoutAfterFailures = data.CancelPayoutAfterFailures,
|
||||
ProcessNewPayoutsInstantly = data.ProcessNewPayoutsInstantly
|
||||
};
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -20,7 +19,6 @@ using BTCPayServer.Payments.PayJoin;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
@ -58,6 +56,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly IFeeProviderFactory _feeProviderFactory;
|
||||
private readonly UTXOLocker _utxoLocker;
|
||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||
private readonly WalletHistogramService _walletHistogramService;
|
||||
|
||||
public GreenfieldStoreOnChainWalletsController(
|
||||
IAuthorizationService authorizationService,
|
||||
@ -74,6 +73,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
WalletReceiveService walletReceiveService,
|
||||
IFeeProviderFactory feeProviderFactory,
|
||||
UTXOLocker utxoLocker,
|
||||
WalletHistogramService walletHistogramService,
|
||||
TransactionLinkProviders transactionLinkProviders
|
||||
)
|
||||
{
|
||||
@ -91,6 +91,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_feeProviderFactory = feeProviderFactory;
|
||||
_utxoLocker = utxoLocker;
|
||||
_walletHistogramService = walletHistogramService;
|
||||
_transactionLinkProviders = transactionLinkProviders;
|
||||
}
|
||||
|
||||
@ -114,6 +115,27 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/histogram")]
|
||||
public async Task<IActionResult> GetOnChainWalletHistogram(string storeId, string paymentMethodId, [FromQuery] string? type = null)
|
||||
{
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network, out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
Enum.TryParse<HistogramType>(type, true, out var histType);
|
||||
var data = await _walletHistogramService.GetHistogram(Store, walletId, histType);
|
||||
if (data == null) return this.CreateAPIError(404, "histogram-not-found", "The wallet histogram was not found.");
|
||||
|
||||
return Ok(new HistogramData
|
||||
{
|
||||
Type = data.Type,
|
||||
Balance = data.Balance,
|
||||
Series = data.Series,
|
||||
Labels = data.Labels
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/feerate")]
|
||||
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string paymentMethodId, int? blockTarget = null)
|
||||
@ -357,7 +379,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (network.ReadonlyWallet)
|
||||
{
|
||||
return this.CreateAPIError(503, "not-available",
|
||||
$"{network.CryptoCode} sending services are not currently available");
|
||||
$"This network only support read-only features");
|
||||
}
|
||||
|
||||
//This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation.
|
||||
@ -430,7 +452,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
try
|
||||
{
|
||||
bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork);
|
||||
amount ??= bip21.Amount.GetValue(network);
|
||||
amount ??= bip21.Amount?.GetValue(network);
|
||||
if (bip21.Address is null)
|
||||
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
|
||||
"This BIP21 destination is missing a bitcoin address", this);
|
||||
@ -551,8 +573,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
if (!derivationScheme.IsHotWallet || signingKeyStr is null)
|
||||
{
|
||||
return this.CreateAPIError(503, "not-available",
|
||||
$"{network.CryptoCode} sending services are not currently available");
|
||||
var reason = !derivationScheme.IsHotWallet ?
|
||||
"You cannot send from a cold wallet" :
|
||||
"NBXplorer doesn't have the seed of the wallet";
|
||||
|
||||
return this.CreateAPIError(503, "not-available", reason);
|
||||
}
|
||||
|
||||
var signingKey = ExtKey.Parse(signingKeyStr, network.NBitcoinNetwork);
|
||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
@ -111,30 +112,25 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost("~/api/v1/stores/{storeId}/logo")]
|
||||
public async Task<IActionResult> UploadStoreLogo(string storeId, IFormFile file)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null) return StoreNotFound();
|
||||
|
||||
if (user == null || store == null) return StoreNotFound();
|
||||
|
||||
UploadImageResultModel upload = null;
|
||||
if (file is null)
|
||||
ModelState.AddModelError(nameof(file), "Invalid file");
|
||||
else if (file.Length > 1_000_000)
|
||||
ModelState.AddModelError(nameof(file), "The uploaded image file should be less than 1MB");
|
||||
else if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
ModelState.AddModelError(nameof(file), "The uploaded file needs to be an image");
|
||||
else if (!file.FileName.IsValidFileName())
|
||||
ModelState.AddModelError(nameof(file.FileName), "Invalid filename");
|
||||
else
|
||||
{
|
||||
var formFile = await file.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
ModelState.AddModelError(nameof(file), "The uploaded file needs to be an image");
|
||||
upload = await _fileService.UploadImage(file, user.Id);
|
||||
if (!upload.Success)
|
||||
ModelState.AddModelError(nameof(file), upload.Response);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var userId = _userManager.GetUserId(User)!;
|
||||
var storedFile = await _fileService.AddFile(file!, userId);
|
||||
var storedFile = upload!.StoredFile!;
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.LogoUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
|
||||
store.SetStoreBlob(blob);
|
||||
|
@ -7,6 +7,7 @@ using System.Xml.Linq;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
@ -233,27 +234,24 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost("~/api/v1/users/me/picture")]
|
||||
public async Task<IActionResult> UploadCurrentUserProfilePicture(IFormFile? file)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null) return this.UserNotFound();
|
||||
|
||||
UploadImageResultModel? upload = null;
|
||||
if (file is null)
|
||||
ModelState.AddModelError(nameof(file), "Invalid file");
|
||||
else if (file.Length > 1_000_000)
|
||||
ModelState.AddModelError(nameof(file), "The uploaded image file should be less than 1MB");
|
||||
else if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
ModelState.AddModelError(nameof(file), "The uploaded file needs to be an image");
|
||||
else if (!file.FileName.IsValidFileName())
|
||||
ModelState.AddModelError(nameof(file.FileName), "Invalid filename");
|
||||
else
|
||||
{
|
||||
var formFile = await file.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
ModelState.AddModelError(nameof(file), "The uploaded file needs to be an image");
|
||||
upload = await _fileService.UploadImage(file, user.Id);
|
||||
if (!upload.Success)
|
||||
ModelState.AddModelError(nameof(file), upload.Response);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
try
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
var storedFile = await _fileService.AddFile(file!, user!.Id);
|
||||
var storedFile = upload!.StoredFile!;
|
||||
var blob = user.GetBlob() ?? new UserBlob();
|
||||
var fileIdUri = new UnresolvedUri.FileIdUri(storedFile.Id);
|
||||
blob.ImageUrl = fileIdUri.ToString();
|
||||
@ -274,10 +272,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> DeleteCurrentUserProfilePicture()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return this.UserNotFound();
|
||||
}
|
||||
if (user is null) return this.UserNotFound();
|
||||
|
||||
var blob = user.GetBlob() ?? new UserBlob();
|
||||
if (!string.IsNullOrEmpty(blob.ImageUrl))
|
||||
|
@ -14,18 +14,15 @@ 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;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
using Language = BTCPayServer.Client.Models.Language;
|
||||
@ -385,6 +382,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await GetController<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode, token));
|
||||
}
|
||||
|
||||
public override async Task<HistogramData> GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<HistogramData>(
|
||||
await GetController<GreenfieldStoreLightningNodeApiController>().GetHistogram(cryptoCode, type, token));
|
||||
}
|
||||
|
||||
public override async Task ConnectToLightningNode(string storeId, string cryptoCode,
|
||||
ConnectToNodeRequest request, CancellationToken token = default)
|
||||
{
|
||||
@ -461,6 +465,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await GetController<GreenfieldInternalLightningNodeApiController>().GetBalance(cryptoCode));
|
||||
}
|
||||
|
||||
public override async Task<HistogramData> GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<HistogramData>(
|
||||
await GetController<GreenfieldInternalLightningNodeApiController>().GetHistogram(cryptoCode, type, token));
|
||||
}
|
||||
|
||||
public override async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
@ -702,6 +713,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return GetFromActionResult<OnChainWalletOverviewData>(
|
||||
await GetController<GreenfieldStoreOnChainWalletsController>().ShowOnChainWalletOverview(storeId, cryptoCode));
|
||||
}
|
||||
|
||||
public override async Task<HistogramData> GetOnChainWalletHistogram(string storeId, string cryptoCode, HistogramType? type = null, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<HistogramData>(
|
||||
await GetController<GreenfieldStoreOnChainWalletsController>().GetOnChainWalletHistogram(storeId, cryptoCode, type?.ToString()));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId,
|
||||
string cryptoCode, bool forceGenerate = false,
|
||||
@ -1162,6 +1179,17 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteApp(appId));
|
||||
}
|
||||
|
||||
public override async Task<FileData> UploadAppItemImage(string appId, string filePath, string mimeType, CancellationToken token = default)
|
||||
{
|
||||
var file = GetFormFile(filePath, mimeType);
|
||||
return GetFromActionResult<FileData>(await GetController<GreenfieldAppsController>().UploadAppItemImage(appId, file));
|
||||
}
|
||||
|
||||
public override async Task DeleteAppItemImage(string appId, string fileId, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteAppItemImage(appId, fileId));
|
||||
}
|
||||
|
||||
public override Task<List<RateSource>> GetRateSources(CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(GetFromActionResult(GetController<GreenfieldStoreRateConfigurationController>().GetRateSources()));
|
||||
|
@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -47,6 +48,7 @@ namespace BTCPayServer.Controllers
|
||||
readonly ILogger _logger;
|
||||
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
public Logs Logs { get; }
|
||||
|
||||
public UIAccountController(
|
||||
@ -62,6 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
LnurlAuthService lnurlAuthService,
|
||||
LinkGenerator linkGenerator,
|
||||
IStringLocalizer stringLocalizer,
|
||||
Logs logs)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@ -78,6 +81,7 @@ namespace BTCPayServer.Controllers
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logs.PayServer;
|
||||
Logs = logs;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
@ -149,7 +153,7 @@ namespace BTCPayServer.Controllers
|
||||
var userId = _userLoginCodeService.Verify(code);
|
||||
if (userId is null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Login code was invalid";
|
||||
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Login code was invalid"].Value;
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
|
||||
@ -187,7 +191,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
// Require the user to pass basic checks (approval, confirmed email, not disabled) before they can log on
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
const string errorMessage = "Invalid login attempt.";
|
||||
var errorMessage = StringLocalizer["Invalid login attempt."].Value;
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
@ -311,7 +315,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var errorMessage = "Invalid login attempt.";
|
||||
var errorMessage = StringLocalizer["Invalid login attempt."].Value;
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
@ -629,7 +633,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
RegisteredUserId = user.Id;
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Account created.";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account created."].Value;
|
||||
var requiresConfirmedEmail = policies.RequiresConfirmedEmail && !user.EmailConfirmed;
|
||||
var requiresUserApproval = policies.RequiresUserApproval && !user.Approved;
|
||||
if (requiresConfirmedEmail)
|
||||
@ -704,7 +708,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
Message = StringLocalizer["Your email has been confirmed."].Value
|
||||
});
|
||||
await FinalizeInvitationIfApplicable(user);
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
@ -713,7 +717,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your email has been confirmed. Please set your password."
|
||||
Message = StringLocalizer["Your email has been confirmed. Please set your password."].Value
|
||||
});
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
@ -811,7 +815,9 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = hasPassword ? "Password successfully set." : "Account successfully created."
|
||||
Message = hasPassword
|
||||
? StringLocalizer["Password successfully set."].Value
|
||||
: StringLocalizer["Account successfully created."].Value
|
||||
});
|
||||
if (!hasPassword) await FinalizeInvitationIfApplicable(user);
|
||||
return RedirectToAction(nameof(Login));
|
||||
@ -848,7 +854,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Invitation accepted. Please set your password."
|
||||
Message = StringLocalizer["Invitation accepted. Please set your password."].Value
|
||||
});
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
@ -857,7 +863,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your password has been set by the user who invited you."
|
||||
Message = StringLocalizer["Your password has been set by the user who invited you."].Value
|
||||
});
|
||||
|
||||
await FinalizeInvitationIfApplicable(user);
|
||||
@ -930,7 +936,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "You cannot login over an insecure connection. Please use HTTPS or Tor."
|
||||
Message = StringLocalizer["You cannot login over an insecure connection. Please use HTTPS or Tor."].Value
|
||||
});
|
||||
|
||||
ViewData["disabled"] = true;
|
||||
|
@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository,
|
||||
IFileService fileService,
|
||||
AppService appService,
|
||||
IStringLocalizer stringLocalizer,
|
||||
IHtmlHelper html)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@ -39,6 +41,7 @@ namespace BTCPayServer.Controllers
|
||||
_fileService = fileService;
|
||||
_appService = appService;
|
||||
Html = html;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
@ -50,6 +53,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public string CreatedAppId { get; set; }
|
||||
public IHtmlHelper Html { get; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public class AppUpdated
|
||||
{
|
||||
@ -158,7 +162,7 @@ namespace BTCPayServer.Controllers
|
||||
var type = _appService.GetAppType(vm.AppType ?? vm.SelectedAppType);
|
||||
if (type is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
|
||||
ModelState.AddModelError(nameof(vm.SelectedAppType), StringLocalizer["Invalid App Type"]);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
@ -177,7 +181,7 @@ namespace BTCPayServer.Controllers
|
||||
await _appService.SetDefaultSettings(appData, defaultCurrency);
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["App successfully created"].Value;
|
||||
CreatedAppId = appData.Id;
|
||||
|
||||
var url = await type.ConfigureLink(appData);
|
||||
@ -192,7 +196,7 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{Html.Encode(app.Name)}</strong> and its settings will be permanently deleted. Are you sure?", "Delete"));
|
||||
return View("Confirm", new ConfirmModel(StringLocalizer["Delete app"], $"The app <strong>{Html.Encode(app.Name)}</strong> and its settings will be permanently deleted. Are you sure?", StringLocalizer["Delete"]));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -204,7 +208,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
if (await _appService.DeleteApp(app))
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App deleted successfully.";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["App deleted successfully."].Value;
|
||||
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
|
||||
}
|
||||
@ -227,12 +231,14 @@ namespace BTCPayServer.Controllers
|
||||
if (await _appService.SetArchived(app, archived))
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = archived
|
||||
? "The app has been archived and will no longer appear in the apps list by default."
|
||||
: "The app has been unarchived and will appear in the apps list by default again.";
|
||||
? StringLocalizer["The app has been archived and will no longer appear in the apps list by default."].Value
|
||||
: StringLocalizer["The app has been unarchived and will appear in the apps list by default again."].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Failed to {(archived ? "archive" : "unarchive")} the app.";
|
||||
TempData[WellKnownTempData.ErrorMessage] = archived
|
||||
? StringLocalizer["Failed to archive the app."].Value
|
||||
: StringLocalizer["Failed to unarchive the app."].Value;
|
||||
}
|
||||
|
||||
var url = await type.ConfigureLink(app);
|
||||
|
@ -65,6 +65,6 @@ public class UIBoltcardController : Controller
|
||||
if (!cardKey.CheckSunMac(c, piccData))
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", cancellationToken);
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", true, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ namespace BTCPayServer.Controllers
|
||||
var newDeliveryId = await WebhookNotificationManager.Redeliver(deliveryId);
|
||||
if (newDeliveryId is null)
|
||||
return NotFound();
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Successfully planned a redelivery"].Value;
|
||||
return RedirectToAction(nameof(Invoice),
|
||||
new
|
||||
{
|
||||
@ -128,6 +128,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreLink = Url.Action(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId = store.Id }),
|
||||
PaymentRequestLink = Url.Action(nameof(UIPaymentRequestController.ViewPaymentRequest), "UIPaymentRequest", new { payReqId = invoice.Metadata.PaymentRequestId }),
|
||||
Id = invoice.Id,
|
||||
Entity = invoice,
|
||||
State = invoiceState,
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
@ -293,9 +294,9 @@ namespace BTCPayServer.Controllers
|
||||
var payoutMethodIds = _payoutHandlers.GetSupportedPayoutMethods(this.GetCurrentStore());
|
||||
if (!payoutMethodIds.Any())
|
||||
{
|
||||
var vm = new RefundModel { Title = "No matching payment method" };
|
||||
var vm = new RefundModel { Title = StringLocalizer["No matching payment method"] };
|
||||
ModelState.AddModelError(nameof(vm.AvailablePaymentMethods),
|
||||
"There are no payment methods available to provide refunds with for this invoice.");
|
||||
StringLocalizer["There are no payment methods available to provide refunds with for this invoice."]);
|
||||
return View("_RefundModal", vm);
|
||||
}
|
||||
|
||||
@ -305,7 +306,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var refund = new RefundModel
|
||||
{
|
||||
Title = "Payment method",
|
||||
Title = StringLocalizer["Payment method"],
|
||||
AvailablePaymentMethods =
|
||||
new SelectList(payoutMethodIds.Select(id => new SelectListItem(id.ToString(), id.ToString())),
|
||||
"Value", "Text"),
|
||||
@ -343,7 +344,7 @@ namespace BTCPayServer.Controllers
|
||||
var pmis = _payoutHandlers.GetSupportedPayoutMethods(store);
|
||||
if (!pmis.Contains(pmi))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), $"Invalid payout method");
|
||||
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), StringLocalizer["Invalid payout method"]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
@ -352,7 +353,7 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethod = paymentMethodId is null ? null : invoice.GetPaymentPrompt(paymentMethodId);
|
||||
if (paymentMethod?.Currency is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), $"Invalid payout method");
|
||||
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), StringLocalizer["Invalid payout method"]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
@ -376,7 +377,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
case RefundSteps.SelectPaymentMethod:
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
model.Title = "How much to refund?";
|
||||
model.Title = StringLocalizer["How much to refund?"];
|
||||
|
||||
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
|
||||
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethod.Divisibility);
|
||||
@ -389,7 +390,7 @@ namespace BTCPayServer.Controllers
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
StringLocalizer["Impossible to fetch rate: {0}", rateResult.EvaluatedRule]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
@ -412,7 +413,7 @@ namespace BTCPayServer.Controllers
|
||||
case RefundSteps.SelectRate:
|
||||
createPullPayment = new CreatePullPayment
|
||||
{
|
||||
Name = $"Refund {invoice.Id}",
|
||||
Name = StringLocalizer["Refund {0}", invoice.Id],
|
||||
PayoutMethods = new[] { pmi },
|
||||
StoreId = invoice.StoreId,
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
|
||||
@ -422,7 +423,7 @@ namespace BTCPayServer.Controllers
|
||||
.Succeeded;
|
||||
if (model.SubtractPercentage is < 0 or > 100)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
|
||||
ModelState.AddModelError(nameof(model.SubtractPercentage), StringLocalizer["Percentage must be a numeric value between 0 and 100"]);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -456,11 +457,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!isPaidOver)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invoice is not overpaid");
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), StringLocalizer["Invoice is not overpaid"]);
|
||||
}
|
||||
if (overpaidAmount == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Overpaid amount cannot be calculated");
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), StringLocalizer["Overpaid amount cannot be calculated"]);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -473,17 +474,17 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
|
||||
case "Custom":
|
||||
model.Title = "How much to refund?";
|
||||
model.Title = StringLocalizer["How much to refund?"];
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
|
||||
if (model.CustomAmount <= 0)
|
||||
{
|
||||
model.AddModelError(refundModel => refundModel.CustomAmount, "Amount must be greater than 0", this);
|
||||
model.AddModelError(refundModel => refundModel.CustomAmount, StringLocalizer["Amount must be greater than 0"], this);
|
||||
}
|
||||
if (string.IsNullOrEmpty(model.CustomCurrency) ||
|
||||
_CurrencyNameTable.GetCurrencyData(model.CustomCurrency, false) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CustomCurrency), "Invalid currency");
|
||||
ModelState.AddModelError(nameof(model.CustomCurrency), StringLocalizer["Invalid currency"]);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -499,7 +500,7 @@ namespace BTCPayServer.Controllers
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
StringLocalizer["Impossible to fetch rate: {0}", rateResult.EvaluatedRule]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
@ -509,7 +510,7 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
|
||||
default:
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Please select an option before proceeding");
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), StringLocalizer["Please select an option before proceeding"]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
break;
|
||||
@ -554,6 +555,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(false),
|
||||
Entity = invoice,
|
||||
CryptoPayments = invoice.GetPaymentPrompts().Select(
|
||||
data =>
|
||||
{
|
||||
@ -606,10 +608,12 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
await _InvoiceRepository.ToggleInvoiceArchival(invoiceId, !invoice.Archived);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = invoice.Archived ? "The invoice has been unarchived and will appear in the invoice list by default again." : "The invoice has been archived and will no longer appear in the invoice list by default."
|
||||
Message = invoice.Archived
|
||||
? StringLocalizer["The invoice has been unarchived and will appear in the invoice list by default again."].Value
|
||||
: StringLocalizer["The invoice has been archived and will no longer appear in the invoice list by default."].Value
|
||||
});
|
||||
return RedirectToAction(nameof(invoice), new { invoiceId });
|
||||
}
|
||||
@ -624,28 +628,32 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListInvoices), new { storeId });
|
||||
}
|
||||
if (selectedItems.Length == 0)
|
||||
return NotSupported("No invoice has been selected");
|
||||
return NotSupported(StringLocalizer["No invoice has been selected"]);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "archive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
|
||||
TempData[WellKnownTempData.SuccessMessage] = selectedItems.Length == 1
|
||||
? StringLocalizer["{0} invoice archived.", selectedItems.Length].Value
|
||||
: StringLocalizer["{0} invoices archived.", selectedItems.Length].Value;
|
||||
break;
|
||||
|
||||
case "unarchive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems, false);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
|
||||
TempData[WellKnownTempData.SuccessMessage] = selectedItems.Length == 1
|
||||
? StringLocalizer["{0} invoice unarchived.", selectedItems.Length].Value
|
||||
: StringLocalizer["{0} invoices unarchived.", selectedItems.Length].Value;
|
||||
break;
|
||||
case "cpfp" when storeId is not null:
|
||||
var network = _NetworkProvider.DefaultNetwork;
|
||||
var explorer = _ExplorerClients.GetExplorerClient(network);
|
||||
if (explorer is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
return NotSupported(StringLocalizer["This feature is only available to BTC wallets"]);
|
||||
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
|
||||
return Forbid();
|
||||
|
||||
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_handlers, network.CryptoCode))?.AccountDerivation;
|
||||
var derivationScheme = GetCurrentStore().GetDerivationSchemeSettings(_handlers, network.CryptoCode)?.AccountDerivation;
|
||||
if (derivationScheme is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
|
||||
@ -655,7 +663,7 @@ namespace BTCPayServer.Controllers
|
||||
var parameters = new MultiValueDictionary<string, string>();
|
||||
foreach (var utxo in bumpableUTXOs)
|
||||
{
|
||||
parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
|
||||
parameters.Add("outpoints[]", utxo.Outpoint.ToString());
|
||||
}
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
@ -692,10 +700,11 @@ namespace BTCPayServer.Controllers
|
||||
if (invoiceId is null)
|
||||
return NotFound();
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
var model = await GetCheckoutModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
if (model == null)
|
||||
{
|
||||
// see if the invoice actually exists and is in a state for which we do not display the checkout
|
||||
// TODO: Can happen if the invoice has lazy activation which failed for all payment methods. We should display error instead...
|
||||
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;
|
||||
@ -711,23 +720,9 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("invoice-noscript")]
|
||||
public async Task<IActionResult> CheckoutNoScript(string? invoiceId, string? id = null, string? paymentMethodId = null, [FromQuery] string? lang = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
//
|
||||
if (invoiceId is null)
|
||||
return NotFound();
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId is null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<PaymentModel?> GetInvoiceModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang)
|
||||
private async Task<CheckoutModel?> GetCheckoutModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang, HashSet<PaymentMethodId>? excludedPaymentMethodIds = null)
|
||||
{
|
||||
var originalPaymentMethodId = paymentMethodId;
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
@ -735,11 +730,13 @@ namespace BTCPayServer.Controllers
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
if (store == null)
|
||||
return null;
|
||||
|
||||
excludedPaymentMethodIds ??= new HashSet<PaymentMethodId>();
|
||||
bool isDefaultPaymentId = false;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
var displayedPaymentMethods = invoice.GetPaymentPrompts().Select(p => p.PaymentMethodId).ToList();
|
||||
var displayedPaymentMethods = invoice.GetPaymentPrompts()
|
||||
.Where(p => !excludedPaymentMethodIds.Contains(p.PaymentMethodId))
|
||||
.Select(p => p.PaymentMethodId).ToHashSet();
|
||||
|
||||
|
||||
var btcId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
|
||||
@ -767,34 +764,7 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethodId = null;
|
||||
if (paymentMethodId is null)
|
||||
{
|
||||
PaymentMethodId? invoicePaymentId = invoice.DefaultPaymentMethod;
|
||||
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
|
||||
if (invoicePaymentId is not null)
|
||||
{
|
||||
if (displayedPaymentMethods.Contains(invoicePaymentId))
|
||||
paymentMethodId = invoicePaymentId;
|
||||
}
|
||||
if (paymentMethodId is null && storePaymentId is not null)
|
||||
{
|
||||
if (displayedPaymentMethods.Contains(storePaymentId))
|
||||
paymentMethodId = storePaymentId;
|
||||
}
|
||||
if (paymentMethodId is null && invoicePaymentId is not null)
|
||||
{
|
||||
paymentMethodId = invoicePaymentId.FindNearest(displayedPaymentMethods);
|
||||
}
|
||||
if (paymentMethodId is null && storePaymentId is not null)
|
||||
{
|
||||
paymentMethodId = storePaymentId.FindNearest(displayedPaymentMethods);
|
||||
}
|
||||
if (paymentMethodId is null)
|
||||
{
|
||||
var defaultBTC = PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode);
|
||||
var defaultLNURLPay = PaymentTypes.LNURL.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode);
|
||||
paymentMethodId = displayedPaymentMethods.FirstOrDefault(e => e == defaultBTC) ??
|
||||
displayedPaymentMethods.FirstOrDefault(e => e == defaultLNURLPay) ??
|
||||
displayedPaymentMethods.FirstOrDefault();
|
||||
}
|
||||
paymentMethodId = invoice.GetDefaultPaymentMethodId(store, _NetworkProvider, displayedPaymentMethods);
|
||||
isDefaultPaymentId = true;
|
||||
}
|
||||
if (paymentMethodId is null)
|
||||
@ -835,7 +805,14 @@ namespace BTCPayServer.Controllers
|
||||
if (prompt is null)
|
||||
return null;
|
||||
if (activated)
|
||||
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
|
||||
return await GetCheckoutModel(invoiceId, paymentMethodId, lang, excludedPaymentMethodIds);
|
||||
|
||||
if (!prompt.Activated)
|
||||
{
|
||||
// It failed to activate. Let's try to exclude it and retry
|
||||
excludedPaymentMethodIds.Add(prompt.PaymentMethodId);
|
||||
return await GetCheckoutModel(invoiceId, originalPaymentMethodId, lang, excludedPaymentMethodIds);
|
||||
}
|
||||
|
||||
var accounting = prompt.Calculate();
|
||||
|
||||
@ -876,11 +853,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
string ShowMoney(decimal value) => MoneyExtensions.ShowMoney(value, prompt.RateDivisibility ?? prompt.Divisibility);
|
||||
var model = new PaymentModel
|
||||
var model = new CheckoutModel
|
||||
{
|
||||
Activated = prompt.Activated,
|
||||
PaymentMethodName = _prettyName.PrettyName(paymentMethodId),
|
||||
CryptoCode = prompt.Currency,
|
||||
PaymentMethodName = _prettyName.PrettyName(paymentMethodId, true),
|
||||
PaymentMethodCurrency = prompt.Currency,
|
||||
RootPath = Request.PathBase.Value.WithTrailingSlash(),
|
||||
OrderId = orderId,
|
||||
InvoiceId = invoiceId,
|
||||
@ -892,9 +869,9 @@ namespace BTCPayServer.Controllers
|
||||
CelebratePayment = storeBlob.CelebratePayment,
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(paymentMethodId)),
|
||||
BtcAddress = prompt.Destination,
|
||||
BtcDue = ShowMoney(accounting.Due),
|
||||
BtcPaid = ShowMoney(accounting.Paid),
|
||||
Address = prompt.Destination,
|
||||
Due = ShowMoney(accounting.Due),
|
||||
Paid = ShowMoney(accounting.Paid),
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
// 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)),
|
||||
@ -922,45 +899,31 @@ namespace BTCPayServer.Controllers
|
||||
Status = invoice.Status.ToString(),
|
||||
// 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()
|
||||
AvailablePaymentMethods = invoice.GetPaymentPrompts()
|
||||
.Select(kv =>
|
||||
{
|
||||
var handler = _handlers[kv.PaymentMethodId];
|
||||
return new PaymentModel.AvailableCrypto
|
||||
return new CheckoutModel.AvailablePaymentMethod
|
||||
{
|
||||
Displayed = displayedPaymentMethods.Contains(kv.PaymentMethodId),
|
||||
PaymentMethodId = kv.PaymentMethodId.ToString(),
|
||||
CryptoCode = kv.Currency,
|
||||
PaymentMethodName = _prettyName.PrettyName(kv.PaymentMethodId),
|
||||
IsLightning = handler is ILightningPaymentHandler,
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(kv.PaymentMethodId)),
|
||||
Link = Url.Action(nameof(Checkout),
|
||||
new
|
||||
{
|
||||
invoiceId,
|
||||
paymentMethodId = kv.PaymentMethodId.ToString()
|
||||
})
|
||||
PaymentMethodId = kv.PaymentMethodId,
|
||||
PaymentMethodName = _prettyName.PrettyName(kv.PaymentMethodId, true),
|
||||
Order = kv.PaymentMethodId switch
|
||||
{
|
||||
_ when PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 0,
|
||||
_ when PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 1,
|
||||
_ when handler is ILightningPaymentHandler => 2,
|
||||
_ => 3
|
||||
}
|
||||
};
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.OrderByDescending(a => a.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode).ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
|
||||
})
|
||||
.OrderBy(a => a.Order)
|
||||
.ToList()
|
||||
};
|
||||
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension))
|
||||
extension.ModifyPaymentModel(new PaymentModelContext(model, store, storeBlob, invoice, Url, prompt, handler));
|
||||
model.UISettings = _viewProvider.TryGetViewViewModel(prompt, "CheckoutUI")?.View as CheckoutUIPaymentMethodSettings;
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
model.OrderAmountFiat = OrderAmountFromInvoice(model.CryptoCode, invoice, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
|
||||
foreach (var paymentPrompt in invoice.GetPaymentPrompts())
|
||||
{
|
||||
var vvm = _viewProvider.TryGetViewViewModel(paymentPrompt, "CheckoutUI");
|
||||
if (vvm?.View is CheckoutUIPaymentMethodSettings { ExtensionPartial: { } partial })
|
||||
{
|
||||
model.ExtensionPartials.Add(partial);
|
||||
}
|
||||
}
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
model.OrderAmountFiat = OrderAmountFromInvoice(model.PaymentMethodCurrency, invoice, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
|
||||
if (storeBlob.PlaySoundOnPayment)
|
||||
{
|
||||
@ -973,6 +936,12 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = expiration.PrettyPrint();
|
||||
|
||||
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension) &&
|
||||
_handlers.TryGetValue(paymentMethodId, out var h))
|
||||
{
|
||||
extension.ModifyCheckoutModel(new CheckoutModelContext(model, store, storeBlob, invoice, Url, prompt, h));
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
@ -1008,7 +977,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (string.IsNullOrEmpty(paymentMethodId))
|
||||
paymentMethodId = implicitPaymentMethodId;
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
var model = await GetCheckoutModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
@ -1164,7 +1133,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (string.IsNullOrEmpty(model?.StoreId))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "You need to select a store before creating an invoice.";
|
||||
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["You need to select a store before creating an invoice."].Value;
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
|
||||
@ -1212,7 +1181,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Metadata), "Metadata was not valid JSON");
|
||||
ModelState.AddModelError(nameof(model.Metadata), StringLocalizer["Metadata was not valid JSON"]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1238,7 +1207,7 @@ namespace BTCPayServer.Controllers
|
||||
metadata.BuyerEmail = model.BuyerEmail;
|
||||
}
|
||||
|
||||
var result = await CreateInvoiceCoreRaw(new CreateInvoiceRequest()
|
||||
var result = await CreateInvoiceCoreRaw(new CreateInvoiceRequest
|
||||
{
|
||||
Amount = model.Amount,
|
||||
Currency = model.Currency,
|
||||
@ -1259,14 +1228,14 @@ namespace BTCPayServer.Controllers
|
||||
},
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Id} just created!";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Invoice {0} just created!", result.Id].Value;
|
||||
CreatedInvoiceId = result.Id;
|
||||
|
||||
return RedirectToAction(nameof(Invoice), new { storeId = result.StoreId, invoiceId = result.Id });
|
||||
}
|
||||
catch (BitpayHttpException ex)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = ex.Message
|
||||
|
@ -35,6 +35,7 @@ using StoreData = BTCPayServer.Data.StoreData;
|
||||
using Serilog.Filters;
|
||||
using PeterO.Numbers;
|
||||
using BTCPayServer.Payouts;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -62,14 +63,14 @@ namespace BTCPayServer.Controllers
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
|
||||
private readonly PaymentMethodViewProvider _viewProvider;
|
||||
private readonly Dictionary<PaymentMethodId, ICheckoutModelExtension> _paymentModelExtensions;
|
||||
private readonly PrettyNameProvider _prettyName;
|
||||
private readonly AppService _appService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UIInvoiceController(
|
||||
InvoiceRepository invoiceRepository,
|
||||
@ -98,8 +99,8 @@ namespace BTCPayServer.Controllers
|
||||
DefaultRulesCollection defaultRules,
|
||||
IAuthorizationService authorizationService,
|
||||
TransactionLinkProviders transactionLinkProviders,
|
||||
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
|
||||
PaymentMethodViewProvider viewProvider,
|
||||
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
|
||||
IStringLocalizer stringLocalizer,
|
||||
PrettyNameProvider prettyName)
|
||||
{
|
||||
_displayFormatter = displayFormatter;
|
||||
@ -124,12 +125,12 @@ namespace BTCPayServer.Controllers
|
||||
_authorizationService = authorizationService;
|
||||
_transactionLinkProviders = transactionLinkProviders;
|
||||
_paymentModelExtensions = paymentModelExtensions;
|
||||
_viewProvider = viewProvider;
|
||||
_prettyName = prettyName;
|
||||
_fileService = fileService;
|
||||
_uriResolver = uriResolver;
|
||||
_defaultRules = defaultRules;
|
||||
_appService = appService;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(Data.PaymentRequestData prData, decimal? amount, decimal amountDue, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
|
||||
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -23,22 +24,24 @@ namespace BTCPayServer
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly LnurlAuthService _lnurlAuthService;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UILNURLAuthController(UserManager<ApplicationUser> userManager, LnurlAuthService lnurlAuthService,
|
||||
LinkGenerator linkGenerator)
|
||||
IStringLocalizer stringLocalizer, LinkGenerator linkGenerator)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_lnurlAuthService = lnurlAuthService;
|
||||
_linkGenerator = linkGenerator;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/delete")]
|
||||
public IActionResult Remove(string id)
|
||||
{
|
||||
return View("Confirm",
|
||||
new ConfirmModel("Remove LNURL Auth link",
|
||||
"Your account will no longer have this Lightning wallet as an option for two-factor authentication.",
|
||||
"Remove"));
|
||||
new ConfirmModel(StringLocalizer["Remove LNURL Auth link"],
|
||||
StringLocalizer["Your account will no longer have this Lightning wallet as an option for two-factor authentication."],
|
||||
StringLocalizer["Remove"]));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
@ -49,7 +52,7 @@ namespace BTCPayServer
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = "LNURL Auth was removed successfully."
|
||||
Message = StringLocalizer["LNURL Auth was removed successfully."].Value
|
||||
});
|
||||
|
||||
return RedirectToList();
|
||||
@ -65,7 +68,7 @@ namespace BTCPayServer
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Html = "The Lightning node could not be registered."
|
||||
Html = StringLocalizer["The Lightning node could not be registered."].Value
|
||||
});
|
||||
|
||||
return RedirectToList();
|
||||
|
@ -19,11 +19,13 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.LNURLPay;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.Lightning;
|
||||
using BTCPayServer.Payouts;
|
||||
using BTCPayServer.Plugins;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -34,12 +36,11 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -60,11 +61,14 @@ namespace BTCPayServer
|
||||
private readonly IPluginHookService _pluginHookService;
|
||||
private readonly InvoiceActivator _invoiceActivator;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UILNURLController(InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
PayoutMethodHandlerDictionary payoutHandlers,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
PayoutProcessorService payoutProcessorService,
|
||||
StoreRepository storeRepository,
|
||||
AppService appService,
|
||||
UIInvoiceController invoiceController,
|
||||
@ -73,12 +77,14 @@ namespace BTCPayServer
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||
IPluginHookService pluginHookService,
|
||||
IStringLocalizer stringLocalizer,
|
||||
InvoiceActivator invoiceActivator)
|
||||
{
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_handlers = handlers;
|
||||
_payoutProcessorService = payoutProcessorService;
|
||||
_storeRepository = storeRepository;
|
||||
_appService = appService;
|
||||
_invoiceController = invoiceController;
|
||||
@ -88,17 +94,18 @@ namespace BTCPayServer
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_pluginHookService = pluginHookService;
|
||||
_invoiceActivator = invoiceActivator;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
||||
public Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, [FromQuery] string pr, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetLNURLForPullPayment(cryptoCode, pullPaymentId, pr, pullPaymentId, cancellationToken);
|
||||
return GetLNURLForPullPayment(cryptoCode, pullPaymentId, pr, pullPaymentId, false, cancellationToken);
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
internal async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, string k1, CancellationToken cancellationToken)
|
||||
internal async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, string k1, bool nonInteractiveOnly, CancellationToken cancellationToken)
|
||||
{
|
||||
var network = GetNetwork(cryptoCode);
|
||||
if (network is null || !network.SupportLightning)
|
||||
@ -151,6 +158,7 @@ namespace BTCPayServer
|
||||
|
||||
if (result.MinimumAmount < request.MinWithdrawable || result.MinimumAmount > request.MaxWithdrawable)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = $"Payment request was not within bounds ({request.MinWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} - {request.MaxWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} sats)" });
|
||||
|
||||
var store = await _storeRepository.FindStore(pp.StoreId);
|
||||
var pm = store!.GetPaymentMethodConfig<LightningPaymentMethodConfig>(paymentMethodId, _handlers);
|
||||
if (pm is null)
|
||||
@ -158,74 +166,94 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest
|
||||
var processors = await _payoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery()
|
||||
{
|
||||
Stores = [pp.StoreId],
|
||||
PayoutMethods = [pmi],
|
||||
Processors = [LightningAutomatedPayoutSenderFactory.ProcessorName]
|
||||
});
|
||||
var processorBlob = processors.FirstOrDefault()?.HasTypedBlob<LightningAutomatedPayoutBlob>().GetBlob();
|
||||
var instantProcessing = processorBlob?.ProcessNewPayoutsInstantly is true;
|
||||
if (nonInteractiveOnly && !instantProcessing)
|
||||
{
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment cancelled: The payer must activate the lightning automated payment process and must check \"Process approved payouts instantly\"." });
|
||||
}
|
||||
|
||||
var interval = processorBlob?.Interval.TotalMinutes;
|
||||
var autoApprove = pp.GetBlob().AutoApproveClaims;
|
||||
if (nonInteractiveOnly && !autoApprove)
|
||||
{
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment cancelled: The payer must activate \"Automatically approve claims\" in the settings of the pull payment." });
|
||||
}
|
||||
|
||||
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest
|
||||
{
|
||||
Destination = new BoltInvoiceClaimDestination(pr, result),
|
||||
PayoutMethodId = pmi,
|
||||
PullPaymentId = pullPaymentId,
|
||||
StoreId = pp.StoreId,
|
||||
Value = result.MinimumAmount.ToDecimal(unit)
|
||||
NonInteractiveOnly = nonInteractiveOnly,
|
||||
ClaimedAmount = result.MinimumAmount.ToDecimal(unit),
|
||||
});
|
||||
|
||||
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" });
|
||||
|
||||
var lightningHandler = _handlers.GetLightningHandler(network);
|
||||
switch (claimResponse.PayoutData.State)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = $"Payment request could not be paid (Claim result: {claimResponse.Result})" });
|
||||
var payout = claimResponse.PayoutData;
|
||||
DateTimeOffset since = DateTimeOffset.UtcNow;
|
||||
while (true)
|
||||
{
|
||||
case PayoutState.AwaitingPayment:
|
||||
{
|
||||
var client =
|
||||
lightningHandler.CreateLightningClient(pm);
|
||||
var payResult = await UILightningLikePayoutController.TrypayBolt(client,
|
||||
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
|
||||
claimResponse.PayoutData, result, cancellationToken);
|
||||
|
||||
switch (payResult.Result)
|
||||
{
|
||||
case PayResult.Ok:
|
||||
case PayResult.Unknown:
|
||||
await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest
|
||||
{
|
||||
PayoutId = claimResponse.PayoutData.Id,
|
||||
State = claimResponse.PayoutData.State,
|
||||
Proof = claimResponse.PayoutData.GetProofBlobJson()
|
||||
});
|
||||
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "OK",
|
||||
Reason = payResult.Message
|
||||
});
|
||||
case PayResult.CouldNotFindRoute:
|
||||
case PayResult.Error:
|
||||
default:
|
||||
await _pullPaymentHostedService.Cancel(
|
||||
new PullPaymentHostedService.CancelRequest(new[]
|
||||
{ claimResponse.PayoutData.Id }, null));
|
||||
|
||||
return BadRequest(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "ERROR",
|
||||
Reason = payResult.Message ?? payResult.Result.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
case PayoutState.AwaitingApproval:
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "OK",
|
||||
Reason =
|
||||
"The payment request has been recorded, but still needs to be approved before execution."
|
||||
});
|
||||
case PayoutState.InProgress:
|
||||
case PayoutState.Completed:
|
||||
return Ok(new LNUrlStatusResponse { Status = "OK" });
|
||||
case PayoutState.Cancelled:
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" });
|
||||
switch (payout.State)
|
||||
{
|
||||
case PayoutState.Completed:
|
||||
return Ok(new LNUrlStatusResponse { Status = "OK" });
|
||||
case PayoutState.Cancelled:
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid (Payout state: Cancelled)" });
|
||||
case PayoutState.AwaitingApproval when !autoApprove:
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "OK",
|
||||
Reason =
|
||||
"The request has been recorded, but still need to be approved before execution."
|
||||
});
|
||||
}
|
||||
if (instantProcessing)
|
||||
{
|
||||
if (DateTimeOffset.UtcNow - since > TimeSpan.FromSeconds(10.0))
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "OK",
|
||||
Reason = $"The payment is in pending state and should be completed shortly. ({payout.State})"
|
||||
});
|
||||
await WaitPayoutChanged(claimResponse.PayoutData.Id, cancellationToken);
|
||||
payout = (await _pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
||||
{
|
||||
PayoutIds = [claimResponse.PayoutData.Id]
|
||||
})).Single();
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = interval switch
|
||||
{
|
||||
double intervalMinutes => $"The payment will be sent after {intervalMinutes} minutes.",
|
||||
null => "The sender needs to send the payment manually. (Or activate the lightning automated payment processor)"
|
||||
};
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "OK",
|
||||
Reason = $"The request has been approved. {message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(request);
|
||||
private async Task WaitPayoutChanged(string payoutId, CancellationToken cancellationToken)
|
||||
{
|
||||
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
// We also wait delay, in case we missed the event
|
||||
var delay = Task.Delay(1000, cts.Token);
|
||||
var payoutEvent = _eventAggregator.WaitNext<PayoutEvent>(o => o.Payout.Id == payoutId, cts.Token);
|
||||
await Task.WhenAny(delay, payoutEvent);
|
||||
cts.Cancel();
|
||||
}
|
||||
|
||||
private BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
@ -262,7 +290,7 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ViewPointOfSaleViewModel.Item[] items;
|
||||
AppItem[] items;
|
||||
string currencyCode;
|
||||
PointOfSaleSettings posS = null;
|
||||
switch (app.AppType)
|
||||
@ -282,24 +310,19 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ViewPointOfSaleViewModel.Item item = null;
|
||||
AppItem item = null;
|
||||
if (!string.IsNullOrEmpty(itemCode))
|
||||
{
|
||||
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _);
|
||||
if (pmi is null)
|
||||
return NotFound("LNUrl or LN is disabled");
|
||||
return NotFound(StringLocalizer["LNURL or LN is disabled"]);
|
||||
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
|
||||
item = items.FirstOrDefault(item1 =>
|
||||
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (item is null ||
|
||||
item.Inventory <= 0 ||
|
||||
(item.PaymentMethods?.Any() is true &&
|
||||
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
|
||||
{
|
||||
if (item is null || item.Inventory <= 0)
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
else if (app.AppType == PointOfSaleAppType.AppType && posS?.ShowCustomAmount is not true)
|
||||
{
|
||||
@ -308,7 +331,7 @@ namespace BTCPayServer
|
||||
|
||||
var createInvoice = new CreateInvoiceRequest
|
||||
{
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? null : item?.Price,
|
||||
Amount = item?.PriceType == AppItemPriceType.Topup ? null : item?.Price,
|
||||
Currency = currencyCode,
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions
|
||||
{
|
||||
@ -322,7 +345,7 @@ namespace BTCPayServer
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
};
|
||||
|
||||
var allowOverpay = item?.PriceType is not ViewPointOfSaleViewModel.ItemPriceType.Fixed;
|
||||
var allowOverpay = item?.PriceType is not AppItemPriceType.Fixed;
|
||||
var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() };
|
||||
if (item != null)
|
||||
{
|
||||
@ -385,7 +408,7 @@ namespace BTCPayServer
|
||||
return NotFound("Unknown username");
|
||||
|
||||
LNURLPayRequest lnurlRequest;
|
||||
|
||||
|
||||
// Check core and fall back to lookup Lightning Address via plugins
|
||||
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
||||
if (lightningAddressSettings is null)
|
||||
@ -408,12 +431,13 @@ namespace BTCPayServer
|
||||
return NotFound("LNURL not available for store");
|
||||
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
lnurlRequest = new LNURLPayRequest
|
||||
lnurlRequest = new StoreLNURLPayRequest
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
|
||||
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0,
|
||||
Store = store
|
||||
};
|
||||
|
||||
var lnUrlMetadata = new Dictionary<string, string>
|
||||
@ -442,11 +466,11 @@ namespace BTCPayServer
|
||||
{
|
||||
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
||||
if (lightningAddressSettings is null || username is null)
|
||||
return NotFound("Unknown username");
|
||||
return NotFound(StringLocalizer["Unknown username"]);
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
return NotFound(StringLocalizer["Unknown username"]);
|
||||
var result = await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
@ -456,10 +480,11 @@ namespace BTCPayServer
|
||||
Currency = blob?.CurrencyCode,
|
||||
Metadata = blob?.InvoiceMetadata
|
||||
},
|
||||
new LNURLPayRequest
|
||||
new StoreLNURLPayRequest
|
||||
{
|
||||
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
|
||||
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
|
||||
Store = store,
|
||||
},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
@ -488,7 +513,7 @@ namespace BTCPayServer
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
if (!blob.AnyoneCanInvoice)
|
||||
return NotFound("'Anyone can invoice' is turned off");
|
||||
return NotFound(StringLocalizer["'Anyone can invoice' is turned off"]);
|
||||
var metadata = new InvoiceMetadata();
|
||||
if (!string.IsNullOrEmpty(orderId))
|
||||
{
|
||||
@ -518,7 +543,7 @@ namespace BTCPayServer
|
||||
{
|
||||
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _);
|
||||
if (pmi is null)
|
||||
return NotFound("LNUrl or LN is disabled");
|
||||
return NotFound(StringLocalizer["LNURL or LN is disabled"]);
|
||||
|
||||
InvoiceEntity i;
|
||||
try
|
||||
@ -550,7 +575,7 @@ namespace BTCPayServer
|
||||
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out var lnUrlMethod);
|
||||
if (pmi is null)
|
||||
return null;
|
||||
lnurlRequest ??= new LNURLPayRequest();
|
||||
lnurlRequest ??= new StoreLNURLPayRequest{Store = store};
|
||||
lnUrlMetadata ??= new Dictionary<string, string>();
|
||||
|
||||
var pm = i.GetPaymentPrompt(pmi);
|
||||
@ -706,7 +731,7 @@ namespace BTCPayServer
|
||||
new LNURLPayRequest.LNURLPayRequestCallbackResponse.LNURLPayRequestSuccessActionUrl
|
||||
{
|
||||
Tag = "url",
|
||||
Description = "Thank you for your purchase. Here is your receipt",
|
||||
Description = StringLocalizer["Thank you for your purchase. Here is your receipt"],
|
||||
Url = _linkGenerator.GetUriByAction(
|
||||
nameof(UIInvoiceController.InvoiceReceipt),
|
||||
"UIInvoice",
|
||||
@ -818,7 +843,7 @@ namespace BTCPayServer
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = "LNURL is required for lightning addresses but has not yet been enabled.",
|
||||
Message = StringLocalizer["LNURL is required for lightning addresses but has not yet been enabled."].Value,
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId });
|
||||
@ -858,7 +883,7 @@ namespace BTCPayServer
|
||||
if (!string.IsNullOrEmpty(vm.Add.CurrencyCode) &&
|
||||
currencyNameTable.GetCurrencyData(vm.Add.CurrencyCode, false) is null)
|
||||
{
|
||||
vm.AddModelError(addressVm => addressVm.Add.CurrencyCode, "Currency is invalid", this);
|
||||
vm.AddModelError(addressVm => addressVm.Add.CurrencyCode, StringLocalizer["Currency is invalid"], this);
|
||||
}
|
||||
|
||||
JObject metadata = null;
|
||||
@ -870,7 +895,7 @@ namespace BTCPayServer
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
vm.AddModelError(addressVm => addressVm.Add.InvoiceMetadata, "Metadata must be a valid json object", this);
|
||||
vm.AddModelError(addressVm => addressVm.Add.InvoiceMetadata, StringLocalizer["Metadata must be a valid JSON object"], this);
|
||||
}
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
@ -894,12 +919,12 @@ namespace BTCPayServer
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Lightning address added successfully."
|
||||
Message = StringLocalizer["Lightning address added successfully."].Value
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.AddModelError(addressVm => addressVm.Add.Username, "Username is already taken", this);
|
||||
vm.AddModelError(addressVm => addressVm.Add.Username, StringLocalizer["Username is already taken"], this);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -917,13 +942,13 @@ namespace BTCPayServer
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = $"Lightning address {index} removed successfully."
|
||||
Message = StringLocalizer["Lightning address {0} removed successfully.", index].Value
|
||||
});
|
||||
return RedirectToAction("EditLightningAddress");
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.AddModelError(addressVm => addressVm.Add.Username, "Username could not be removed", this);
|
||||
vm.AddModelError(addressVm => addressVm.Add.Username, StringLocalizer["Username could not be removed"], this);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
|
@ -58,10 +58,10 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
await _apiKeyRepository.Remove(id, _userManager.GetUserId(User));
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "API Key removed"
|
||||
Message = StringLocalizer["API Key removed"].Value
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
@ -71,10 +71,10 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure(HttpContext))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot generate api keys while not on https or tor"
|
||||
Message = StringLocalizer["Cannot generate API keys while not using HTTPS or Tor"].Value
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
@ -91,7 +91,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot generate API keys while not on https or using Tor"
|
||||
Message = StringLocalizer["Cannot generate API keys while not using HTTPS or Tor"].Value
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
@ -199,7 +199,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code class='alert-link'>{key.Id}</code>"
|
||||
Html = StringLocalizer["API key generated!"].Value + $" <code class='alert-link'>{key.Id}</code>"
|
||||
});
|
||||
|
||||
return RedirectToAction("APIKeys", new { key = key.Id });
|
||||
@ -242,7 +242,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code class='alert-link'>{key.Id}</code>"
|
||||
Html = StringLocalizer["API key generated!"].Value + $" <code class='alert-link'>{key.Id}</code>"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
@ -56,7 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
await _userManager.UpdateAsync(user);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = "Updated successfully.",
|
||||
Message = StringLocalizer["Updated successfully."].Value,
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
return RedirectToAction("NotificationSettings");
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user