Compare commits

..

79 Commits

Author SHA1 Message Date
8a5a160645 bump 2024-11-04 13:12:35 +09:00
5cbadc09f9 Changelog 2.0.1 2024-11-04 13:08:57 +09:00
7aa87d397e Fix: Wrong manifest downloaded when installing plugin on old btcpay (Fix #6344) (#6354) 2024-11-04 13:05:10 +09:00
693eceb80f Reolve pull payment timezone (#6348) 2024-11-01 08:28:43 +09:00
7d8fc14159 fix: save proof blob if payout is in progress (#6343)
the payout cant be tracked later otherwise and will be marked as
cancelled
2024-11-01 08:24:21 +09:00
4687bb95cb Fix: Incorrect percentage accounting of raised money in crowdfunding (#6347) 2024-11-01 08:23:10 +09:00
e3ec07da76 Fix: Crowdfund page was crashing from 2.0.0 (#6342) (#6346) 2024-10-31 23:42:18 +09:00
910801d305 Replace font-awesome icon on Policies page 2024-10-31 12:23:30 +01:00
5ad0b128aa Dummy commit 2024-10-30 23:39:11 +09:00
5cbeea4fb3 Changelog 2.0 (#6313)
* Changelog 2.0

* Update Changelog.md

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2024-10-30 15:35:00 +01:00
a6e18736d6 Keypad updates (#6338)
* Add keypad icons

Closes #6195.

* Keypad JS fixes
2024-10-29 23:44:37 +09:00
373b90e3b5 Liquid fixes (#6340)
make sure link provider is per payment method of liquid assets. Also remove ETB as it has been unused. Also hide the send button as it is not supported thrrough BTCPay
2024-10-29 23:43:37 +09:00
92f9b226fe Prevent additional concurrency issues with LightnignPendingPayoutListener 2024-10-28 22:12:29 +09:00
0ac6553840 Add download icon 2024-10-28 08:25:30 +01:00
41a2241ae1 feat: log download button (#6330)
* feat: add download button to logs view

* fix: add using block for `fileStream` if it isnt downloaded
2024-10-27 21:43:47 +09:00
9bb1a5b80a Prevent concurrency race on lightning payout update 2024-10-27 19:55:30 +09:00
0e59107eee Fix tests with LightningPendingPayoutListener overriding automated payouts state changes 2024-10-27 19:34:20 +09:00
c9fe68b812 fix: pass current offset to log route (#6329)
the current offset is lost otherwise and will cause a 404 if it was
greater than 0
2024-10-27 19:12:39 +09:00
e7b9688602 refactor: make BitcoinCheckoutModelExtension support other payment handlers (#6311)
* refactor: make `BitcoinCheckoutModelExtension` support other payment handlers

The bitcoin checkout extension doesn't have to be tied to the native
bitcoin handler since it only really needs the payment details to be in
a specific format, which can be provided by other handlers aswell,
allowing for better code reuse.

* refactor: initialize payment methods in constructor
2024-10-25 22:50:46 +09:00
a962e60de9 More Translations (#6318)
* Store selector

* Footer

* Notifications

* Checkout Appearance

* Users list

* Forms

* Emails

* Pay Button

* Edit Dictionary

* Remove newlines, fix typos

* Forms

* Pull payments and payouts

* Various pages

* Use local docs link

* Fix

* Even more translations

* Fixes #6325

* Account pages

* Notifications

* Placeholders

* Various pages and components

* Add more
2024-10-25 22:48:53 +09:00
e5611f9165 Fix tests (#6333) 2024-10-25 22:23:27 +09:00
540ad13265 Paging improvements (#6332)
* Domain Mapping: Passthrough query params when redirecting

* Clean up Pager

* Use current URL when paging

* Refactor
2024-10-25 22:23:03 +09:00
2849426092 Checkout: Allow breaking long item description texts 2024-10-25 13:11:26 +02:00
c4a2b4e975 Merge pull request #6327 from btcpayserver/bugfix/vaulticons
Properly cleaning up old feedback in vault feedback items
2024-10-24 15:33:44 -05:00
d508f5dc09 Properly cleaning up old feedback in vault feedback items 2024-10-22 21:50:22 -05:00
81ce8b0469 Fix flaky test 2024-10-23 00:17:02 +09:00
5a3a661e91 Revert "Revert "Fix flaky test""
This reverts commit bb5c6bd68d15bab091e3835c2d51687da8a55def.
2024-10-22 23:44:16 +09:00
bb5c6bd68d Revert "Fix flaky test"
This reverts commit 9dfabeab52644ba863230be39eca7496f4377365.
2024-10-22 23:41:41 +09:00
9dfabeab52 Fix flaky test 2024-10-22 23:36:12 +09:00
ad07330bf1 Update CLN support to 24.08.2 (#6323) 2024-10-21 00:25:50 +09:00
74011e50e3 Do not translate checkout with the backend language 2024-10-20 11:49:36 +09:00
3dfdbf544a Automated processor get disabled after some repeated failures (#6320) 2024-10-20 00:08:28 +09:00
4bf0b79c2a Simple rename 2024-10-19 22:07:20 +09:00
cc0ea0b3f8 Refactor payouts processing (#6314) 2024-10-19 21:33:34 +09:00
62d765125d Toggle color fix 2024-10-18 15:14:30 +02:00
b5b45d9a27 Rename Transaction->Translation 2024-10-18 16:06:51 +09:00
8e098710c1 Require non interactivity for boltcard payments (#6289) 2024-10-18 14:09:41 +09:00
6dfb369b55 UI: Inactive toggle hover color fix (#6299) 2024-10-18 14:08:34 +09:00
817522ff97 refactor(checkout): displayed payment methods vue component (#6316)
The displayed payment methods can change with updates aswell, so it
should be rendered as a vue component instead
2024-10-18 14:05:00 +09:00
8b5b90d247 refactor: make BeforeFetchingRates function public (#6310)
This function is useful when a payment method wants to update its
payment prompt after receiving a partial payment, like ln does
2024-10-18 14:03:37 +09:00
b670097592 feat: provide store info to modify-lnurlp-request filter (#6312)
adds store data to the filter using a new `StoreLNURLPayRequest` class
which simply adds a `Store` member.

closes: https://github.com/btcpayserver/btcpayserver/issues/6301
2024-10-18 14:03:07 +09:00
7b6a115adc [Greenfield] Select default payoutMethodId if none are selected in the Refund route (#6315) 2024-10-17 22:54:59 +09:00
77fba4aee3 Add more translations (#6302)
* Newlines

* Dashboard

* Add more translations

* Moar

* Remove &nbsp; from translated texts

* Dictionary controller translations

* Batch 1 of controller updates

* Batch 2 of controller updates

* Component translations

* Batch 3 of controller updates

* Fixes
2024-10-17 22:51:40 +09:00
7e1712c8cd Merge pull request #6309 from NicolasDorier/fixmonero
Fix monero payments
2024-10-17 20:40:05 +09:00
b7affb1d34 Fix monero payments 2024-10-17 18:55:00 +09:00
d7fd90c4c3 Bump client lib 2024-10-17 16:41:36 +09:00
1d94782463 Merge pull request #6305 from NicolasDorier/fixelements
Fix elements payments
2024-10-16 22:34:32 +09:00
c7a05c3f09 Fix elements payments 2024-10-16 22:34:17 +09:00
2dc58a82b7 Item editor: Do not use only known props for diff (#6307)
Brooke the file seller plugin which adds a property and this did not detect the item change
2024-10-16 14:25:06 +02:00
755dbbab00 fix server nav ui extensin (#6306) 2024-10-16 14:24:56 +02:00
b470fe22f1 Remove potential NRE 2024-10-16 17:04:27 +09:00
5b2560ddf7 Merge pull request #6304 from NicolasDorier/cleanupetw
Cleanup useless code
2024-10-16 16:38:15 +09:00
be429c527c Cleanup useless code 2024-10-16 16:25:16 +09:00
65fd537200 Merge pull request #6303 from btcpayserver/feat/lnd-0.18.3
Bumping LND to 0.18.3-beta
2024-10-15 18:49:41 -05:00
6e43c7f06f Bumping LND to 0.18.3-beta 2024-10-15 18:21:26 -05:00
5867b5c000 Merge pull request #6297 from NicolasDorier/fioqnt
Pretty names of payment methods isn't provided by CheckoutExtensions
2024-10-15 23:28:42 +09:00
05887cf8b0 Fix potential stack overflow 2024-10-15 23:11:28 +09:00
c43721d489 Pretty names of payment methods isn't provided by CheckoutExtensions 2024-10-14 21:53:14 +09:00
0bf75d52d7 Merge pull request #6292 from NicolasDorier/addtranslations
Add translations to the Dashboard and more
2024-10-14 19:45:06 +09:00
c35af2dc69 Add translations to the Dashboard 2024-10-14 19:19:56 +09:00
73a9835a27 Fix build warning 2024-10-13 00:17:49 +09:00
87ab15f754 Fix crash on Monero/ZCash on invoices list 2024-10-13 00:10:49 +09:00
6bc608c081 Merge pull request #6287 from btcpayserver/feat/plugin-search
Support for searching plugins by name
2024-10-11 08:05:27 -05:00
cbea1d8691 Merge pull request #6291 from NicolasDorier/markedfordeletion
Improve UX for uninstalling disabled plugins
2024-10-11 21:41:01 +09:00
58f21a69aa Improve UX for uninstalling disabled plugins 2024-10-11 19:35:37 +09:00
426c5b9a24 Merge pull request #6290 from NicolasDorier/plugincrashdetect
Disable plugins crashing at startup
2024-10-11 10:56:46 +09:00
511e90efd1 Disable plugins crashing at startup 2024-10-11 10:50:49 +09:00
bc7b856654 Start sending BTCPay version string to help with filtering on plugin-builder side 2024-10-10 05:46:07 -05:00
d50d2f9ca0 Merge pull request #6288 from NicolasDorier/bettershowwarnings
Show warnings if NFC payment isn't complete
2024-10-10 19:23:42 +09:00
1b53defab3 Show warnings if NFC payment isn't complete 2024-10-10 19:16:09 +09:00
3e612921f3 Remove flaky test 2024-10-10 17:50:18 +09:00
ec51d43490 Improve error message if LNWithdraw fails 2024-10-10 17:24:19 +09:00
80dc5028f7 Fix migration crashes for instance having monero, zcash 2024-10-10 11:13:23 +09:00
2329c4a75f Support for searching plugins by name 2024-10-09 06:47:11 -05:00
ae76cc1ca2 Small UI improvements in the payout processors 2024-10-09 17:44:19 +09:00
e4f79f046a Remove unused field from automated payout settings 2024-10-09 13:13:10 +09:00
622d837ea1 Merge pull request #6286 from NicolasDorier/efwoiiqnf
Prevent double BOLT11 payment with LNUrlWithdraw
2024-10-09 13:10:17 +09:00
c0aa9a8bd4 Prevent double BOLT11 payment with LNUrlWithdraw 2024-10-09 13:10:04 +09:00
9b1052f023 Remove useless code 2024-10-08 21:25:37 +09:00
371 changed files with 5667 additions and 3661 deletions

View File

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

View File

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

View File

@ -16,7 +16,7 @@
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
<Version Condition=" '$(Version)' == '' ">2.0.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>

View File

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

View File

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

View File

@ -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)
};
}
@ -154,7 +155,7 @@ namespace BTCPayServer.Data
{
return paymentType switch
{
"BTCLike" => $"{cryptoCode}-CHAIN",
"BTCLike" or "MoneroLike" or "ZcashLike" => $"{cryptoCode}-CHAIN",
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
"LNURLPAY" => $"{cryptoCode}-LNURL",
_ => paymentMethodId

View File

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

View File

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

View File

@ -560,7 +560,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("numeric");
b.Property<string>("Blob")
.HasColumnType("JSONB");
.HasColumnType("jsonb");
b.Property<string>("Currency")
.HasColumnType("text");

View File

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

View File

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

View File

@ -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;
@ -2980,9 +2981,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);
@ -3046,7 +3048,7 @@ namespace BTCPayServer.Tests
new CreateInvoiceRequest
{
Currency = "USD",
Amount = 100,
Amount = 0.1m,
Checkout = new CreateInvoiceRequest.CheckoutOptions
{
PaymentMethods = new[] { "BTC-LN" },
@ -4171,13 +4173,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 +4207,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 +4250,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)]

View File

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

View File

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

View File

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

View File

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

1 Id Blob Created ExceptionStatus Status StoreDataId Archived Blob2
2 Q7RqoHLngK9svM4MgRyi9y \x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000 \x1f8b080000000000000ac454cb76da400cfd97595302b1c1981d6012124c0e8f104e12b2f043b627d833663c767073f2655df493fa0bd50049714fbae9262b1f4ba3ab2be94abf7efc7c25d4275d3233e65b3eb2593836b362a24fc27949cd92d44826b9802bf544cbc2e5602097fdb0bde8d8d71be1fa416fe566d7936dbfbcb08362b17dbe6cefc6fdd5d0c7402e7c102a90e5718c3829803fe531f54ad26dd4887024906ebb65b6eb9a6e187adbd0b4965e2394159c7a704b1374375b5a4733b596765e23b04b290651ce4e7dbaae7c3ea43ca3b2e7fb02b24c71ed9b634b338ca56d19c3fb6075afcd4661b0ea5ce8fd17695ba14d91612ab89f7bf28a055c247b64d2c57648482cc83c44710104be539601f7e1bd94342a33ea3931e9064e9c810242c6a46bd4b12e2f1702982a922c171679ab11372fb111d5247bdb8d937c60ee0dc3c4a171c532e03993a2acd81e685af95fc87d2b4fa3a8ac861c1b73fe99b159314e23ce8e60481dbb6a39d2797f8103a5c5be868554f30b5595bb3477db56cf0da3209f0f56edd95668e360613dd38dbe192e69d1db049177399a6cee86a0bb0fdb967637fb6e981b5d1ff2596b62a5f6f578dc9b8e5bc5dd5d7bba3396cdb966c28cbacc8dac6c65e6c35973ec47e6c0729370feed313dcfa227f2091b0a6af4af6bd2bf1dac4977fd45d4d6e46dbf378ecc151f062ff80b3b0fd203d783fd2825a74c8049fc7f7cc29d802067bee3c6f021ad836972a20b013e15e0c9e5dc46f448ca34eb9e9d254e264130542926de5016d63d9e605eb9bb00b52c0d946680f1375cd200c5aba8605629724cc24e8c7bd8e3ca8228402c455cc943190359f721e175577a58c0e1599d8b10f379a24c253f88e6550d427dfeb5ebc7ea2720238e87e215151700ccf7af9b55eeffb3e108677f3103c44b38a378437124c38f0bf67ebd3a0d75bd22aa8eacba284770a5e3c3089c0227af0471f48c9c2cfae3859d04e683ffd7508fd281e2a0ac8ad26e790cc261ea5c35eb8db7df000000ffff0300d1083b8e01060000 2018-10-01 11:32:12+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
3 Ka6GHBrFJPRwFRga1RD6Yz \x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000 2018-10-01 11:54:10+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
4 Q3kZ3F8cUD57WUqcc8QLs2 \x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000 2018-10-01 11:54:32+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
5 FSktP1Nrxu7arh7TUFAgTZ \x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000 2018-10-01 11:57:15+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f

View File

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

1 Id Blob InvoiceDataId Accounted Blob2 Type
9 afc39884e024cbb3a48ca997b7e878b199d85fe97f90f94471364cc866ba83ad-1 \x1f8b080000000000000374904d6ac3301085f73945d03a14c992f593655cb20aa5942cbd1949a362d258c191534cc8c9bae8917a854a2e0ddd7476ef3de6d3e87d7d7c5e17cb25b9e070ee624fd64bb62a7a4087dd05fdbe3b6231eb9ad5a21246ce698fe93d0e872d968c3ed032d59cc4319d62d7a7ec13f09a5b90d269e1b8544c88600c55014d1dbc36c632a5515b651418ed4058ee6c25906a91258740d90ca6e44e1e672eb5402b47efc3f23e13351a270cc7bc2aa407164ce0ca04e1a9ac38b34885e65c4aabd50f0f9c8b639fd067641a469c4d374ca7149be8cbc7c866df903ff6334c47ecd3232428e9b5254dec43371c21e5ea9a426bc95ae59e562d79d96cb398c12d79baf795bddc57b676f80a6eca32c0db196fffbcb39f4ebf97ecba0392c5ed1b0000ffff030018cc54a1b1010000 Q7RqoHLngK9svM4MgRyi9y t
10 6f2b8513ebfdb14b41d4de235f050cd028400cb01fde700d72c6bedc2ad8af1e-5 \x1f8b08000000000000037490bf6ac3301087f73c45d01cc249966425635c3285528a472ffa732a26b5151c39c5843c59873e525fa192434397de22ddef83efa4fbfefcba2e964b72c1e1dc869e6c977495fb012db6177475db610e85908a6d24c04c7b8c1f6138ee313358432e369330c65368fb987242517ba79876d6a0952503578243ea0d80e5c0944b2708e159e1d0714eb931d43b34b4508219e941cc62200ff378f752c719c2a368a937942b445158855e6967b8b05c0a2e9934c0b59585960c0b55d272a3cabb4f5b1bc63ea24b4aafdfcf38a776984e3154c1e59f915d5d913ff18b9e3aece3938e3ad36b43aad0fb76e8744cbbabb2ae215b5835e475b74fb7388c989ae7c7b6325d677ec0376da7d4ce936fff0ca9a7d3ef330eed11c9e2f6030000ffff0300b09cf40daf010000 Q7RqoHLngK9svM4MgRyi9y f
11 3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61 \x1f8b08000000000000037490bd6ec3201485f73c45c41c5580f973c6b8ca14555595d10be65e222b8d891c9cca8af2641dfa487d8582ab585d7a27ce39f001e7fbf3ebb6582ec915fb4b1b3ab25eb255d63d3a6caf08fbf684d994525121a85653da61fc08fd718b39a34f340f9f9230c47368bb987c22256fb4f09e7bc79d914a000aea8c6bd083c1461bcfd2524241593a5f3a250aef8d2991c952a9c21630812999c9c3c4d55615cad37998b625131eb8e5c080496c941525958c97b2708c838286022ac5b56060f42fcf3a17862e222464ec079c4cd78fe718aa00f96364b3afc81ffbd58e27ece2b38d36a7b79a54a1f36d7fb2315557655a4dd64c30b1aac9db669b84b7ef174cea652e2c99a9b064edf060ddf8d873ffe7a2fd787e3c65d71e912cee3f000000ffff0300a991d8c4b2010000 Q7RqoHLngK9svM4MgRyi9y t
12 26c879f3d27a894a62f8730c84205ac9dec38b7bbc0a11ccc0c196d1259b25aa-1 \x1f8b08000000000000036c90cf6ed4301087dfc5e7086c27f19fdccaa2b0b4dbaa452b964ab94cec314d378943e204d26a9f8c038fc42be02eea01099f3c9faddfcc7cbf7ffe7a260b8e53e37b52b0848c68b059d0ee9b0eafa78804cfa9d69aa954c884f418befbf158229282bea109f173187cd3075210009eeb9ae796696198a1c63046a136b256b5492d1a9d03a72aa346a64e099e81d2122c77a9d4467141193d1ff237758e99fddcb6090163fcdc07b4a408e38c0931e33a04bff1364e4176eff61bf2ca6e61edb00fef21407c7aae88f1bd6bc60e425c70f3925291422415196b176f0eda096305d68e384d9154e4f3e5ae94878b61f1eeca680fdf862ffb3be5ecc5aacb9b0f87fce89e9aedfd63d64cf869fbb46c1fe67ab854dd74d5fd58efef4ad61c9c6bacce1eafa1bbc1beac486c70c4f516c2c3b9017d1b3da567bc403b6384715098260c1fedf98770944b8d1a2dcd585427a347ed7269a9a15c73953191096ab25a720e9866b904c6104daa98c9b9b0ff647325b253ac5bfc0a667d5dfaf43f65fb7578311a85ee9a2392d31f000000ffff03004a65a9b61e020000 Q7RqoHLngK9svM4MgRyi9y t

View File

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

View File

@ -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)]
@ -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)
@ -3246,7 +3251,7 @@ namespace BTCPayServer.Tests
report = await GetReport(acc, new() { ViewName = "Payments", TimePeriod = new TimePeriod() { From = date2018, To = date2018 + TimeSpan.FromDays(365) } });
var invoiceIdIndex = report.GetIndex("InvoiceId");
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
var addr = await tester.ExplorerNode.GetNewAddressAsync();
// Two invoices get refunded

View File

@ -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,25 +360,17 @@ 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);
@ -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>();

View File

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

View File

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

View File

@ -41,7 +41,7 @@
<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.6" />
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Fido2" Version="2.0.2" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Mvc;

View File

@ -5,7 +5,7 @@
}
<div id="StoreLightningBalance-@Model.Store.Id" 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 +29,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 +41,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 +52,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 +63,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 +74,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 +87,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 +98,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 +109,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 +120,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>
}
@ -132,7 +132,7 @@
{
<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>
<span class="ms-1" text-translate="true">Details</span>
</button>
}
}
@ -140,7 +140,7 @@
{
<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>

View File

@ -4,14 +4,15 @@
{
<div id="StoreLightningServices-@Model.Store.Id" 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"
target="_blank"
id="PublicNodeInfo">
id="PublicNodeInfo"
text-translate="true">
Node Info
</a>
</header>

View File

@ -6,7 +6,7 @@
{
<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>
@ -24,7 +24,7 @@
{
<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>
@ -34,14 +34,14 @@
</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.Store.Id" 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>

View File

@ -6,17 +6,17 @@
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
<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.Store.Id" 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>
@ -36,10 +36,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 +65,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.Store.Id" class="fw-semibold" text-translate="true">
Create Invoice
</a>
}

View File

@ -4,17 +4,17 @@
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
<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>
@ -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>
}

View File

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

View File

@ -4,7 +4,7 @@
@inject BTCPayNetworkProvider NetworkProvider
<div id="StoreWalletBalance-@Model.Store.Id" 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">
@ -39,7 +39,7 @@
{
<div class="ct-chart"></div>
}
else if (Model.Store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(Model.CryptoCode)) is null)
else if (Model.Store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(Model.CryptoCode)) is null)
{
<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>.

View File

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

View File

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

View File

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

View File

@ -407,23 +407,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 +462,7 @@ namespace BTCPayServer.Controllers.Greenfield
PullPaymentBlob? ppBlob = null;
string? ppCurrency = null;
if (request?.PullPaymentId is not null)
{
@ -464,6 +471,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 +480,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)

View File

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

View File

@ -430,7 +430,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);

View File

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

View File

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

View File

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

View File

@ -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
{
@ -294,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);
}
@ -306,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"),
@ -344,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);
}
@ -353,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);
}
@ -377,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);
@ -390,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);
}
@ -413,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
@ -423,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)
{
@ -457,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)
{
@ -474,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)
{
@ -500,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);
}
@ -510,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;
@ -608,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 });
}
@ -626,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");
@ -657,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
{
@ -758,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)
@ -877,7 +856,7 @@ namespace BTCPayServer.Controllers
var model = new CheckoutModel
{
Activated = prompt.Activated,
PaymentMethodName = _prettyName.PrettyName(paymentMethodId),
PaymentMethodName = _prettyName.PrettyName(paymentMethodId, true),
PaymentMethodCurrency = prompt.Currency,
RootPath = Request.PathBase.Value.WithTrailingSlash(),
OrderId = orderId,
@ -929,7 +908,7 @@ namespace BTCPayServer.Controllers
{
Displayed = displayedPaymentMethods.Contains(kv.PaymentMethodId),
PaymentMethodId = kv.PaymentMethodId,
PaymentMethodName = _prettyName.PrettyName(kv.PaymentMethodId),
PaymentMethodName = _prettyName.PrettyName(kv.PaymentMethodId, true),
Order = kv.PaymentMethodId switch
{
_ when PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 0,
@ -1154,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");
}
@ -1202,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"]);
}
}
@ -1228,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,
@ -1249,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

View File

@ -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
{
@ -69,6 +70,7 @@ namespace BTCPayServer.Controllers
private readonly UriResolver _uriResolver;
public WebhookSender WebhookNotificationManager { get; }
public IStringLocalizer StringLocalizer { get; }
public UIInvoiceController(
InvoiceRepository invoiceRepository,
@ -98,6 +100,7 @@ namespace BTCPayServer.Controllers
IAuthorizationService authorizationService,
TransactionLinkProviders transactionLinkProviders,
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
IStringLocalizer stringLocalizer,
PrettyNameProvider prettyName)
{
_displayFormatter = displayFormatter;
@ -127,6 +130,7 @@ namespace BTCPayServer.Controllers
_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)

View File

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

View File

@ -19,6 +19,9 @@ 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;
@ -34,12 +37,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 +62,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 +78,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 +95,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 +159,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 +167,93 @@ 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)
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)
@ -287,7 +315,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"]);
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
item = items.FirstOrDefault(item1 =>
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
@ -385,7 +413,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 +436,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 +471,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 +485,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 +518,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 +548,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 +580,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 +736,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 +848,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 +888,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 +900,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 +924,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 +947,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)
{

View File

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

View File

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

View File

@ -7,25 +7,21 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Fido2;
using BTCPayServer.Models;
using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using MimeKit;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewProfile)]
[Route("account/{action:lowercase=Index}")]
public partial class UIManageController : Controller
@ -45,6 +41,7 @@ namespace BTCPayServer.Controllers
private readonly UriResolver _uriResolver;
private readonly IFileService _fileService;
readonly StoreRepository _StoreRepository;
public IStringLocalizer StringLocalizer { get; }
public UIManageController(
UserManager<ApplicationUser> userManager,
@ -61,6 +58,7 @@ namespace BTCPayServer.Controllers
UserService userService,
UriResolver uriResolver,
IFileService fileService,
IStringLocalizer stringLocalizer,
IHtmlHelper htmlHelper
)
{
@ -79,6 +77,7 @@ namespace BTCPayServer.Controllers
_uriResolver = uriResolver;
_fileService = fileService;
_StoreRepository = storeRepository;
StringLocalizer = stringLocalizer;
}
[HttpGet]
@ -135,7 +134,7 @@ namespace BTCPayServer.Controllers
{
if (!(await _userManager.FindByEmailAsync(model.Email) is null))
{
TempData[WellKnownTempData.ErrorMessage] = "The email address is already in use with an other account.";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["The email address is already in use with an other account."].Value;
return RedirectToAction(nameof(Index));
}
var setUserResult = await _userManager.SetUserNameAsync(user, model.Email);
@ -207,11 +206,11 @@ namespace BTCPayServer.Controllers
if (needUpdate is true)
{
needUpdate = await _userManager.UpdateAsync(user) is { Succeeded: true };
TempData[WellKnownTempData.SuccessMessage] = "Your profile has been updated";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Your profile has been updated"].Value;
}
else
{
TempData[WellKnownTempData.ErrorMessage] = "Error updating profile";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Error updating profile"].Value;
}
return RedirectToAction(nameof(Index));
@ -235,7 +234,7 @@ namespace BTCPayServer.Controllers
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent. Please check your email.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value;
return RedirectToAction(nameof(Index));
}
@ -281,8 +280,8 @@ namespace BTCPayServer.Controllers
}
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User changed their password successfully.");
TempData[WellKnownTempData.SuccessMessage] = "Your password has been changed.";
_logger.LogInformation("User changed their password successfully");
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Your password has been changed."].Value;
return RedirectToAction(nameof(ChangePassword));
}
@ -330,7 +329,7 @@ namespace BTCPayServer.Controllers
}
await _signInManager.SignInAsync(user, isPersistent: false);
TempData[WellKnownTempData.SuccessMessage] = "Your password has been set.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Your password has been set."].Value;
return RedirectToAction(nameof(SetPassword));
}
@ -345,7 +344,7 @@ namespace BTCPayServer.Controllers
}
await _userService.DeleteUserAndAssociatedData(user);
TempData[WellKnownTempData.SuccessMessage] = "Account successfully deleted.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account successfully deleted."].Value;
await _signInManager.SignOutAsync();
return RedirectToAction(nameof(UIAccountController.Login), "UIAccount");
}

View File

@ -23,6 +23,7 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
using StoreData = BTCPayServer.Data.StoreData;
@ -47,6 +48,7 @@ namespace BTCPayServer.Controllers
private FormComponentProviders FormProviders { get; }
public FormDataService FormDataService { get; }
public IStringLocalizer StringLocalizer { get; }
public UIPaymentRequestController(
UIInvoiceController invoiceController,
@ -62,6 +64,7 @@ namespace BTCPayServer.Controllers
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders,
FormDataService formDataService,
IStringLocalizer stringLocalizer,
BTCPayNetworkProvider networkProvider)
{
_InvoiceController = invoiceController;
@ -78,6 +81,7 @@ namespace BTCPayServer.Controllers
FormProviders = formProviders;
FormDataService = formDataService;
_networkProvider = networkProvider;
StringLocalizer = stringLocalizer;
}
[HttpGet("/stores/{storeId}/payment-requests")]
@ -169,7 +173,7 @@ namespace BTCPayServer.Controllers
if (paymentRequest?.Archived is true && viewModel.Archived)
{
ModelState.AddModelError(string.Empty, "You cannot edit an archived payment request.");
ModelState.AddModelError(string.Empty, StringLocalizer["You cannot edit an archived payment request."]);
}
var data = paymentRequest ?? new PaymentRequestData();
data.StoreDataId = viewModel.StoreId;
@ -180,7 +184,7 @@ namespace BTCPayServer.Controllers
{
var prInvoices = (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
if (prInvoices.Any())
ModelState.AddModelError(nameof(viewModel.Amount), "Amount and currency are not editable once payment request has invoices");
ModelState.AddModelError(nameof(viewModel.Amount), StringLocalizer["Amount and currency are not editable once payment request has invoices"]);
}
if (!ModelState.IsValid)
@ -210,7 +214,9 @@ namespace BTCPayServer.Controllers
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
_EventAggregator.Publish(new PaymentRequestUpdated { Data = data, PaymentRequestId = data.Id, });
TempData[WellKnownTempData.SuccessMessage] = $"Payment request \"{viewModel.Title}\" {(isNewPaymentRequest ? "created" : "updated")} successfully";
TempData[WellKnownTempData.SuccessMessage] = isNewPaymentRequest
? StringLocalizer["Payment request \"{0}\" created successfully", viewModel.Title].Value
: StringLocalizer["Payment request \"{0}\" updated successfully", viewModel.Title].Value;
return RedirectToAction(nameof(GetPaymentRequests), new { storeId = store.Id, payReqId = data.Id });
}
@ -302,7 +308,7 @@ namespace BTCPayServer.Controllers
{
if (amount.HasValue && amount.Value <= 0)
{
return BadRequest("Please provide an amount greater than 0");
return BadRequest(StringLocalizer["Please provide an amount greater than 0"]);
}
var result = await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId());
@ -318,7 +324,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction("ViewPaymentRequest", new { payReqId });
}
return BadRequest("Payment Request cannot be paid as it has been archived");
return BadRequest(StringLocalizer["Payment Request cannot be paid as it has been archived"]);
}
if (!result.FormSubmitted && !string.IsNullOrEmpty(result.FormId))
{
@ -337,7 +343,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction("ViewPaymentRequest", new { payReqId });
}
return BadRequest("Payment Request has already been settled.");
return BadRequest(StringLocalizer["Payment Request has already been settled."]);
}
if (result.ExpiryDate.HasValue && DateTime.UtcNow >= result.ExpiryDate)
@ -347,7 +353,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction("ViewPaymentRequest", new { payReqId });
}
return BadRequest("Payment Request has expired");
return BadRequest(StringLocalizer["Payment Request has expired"]);
}
var currentInvoice = result.Invoices.GetReusableInvoice(amount);
@ -391,7 +397,7 @@ namespace BTCPayServer.Controllers
if (!result.AllowCustomPaymentAmounts)
{
return BadRequest("Not allowed to cancel this invoice");
return BadRequest(StringLocalizer["Not allowed to cancel this invoice"]);
}
var invoices = result.Invoices.Where(requestInvoice =>
@ -399,7 +405,7 @@ namespace BTCPayServer.Controllers
if (!invoices.Any())
{
return BadRequest("No unpaid pending invoice to cancel");
return BadRequest(StringLocalizer["No unpaid pending invoice to cancel"]);
}
foreach (var invoice in invoices)
@ -409,11 +415,11 @@ namespace BTCPayServer.Controllers
if (redirect)
{
TempData[WellKnownTempData.SuccessMessage] = "Payment cancelled";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Payment cancelled"].Value;
return RedirectToAction(nameof(ViewPaymentRequest), new { payReqId });
}
return Ok("Payment cancelled");
return Ok(StringLocalizer["Payment cancelled"]);
}
[HttpGet("{payReqId}/clone")]
@ -446,8 +452,8 @@ namespace BTCPayServer.Controllers
if(result is not null)
{
TempData[WellKnownTempData.SuccessMessage] = result.Value
? "The payment request has been archived and will no longer appear in the payment request list by default again."
: "The payment request has been unarchived and will appear in the payment request list by default.";
? StringLocalizer["The payment request has been archived and will no longer appear in the payment request list by default again."].Value
: StringLocalizer["The payment request has been unarchived and will appear in the payment request list by default."].Value;
return RedirectToAction("GetPaymentRequests", new { storeId = store.Id });
}

View File

@ -13,6 +13,7 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
@ -21,16 +22,19 @@ namespace BTCPayServer.Controllers
{
public UIPublicController(UIInvoiceController invoiceController,
StoreRepository storeRepository,
IStringLocalizer stringLocalizer,
LinkGenerator linkGenerator)
{
_InvoiceController = invoiceController;
_StoreRepository = storeRepository;
_linkGenerator = linkGenerator;
StringLocalizer = stringLocalizer;
}
private readonly UIInvoiceController _InvoiceController;
private readonly StoreRepository _StoreRepository;
private readonly LinkGenerator _linkGenerator;
public IStringLocalizer StringLocalizer { get; }
[HttpGet]
[IgnoreAntiforgeryToken]
@ -50,16 +54,16 @@ namespace BTCPayServer.Controllers
{
var store = await _StoreRepository.FindStore(model.StoreId);
if (store == null)
ModelState.AddModelError("Store", "Invalid store");
ModelState.AddModelError("Store", StringLocalizer["Invalid store"]);
else
{
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
ModelState.AddModelError("Store", StringLocalizer["Store has not enabled Pay Button"]);
}
if (model == null || (model.Price is decimal v ? v <= 0 : false))
ModelState.AddModelError("Price", "Price must be greater than 0");
ModelState.AddModelError("Price", StringLocalizer["Price must be greater than 0"]);
if (!ModelState.IsValid)
return View();

View File

@ -1,18 +1,15 @@
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.NTag424;
using Dapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin.DataEncoders;
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using static BTCPayServer.BoltcardDataExtensions;
namespace BTCPayServer.Controllers
@ -23,7 +20,7 @@ namespace BTCPayServer.Controllers
[HttpGet("pull-payments/{pullPaymentId}/boltcard/{command}")]
public IActionResult SetupBoltcard(string pullPaymentId, string command)
{
return View(nameof(SetupBoltcard), new SetupBoltcardViewModel()
return View(nameof(SetupBoltcard), new SetupBoltcardViewModel
{
ReturnUrl = Url.Action(nameof(ViewPullPayment), "UIPullPayment", new { pullPaymentId }),
WebsocketPath = Url.Action(nameof(VaultNFCBridgeConnection), "UIPullPayment", new { pullPaymentId }),
@ -34,7 +31,7 @@ namespace BTCPayServer.Controllers
[HttpPost("pull-payments/{pullPaymentId}/boltcard/{command}")]
public IActionResult SetupBoltcardPost(string pullPaymentId, string command)
{
TempData[WellKnownTempData.SuccessMessage] = "Boltcard is configured";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Boltcard is configured"].Value;
return RedirectToAction(nameof(ViewPullPayment), new { pullPaymentId });
}
@ -75,27 +72,27 @@ next:
var permission = await vaultClient.AskPermission(VaultServices.NFC, cts.Token);
if (permission is null)
{
await vaultClient.Show(VaultMessageType.Error, "BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>.", cts.Token);
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["BTCPay Server Vault does not seem to be running, you can download it on {0}.", new HtmlString("<a href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest/\" class=\"alert-link\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub</a>")], cts.Token);
goto next;
}
await vaultClient.Show(VaultMessageType.Ok, "BTCPayServer successfully connected to the vault.", cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["BTCPayServer successfully connected to the vault."], cts.Token);
if (permission is false)
{
await vaultClient.Show(VaultMessageType.Error, "The user declined access to the vault.", cts.Token);
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["The user declined access to the vault."], cts.Token);
goto next;
}
await vaultClient.Show(VaultMessageType.Ok, "Access to vault granted by owner.", cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Access to vault granted by owner."], cts.Token);
await vaultClient.Show(VaultMessageType.Processing, "Waiting for NFC to be presented...", cts.Token);
await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Waiting for NFC to be presented..."], cts.Token);
await transport.WaitForCard(cts.Token);
await vaultClient.Show(VaultMessageType.Ok, "NFC detected.", cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["NFC detected."], cts.Token);
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
CardOrigin cardOrigin = await GetCardOrigin(pullPaymentId, ntag, issuerKey, cts.Token);
if (cardOrigin is CardOrigin.OtherIssuer)
{
await vaultClient.Show(VaultMessageType.Error, "This card is already configured for another issuer", cts.Token);
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["This card is already configured for another issuer"], cts.Token);
goto next;
}
@ -103,7 +100,7 @@ next:
switch (command)
{
case "configure-boltcard":
await vaultClient.Show(VaultMessageType.Processing, "Configuring Boltcard...", cts.Token);
await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Configuring Boltcard..."], cts.Token);
if (cardOrigin is CardOrigin.Blank || cardOrigin is CardOrigin.ThisIssuerReset)
{
await ntag.AuthenticateEV2First(0, AESKey.Default, cts.Token);
@ -119,35 +116,35 @@ next:
await _dbContextFactory.SetBoltcardResetState(issuerKey, uid);
throw;
}
await vaultClient.Show(VaultMessageType.Ok, "The card is now configured", cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["The card is now configured"], cts.Token);
}
else if (cardOrigin is CardOrigin.ThisIssuer)
{
await vaultClient.Show(VaultMessageType.Ok, "This card is already properly configured", cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["This card is already properly configured"], cts.Token);
}
success = true;
break;
case "reset-boltcard":
await vaultClient.Show(VaultMessageType.Processing, "Resetting Boltcard...", cts.Token);
await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Resetting Boltcard..."], cts.Token);
if (cardOrigin is CardOrigin.Blank)
{
await vaultClient.Show(VaultMessageType.Ok, "This card is already in a factory state", cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["This card is already in a factory state"], cts.Token);
}
else if (cardOrigin is CardOrigin.ThisIssuer thisIssuer)
{
var cardKey = issuerKey.CreatePullPaymentCardKey(thisIssuer.Registration.UId, thisIssuer.Registration.Version, pullPaymentId);
await ntag.ResetCard(issuerKey, cardKey);
await _dbContextFactory.SetBoltcardResetState(issuerKey, thisIssuer.Registration.UId);
await vaultClient.Show(VaultMessageType.Ok, "Card reset succeed", cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Card reset succeed"], cts.Token);
}
success = true;
break;
}
if (success)
{
await vaultClient.Show(VaultMessageType.Processing, "Please remove the NFC from the card reader", cts.Token);
await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Please remove the NFC from the card reader"], cts.Token);
await transport.WaitForRemoved(cts.Token);
await vaultClient.Show(VaultMessageType.Ok, "Thank you!", cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Thank you!"], cts.Token);
await vaultClient.SendSimpleMessage("done", cts.Token);
}
}
@ -159,7 +156,7 @@ next:
{
try
{
await vaultClient.Show(VaultMessageType.Error, "Unexpected error: " + ex.Message, ex.ToString(), cts.Token);
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["Unexpected error: {0}", ex.Message], ex.ToString(), cts.Token);
}
catch { }
}

View File

@ -1,11 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Amazon.S3.Model;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
@ -14,24 +10,17 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers.Greenfield;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Dapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NdefLibrary.Ndef;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Controllers
{
@ -48,6 +37,7 @@ namespace BTCPayServer.Controllers
private readonly StoreRepository _storeRepository;
private readonly BTCPayServerEnvironment _env;
private readonly SettingsRepository _settingsRepository;
public IStringLocalizer StringLocalizer { get; }
public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory,
CurrencyNameTable currencyNameTable,
@ -59,6 +49,7 @@ namespace BTCPayServer.Controllers
PayoutMethodHandlerDictionary payoutHandlers,
StoreRepository storeRepository,
BTCPayServerEnvironment env,
IStringLocalizer stringLocalizer,
SettingsRepository settingsRepository)
{
_dbContextFactory = dbContextFactory;
@ -72,6 +63,7 @@ namespace BTCPayServer.Controllers
_env = env;
_settingsRepository = settingsRepository;
_networkProvider = networkProvider;
StringLocalizer = stringLocalizer;
}
[AllowAnonymous]
@ -196,7 +188,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Pull payment updated successfully",
Message = StringLocalizer["Pull payment updated successfully"].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
@ -204,19 +196,22 @@ namespace BTCPayServer.Controllers
}
[AllowAnonymous]
[HttpPost("pull-payments/{pullPaymentId}/claim")]
[HttpPost("pull-payments/{pullPaymentId}")]
public async Task<IActionResult> ClaimPullPayment(string pullPaymentId, ViewPullPaymentModel vm, CancellationToken cancellationToken)
{
await using var ctx = _dbContextFactory.CreateContext();
if (vm.ClaimedAmount == 0)
vm.ClaimedAmount = null;
await using var ctx = _dbContextFactory.CreateContext();
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
if (pp is null)
{
ModelState.AddModelError(nameof(pullPaymentId), "This pull payment does not exists");
ModelState.AddModelError(nameof(pullPaymentId), StringLocalizer["This pull payment does not exists"]);
}
if (string.IsNullOrEmpty(vm.Destination))
{
ModelState.AddModelError(nameof(vm.Destination), "Please provide a destination");
ModelState.AddModelError(nameof(vm.Destination), StringLocalizer["Please provide a destination"]);
return await ViewPullPayment(pullPaymentId);
}
@ -232,7 +227,7 @@ namespace BTCPayServer.Controllers
{
var handler = _payoutHandlers.TryGet(pmId);
(IClaimDestination dst, string err) = handler == null
? (null, "No payment handler found for this payment method")
? (null, StringLocalizer["No payment handler found for this payment method"])
: await handler.ParseAndValidateClaimDestination(vm.Destination, ppBlob, cancellationToken);
error = err;
if (dst is not null && err is null)
@ -256,17 +251,17 @@ namespace BTCPayServer.Controllers
if (destination is null)
{
ModelState.AddModelError(nameof(vm.Destination), error ?? "Invalid destination or payment method");
ModelState.AddModelError(nameof(vm.Destination), error ?? StringLocalizer["Invalid destination or payment method"]);
return await ViewPullPayment(pullPaymentId);
}
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0 ? null : vm.ClaimedAmount, payoutHandler.Currency, pp.Currency);
if (amtError.error is not null)
var claimedAmount = ClaimRequest.GetClaimedAmount(destination, vm.ClaimedAmount, payoutHandler.Currency, pp.Currency);
if (claimedAmount is ClaimRequest.ClaimedAmountResult.Error err2)
{
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error);
ModelState.AddModelError(nameof(vm.ClaimedAmount), err2.Message);
}
else if (amtError.amount is not null)
else if (claimedAmount is ClaimRequest.ClaimedAmountResult.Success succ)
{
vm.ClaimedAmount = amtError.amount.Value;
vm.ClaimedAmount = succ.Amount;
}
if (!ModelState.IsValid)
@ -278,7 +273,7 @@ namespace BTCPayServer.Controllers
{
Destination = destination,
PullPaymentId = pullPaymentId,
Value = vm.ClaimedAmount,
ClaimedAmount = vm.ClaimedAmount,
PayoutMethodId = payoutMethodId,
StoreId = pp.StoreId
});
@ -291,11 +286,19 @@ namespace BTCPayServer.Controllers
return await ViewPullPayment(pullPaymentId);
}
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = $"Your claim request of {_displayFormatter.Currency(vm.ClaimedAmount, pp.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
Severity = StatusMessageModel.StatusSeverity.Success
});
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = (vm.ClaimedAmount, result.PayoutData.State) switch
{
(null, PayoutState.AwaitingApproval) => $"Your claim request to {vm.Destination} has been submitted and is awaiting approval",
(null, PayoutState.AwaitingPayment) => $"Your claim request to {vm.Destination} has been submitted and is awaiting payment",
({ } a, PayoutState.AwaitingApproval) => $"Your claim request of {_displayFormatter.Currency(a, pp.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting approval",
({ } a, PayoutState.AwaitingPayment) => $"Your claim request of {_displayFormatter.Currency(a, pp.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting payment",
_ => $"Unexpected payout state ({result.PayoutData.State})"
},
Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(ViewPullPayment), new { pullPaymentId });
}

View File

@ -18,19 +18,20 @@ namespace BTCPayServer.Controllers
[HttpGet("server/plugins")]
public async Task<IActionResult> ListPlugins(
[FromServices] PluginService pluginService,
[FromServices] BTCPayServerOptions btcPayServerOptions)
[FromServices] BTCPayServerOptions btcPayServerOptions,
string search = null)
{
IEnumerable<PluginService.AvailablePlugin> availablePlugins;
try
{
availablePlugins = await pluginService.GetRemotePlugins();
availablePlugins = await pluginService.GetRemotePlugins(search);
}
catch (Exception)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Remote plugins lookup failed. Try again later."
Message = StringLocalizer["Remote plugins lookup failed. Try again later."].Value
});
availablePlugins = Array.Empty<PluginService.AvailablePlugin>();
}
@ -59,14 +60,24 @@ namespace BTCPayServer.Controllers
public Dictionary<string, AvailablePlugin> DownloadedPluginsByIdentifier { get; set; } = new Dictionary<string, AvailablePlugin>();
}
[HttpPost("server/plugins/uninstall-all")]
public IActionResult UnInstallAllDisabledPlugin(
[FromServices] PluginService pluginService, string plugin)
{
var disabled = pluginService.GetDisabledPlugins();
foreach (var d in disabled)
pluginService.UninstallPlugin(d.Key);
return RedirectToAction(nameof(ListPlugins));
}
[HttpPost("server/plugins/uninstall")]
public IActionResult UnInstallPlugin(
[FromServices] PluginService pluginService, string plugin)
{
pluginService.UninstallPlugin(plugin);
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Plugin scheduled to be uninstalled.",
Message = StringLocalizer["Plugin scheduled to be uninstalled."].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
@ -78,9 +89,9 @@ namespace BTCPayServer.Controllers
[FromServices] PluginService pluginService, string plugin)
{
pluginService.CancelCommands(plugin);
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Plugin action cancelled.",
Message = StringLocalizer["Plugin action cancelled."].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
@ -102,17 +113,17 @@ namespace BTCPayServer.Controllers
{
pluginService.InstallPlugin(plugin);
}
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Plugin scheduled to be installed.",
Message = StringLocalizer["Plugin scheduled to be installed."].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
}
catch (Exception)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "The plugin could not be downloaded. Try again later.",
Message = StringLocalizer["The plugin could not be downloaded. Try again later."].Value,
Severity = StatusMessageModel.StatusSeverity.Error
});
}
@ -131,9 +142,9 @@ namespace BTCPayServer.Controllers
StringComparison.InvariantCultureIgnoreCase));
}
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Files uploaded, restart server to load plugins",
Message = StringLocalizer["Files uploaded, restart server to load plugins"].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction("ListPlugins");

View File

@ -67,12 +67,12 @@ namespace BTCPayServer.Controllers
string successMessage = null;
if (role == "create")
{
successMessage = "Role created";
successMessage = StringLocalizer["Role created"];
role = viewModel.Role;
}
else
{
successMessage = "Role updated";
successMessage = StringLocalizer["Role updated"];
var storeRole = await _StoreRepository.GetStoreRole(new StoreRoleId(role));
if (storeRole == null)
return NotFound();
@ -86,15 +86,15 @@ namespace BTCPayServer.Controllers
var r = await _StoreRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
if (r is null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Role could not be updated"
Message = StringLocalizer["Role could not be updated"].Value
});
return View(viewModel);
}
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = successMessage
@ -114,11 +114,11 @@ namespace BTCPayServer.Controllers
return View("Confirm",
roleData.IsUsed is true
? new ConfirmModel("Delete role",
? new ConfirmModel(StringLocalizer["Delete role"],
$"Unable to proceed: The role <strong>{Html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
: new ConfirmModel("Delete role",
: new ConfirmModel(StringLocalizer["Delete role"],
$"The role <strong>{Html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
"Delete"));
StringLocalizer["Delete"]));
}
[HttpPost("server/roles/{role}/delete")]
@ -137,7 +137,7 @@ namespace BTCPayServer.Controllers
if (errorMessage is null)
{
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Role deleted"].Value;
}
else
{
@ -153,12 +153,12 @@ namespace BTCPayServer.Controllers
var resolved = await _StoreRepository.ResolveStoreRoleId(null, role);
if (resolved is null)
{
TempData[WellKnownTempData.ErrorMessage] = "Role could not be set as default";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Role could not be set as default"].Value;
}
else
{
await _StoreRepository.SetDefaultRole(role);
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Role set default"].Value;
}
return RedirectToAction(nameof(ListRoles));

View File

@ -52,9 +52,9 @@ namespace BTCPayServer.Controllers
if (!allFilesExist)
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Some of the files were not found",
Message = StringLocalizer["Some of the files were not found"].Value,
Severity = StatusMessageModel.StatusSeverity.Warning,
});
}
@ -75,12 +75,12 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Files), new
{
fileIds = Array.Empty<string>(),
statusMessage = "File removed"
statusMessage = StringLocalizer["File removed"].Value
});
}
catch (Exception e)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = e.Message
@ -108,7 +108,7 @@ namespace BTCPayServer.Controllers
{
if (viewModel.TimeAmount <= 0)
{
ModelState.AddModelError(nameof(viewModel.TimeAmount), "Time must be at least 1");
ModelState.AddModelError(nameof(viewModel.TimeAmount), StringLocalizer["Time must be at least 1"]);
}
if (!ModelState.IsValid)
@ -192,21 +192,21 @@ namespace BTCPayServer.Controllers
if (invalidFileNameCount == 0)
{
statusMessage = "Files Added Successfully";
statusMessage = StringLocalizer["Files added successfully"];
statusMessageSeverity = StatusMessageModel.StatusSeverity.Success;
}
else if (invalidFileNameCount > 0 && invalidFileNameCount < files.Count)
{
statusMessage = $"{files.Count - invalidFileNameCount} files were added. {invalidFileNameCount} files had invalid names";
statusMessage = StringLocalizer["{0} files were added. {1} files had invalid names", files.Count - invalidFileNameCount, invalidFileNameCount].Value;
statusMessageSeverity = StatusMessageModel.StatusSeverity.Error;
}
else
{
statusMessage = $"Files could not be added due to invalid names";
statusMessage = StringLocalizer["Files could not be added due to invalid names"].Value;
statusMessageSeverity = StatusMessageModel.StatusSeverity.Error;
}
this.TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = statusMessage,
Severity = statusMessageSeverity
@ -266,10 +266,10 @@ namespace BTCPayServer.Controllers
{
if (!Enum.TryParse(typeof(StorageProvider), provider, out var storageProvider))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = $"{provider} provider is not supported"
Message = StringLocalizer["{0} provider is not supported", provider].Value
});
return RedirectToAction(nameof(Storage));
}
@ -282,10 +282,10 @@ namespace BTCPayServer.Controllers
switch (storageProviderService)
{
case null:
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = $"{storageProvider} is not supported"
Message = StringLocalizer["{0} provider is not supported", storageProvider].Value
});
return RedirectToAction(nameof(Storage));
case AzureBlobStorageFileProviderService fileProviderService:
@ -350,10 +350,10 @@ namespace BTCPayServer.Controllers
data.Provider = storageProvider;
data.Configuration = JObject.FromObject(viewModel);
await _SettingsRepository.UpdateSetting(data);
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Storage settings updated successfully"
Message = StringLocalizer["Storage settings updated successfully"].Value
});
return View(viewModel);
}

View File

@ -38,13 +38,14 @@ namespace BTCPayServer.Controllers
[HttpGet("server/dictionaries/create")]
public async Task<IActionResult> CreateDictionary(string fallback = null)
{
var dictionaries = await this._localizer.GetDictionaries();
return View(new CreateDictionaryViewModel()
var dictionaries = await _localizer.GetDictionaries();
return View(new CreateDictionaryViewModel
{
Name = fallback is not null ? $"Clone of {fallback}" : "",
Fallback = fallback ?? Translations.DefaultLanguage,
}.SetDictionaries(dictionaries));
}
[HttpPost("server/dictionaries/create")]
public async Task<IActionResult> CreateDictionary(CreateDictionaryViewModel viewModel)
{
@ -52,23 +53,23 @@ namespace BTCPayServer.Controllers
{
try
{
await this._localizer.CreateDictionary(viewModel.Name, viewModel.Fallback, "Custom");
await _localizer.CreateDictionary(viewModel.Name, viewModel.Fallback, "Custom");
}
catch (DbException)
{
ModelState.AddModelError(nameof(viewModel.Name), $"'{viewModel.Name}' already exists");
ModelState.AddModelError(nameof(viewModel.Name), StringLocalizer["'{0}' already exists", viewModel.Name]);
}
}
if (!ModelState.IsValid)
return View(viewModel.SetDictionaries(await this._localizer.GetDictionaries()));
TempData[WellKnownTempData.SuccessMessage] = "Dictionary created";
return View(viewModel.SetDictionaries(await _localizer.GetDictionaries()));
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary created"].Value;
return RedirectToAction(nameof(EditDictionary), new { dictionary = viewModel.Name });
}
[HttpGet("server/dictionaries/{dictionary}")]
public async Task<IActionResult> EditDictionary(string dictionary)
{
if ((await this._localizer.GetDictionary(dictionary)) is null)
if ((await _localizer.GetDictionary(dictionary)) is null)
return NotFound();
var translations = await _localizer.GetTranslations(dictionary);
return View(new EditDictionaryViewModel().SetTranslations(translations.Translations));
@ -77,7 +78,7 @@ namespace BTCPayServer.Controllers
[HttpPost("server/dictionaries/{dictionary}")]
public async Task<IActionResult> EditDictionary(string dictionary, EditDictionaryViewModel viewModel)
{
var d = await this._localizer.GetDictionary(dictionary);
var d = await _localizer.GetDictionary(dictionary);
if (d is null)
return NotFound();
if (Environment.CheatMode && viewModel.Command == "Fake")
@ -87,35 +88,38 @@ namespace BTCPayServer.Controllers
foreach (var prop in jobj.Properties())
{
prop.Value = "OK";
if (prop.Name.Contains("{0}")) prop.Value += " {0}";
if (prop.Name.Contains("{1}")) prop.Value += " {1}";
if (prop.Name.Contains("{2}")) prop.Value += " {2}";
}
viewModel.Translations = Translations.CreateFromJson(jobj.ToString()).ToJsonFormat();
}
if (!Translations.TryCreateFromJson(viewModel.Translations, out var translations))
{
ModelState.AddModelError(nameof(viewModel.Translations), "Syntax error");
ModelState.AddModelError(nameof(viewModel.Translations), StringLocalizer["Syntax error"]);
return View(viewModel);
}
await _localizer.Save(d, translations);
TempData[WellKnownTempData.SuccessMessage] = "Dictionary updated";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary updated"].Value;
return RedirectToAction(nameof(ListDictionaries));
}
[HttpGet("server/dictionaries/{dictionary}/select")]
public async Task<IActionResult> SelectDictionary(string dictionary)
{
var settings = await this._SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new();
var settings = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new();
settings.LangDictionary = dictionary;
await _SettingsRepository.UpdateSetting(settings);
await _localizer.Load();
TempData[WellKnownTempData.SuccessMessage] = $"Default dictionary changed to {dictionary}";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Default dictionary changed to {0}", dictionary].Value;
return RedirectToAction(nameof(ListDictionaries));
}
[HttpPost("server/dictionaries/{dictionary}/delete")]
public async Task<IActionResult> DeleteDictionary(string dictionary)
{
await _localizer.DeleteDictionary(dictionary);
TempData[WellKnownTempData.SuccessMessage] = $"Dictionary {dictionary} deleted";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary {0} deleted", dictionary].Value;
return RedirectToAction(nameof(ListDictionaries));
}
}

View File

@ -139,18 +139,18 @@ namespace BTCPayServer.Controllers
{
if (viewModel.ImageFile.Length > 1_000_000)
{
ModelState.AddModelError(nameof(viewModel.ImageFile), "The uploaded image file should be less than 1MB");
ModelState.AddModelError(nameof(viewModel.ImageFile), StringLocalizer["The uploaded image file should be less than {0}", "1MB"]);
}
else if (!viewModel.ImageFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
ModelState.AddModelError(nameof(viewModel.ImageFile), "The uploaded file needs to be an image");
ModelState.AddModelError(nameof(viewModel.ImageFile), StringLocalizer["The uploaded file needs to be an image"]);
}
else
{
var formFile = await viewModel.ImageFile.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
{
ModelState.AddModelError(nameof(viewModel.ImageFile), "The uploaded file needs to be an image");
ModelState.AddModelError(nameof(viewModel.ImageFile), StringLocalizer["The uploaded file needs to be an image"]);
}
else
{
@ -165,7 +165,7 @@ namespace BTCPayServer.Controllers
}
catch (Exception e)
{
ModelState.AddModelError(nameof(viewModel.ImageFile), $"Could not save image: {e.Message}");
ModelState.AddModelError(nameof(viewModel.ImageFile), StringLocalizer["Could not save image: {0}", e.Message]);
}
}
}
@ -181,7 +181,7 @@ namespace BTCPayServer.Controllers
var wasAdmin = Roles.HasServerAdmin(roles);
if (!viewModel.IsAdmin && admins.Count == 1 && wasAdmin)
{
TempData[WellKnownTempData.ErrorMessage] = "This is the only Admin, so their role can't be removed until another Admin is added.";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["This is the only admin, so their role can't be removed until another Admin is added."].Value;
return View(viewModel);
}
@ -199,11 +199,11 @@ namespace BTCPayServer.Controllers
{
if (propertiesChanged is not false && adminStatusChanged is not false && approvalStatusChanged is not false)
{
TempData[WellKnownTempData.SuccessMessage] = "User successfully updated";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["User successfully updated"].Value;
}
else
{
TempData[WellKnownTempData.ErrorMessage] = "Error updating user";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Error updating user"].Value;
}
}
@ -231,7 +231,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = result.Succeeded ? StatusMessageModel.StatusSeverity.Success : StatusMessageModel.StatusSeverity.Error,
Message = result.Succeeded ? "Password successfully set" : "An error occurred while resetting user password"
Message = result.Succeeded ? StringLocalizer["Password successfully set"].Value : StringLocalizer["An error occurred while resetting user password"].Value
});
return RedirectToAction(nameof(ListUsers));
}
@ -326,16 +326,16 @@ namespace BTCPayServer.Controllers
{
if (await _userService.IsUserTheOnlyOneAdmin(user))
{
return View("Confirm", new ConfirmModel("Delete admin",
return View("Confirm", new ConfirmModel(StringLocalizer["Delete admin"],
$"Unable to proceed: As the user <strong>{Html.Encode(user.Email)}</strong> is the last enabled admin, it cannot be removed."));
}
return View("Confirm", new ConfirmModel("Delete admin",
$"The admin <strong>{Html.Encode(user.Email)}</strong> will be permanently deleted. This action will also delete all accounts, users and data associated with the server account. Are you sure?",
"Delete"));
return View("Confirm", new ConfirmModel(StringLocalizer["Delete admin"],
StringLocalizer["The admin {0} will be permanently deleted. This action will also delete all accounts, users and data associated with the server account. Are you sure?", Html.Encode(user.Email)],
StringLocalizer["Delete"]));
}
return View("Confirm", new ConfirmModel("Delete user", $"The user <strong>{Html.Encode(user.Email)}</strong> will be permanently deleted. Are you sure?", "Delete"));
return View("Confirm", new ConfirmModel(StringLocalizer["Delete user"], $"The user <strong>{Html.Encode(user.Email)}</strong> will be permanently deleted. Are you sure?", "Delete"));
}
[HttpPost("server/users/{userId}/delete")]
@ -347,7 +347,7 @@ namespace BTCPayServer.Controllers
await _userService.DeleteUserAndAssociatedData(user);
TempData[WellKnownTempData.SuccessMessage] = "User deleted";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["User deleted"].Value;
return RedirectToAction(nameof(ListUsers));
}
@ -360,7 +360,7 @@ namespace BTCPayServer.Controllers
if (!enable && await _userService.IsUserTheOnlyOneAdmin(user))
{
return View("Confirm", new ConfirmModel("Disable admin",
return View("Confirm", new ConfirmModel(StringLocalizer["Disable admin"],
$"Unable to proceed: As the user <strong>{Html.Encode(user.Email)}</strong> is the last enabled admin, it cannot be disabled."));
}
return View("Confirm", new ConfirmModel($"{(enable ? "Enable" : "Disable")} user", $"The user <strong>{Html.Encode(user.Email)}</strong> will be {(enable ? "enabled" : "disabled")}. Are you sure?", (enable ? "Enable" : "Disable")));
@ -374,12 +374,14 @@ namespace BTCPayServer.Controllers
return NotFound();
if (!enable && await _userService.IsUserTheOnlyOneAdmin(user))
{
TempData[WellKnownTempData.SuccessMessage] = $"User was the last enabled admin and could not be disabled.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["User was the last enabled admin and could not be disabled."].Value;
return RedirectToAction(nameof(ListUsers));
}
await _userService.ToggleUser(userId, enable ? null : DateTimeOffset.MaxValue);
TempData[WellKnownTempData.SuccessMessage] = $"User {(enable ? "enabled" : "disabled")}";
TempData[WellKnownTempData.SuccessMessage] = enable
? StringLocalizer["User enabled"].Value
: StringLocalizer["User disabled"].Value;
return RedirectToAction(nameof(ListUsers));
}
@ -402,7 +404,9 @@ namespace BTCPayServer.Controllers
await _userService.SetUserApproval(userId, approved, Request.GetAbsoluteRootUri());
TempData[WellKnownTempData.SuccessMessage] = $"User {(approved ? "approved" : "unapproved")}";
TempData[WellKnownTempData.SuccessMessage] = approved
? StringLocalizer["User approved"].Value
: StringLocalizer["User unapproved"].Value;
return RedirectToAction(nameof(ListUsers));
}
@ -413,7 +417,7 @@ namespace BTCPayServer.Controllers
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel("Send verification email", $"This will send a verification email to <strong>{Html.Encode(user.Email)}</strong>.", "Send"));
return View("Confirm", new ConfirmModel(StringLocalizer["Send verification email"], $"This will send a verification email to <strong>{Html.Encode(user.Email)}</strong>.", "Send"));
}
[HttpPost("server/users/{userId}/verification-email")]
@ -430,7 +434,7 @@ namespace BTCPayServer.Controllers
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent"].Value;
return RedirectToAction(nameof(ListUsers));
}

View File

@ -33,6 +33,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
@ -69,6 +70,7 @@ namespace BTCPayServer.Controllers
private readonly EmailSenderFactory _emailSenderFactory;
private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly LocalizerService _localizer;
public IStringLocalizer StringLocalizer { get; }
public UIServerController(
UserManager<ApplicationUser> userManager,
@ -96,6 +98,7 @@ namespace BTCPayServer.Controllers
IHtmlHelper html,
TransactionLinkProviders transactionLinkProviders,
LocalizerService localizer,
IStringLocalizer stringLocalizer,
BTCPayServerEnvironment environment
)
{
@ -125,6 +128,7 @@ namespace BTCPayServer.Controllers
_transactionLinkProviders = transactionLinkProviders;
_localizer = localizer;
Environment = environment;
StringLocalizer = stringLocalizer;
}
[HttpGet("server/stores")]
@ -157,7 +161,7 @@ namespace BTCPayServer.Controllers
};
if (!vm.CanUseSSH)
TempData[WellKnownTempData.ErrorMessage] = "Maintenance feature requires access to SSH properly configured in BTCPay Server configuration.";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Maintenance feature requires access to SSH properly configured in BTCPay Server configuration."].Value;
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
vm.DNSDomain = null;
@ -170,7 +174,7 @@ namespace BTCPayServer.Controllers
vm.CanUseSSH = _sshState.CanUseSSH;
if (command != "soft-restart" && !vm.CanUseSSH)
{
TempData[WellKnownTempData.ErrorMessage] = "Maintenance feature requires access to SSH properly configured in BTCPay Server configuration.";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Maintenance feature requires access to SSH properly configured in BTCPay Server configuration."].Value;
return View(vm);
}
if (!ModelState.IsValid)
@ -229,21 +233,21 @@ namespace BTCPayServer.Controllers
builder.Path = null;
builder.Query = null;
TempData[WellKnownTempData.SuccessMessage] = $"Domain name changing... the server will restart, please use \"{builder.Uri.AbsoluteUri}\" (this page won't reload automatically)";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Domain name changing... the server will restart, please use \"{0}\" (this page won't reload automatically)", builder.Uri.AbsoluteUri].Value;
}
else if (command == "update")
{
var error = await RunSSH(vm, $"btcpay-update.sh");
if (error != null)
return error;
TempData[WellKnownTempData.SuccessMessage] = $"The server might restart soon if an update is available... (this page won't reload automatically)";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The server might restart soon if an update is available... (this page won't reload automatically)"].Value;
}
else if (command == "clean")
{
var error = await RunSSH(vm, $"btcpay-clean.sh");
if (error != null)
return error;
TempData[WellKnownTempData.SuccessMessage] = $"The old docker images will be cleaned soon...";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The old docker images will be cleaned soon..."].Value;
}
else if (command == "restart")
{
@ -251,11 +255,11 @@ namespace BTCPayServer.Controllers
if (error != null)
return error;
Logs.PayServer.LogInformation("A hard restart has been requested");
TempData[WellKnownTempData.SuccessMessage] = $"BTCPay will restart momentarily.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["BTCPay will restart momentarily."].Value;
}
else if (command == "soft-restart")
{
TempData[WellKnownTempData.SuccessMessage] = $"BTCPay will restart momentarily.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["BTCPay will restart momentarily."].Value;
Logs.PayServer.LogInformation("A soft restart has been requested");
_ = Task.Delay(3000).ContinueWith((t) => ApplicationLifetime.StopApplication());
}
@ -401,7 +405,7 @@ namespace BTCPayServer.Controllers
_ = _transactionLinkProviders.RefreshTransactionLinkTemplates();
if (_policiesSettings.LangDictionary != settings.LangDictionary)
await _localizer.Load();
TempData[WellKnownTempData.SuccessMessage] = "Policies updated successfully";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Policies updated successfully"].Value;
return RedirectToAction(nameof(Policies));
}
@ -525,7 +529,7 @@ namespace BTCPayServer.Controllers
return NotFound();
if (!string.IsNullOrEmpty(cryptoCode) && !_dashBoard.IsFullySynched(cryptoCode, out _) && service.Type != ExternalServiceTypes.RPC)
{
TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["{0} is not fully synched", cryptoCode].Value;
return RedirectToAction(nameof(Services));
}
try
@ -575,7 +579,7 @@ namespace BTCPayServer.Controllers
case ExternalServiceTypes.Torq:
if (connectionString.AccessKey == null)
{
TempData[WellKnownTempData.ErrorMessage] = $"The access key of the service is not set";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["The access key of the service is not set"].Value;
return RedirectToAction(nameof(Services));
}
LightningWalletServices vm = new LightningWalletServices();
@ -613,7 +617,7 @@ namespace BTCPayServer.Controllers
[HttpGet("server/services/{serviceName}/{cryptoCode}/removelndseed")]
public IActionResult RemoveLndSeed(string serviceName, string cryptoCode)
{
return View("Confirm", new ConfirmModel("Delete LND seed", "This action will permanently delete your LND seed and password. You will not be able to recover them if you don't have a backup. Are you sure?", "Delete"));
return View("Confirm", new ConfirmModel(StringLocalizer["Delete LND seed"], StringLocalizer["This action will permanently delete your LND seed and password. You will not be able to recover them if you don't have a backup. Are you sure?"], StringLocalizer["Delete"]));
}
[HttpPost("server/services/{serviceName}/{cryptoCode}/removelndseed")]
@ -626,24 +630,24 @@ namespace BTCPayServer.Controllers
var model = LndSeedBackupViewModel.Parse(service.ConnectionString.CookieFilePath);
if (!model.IsWalletUnlockPresent)
{
TempData[WellKnownTempData.ErrorMessage] = $"File with wallet password and seed info not present";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["File with wallet password and seed info not present"].Value;
return RedirectToAction(nameof(Services));
}
if (string.IsNullOrEmpty(model.Seed))
{
TempData[WellKnownTempData.ErrorMessage] = $"Seed information was already removed";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Seed information was already removed"].Value;
return RedirectToAction(nameof(Services));
}
if (await model.RemoveSeedAndWrite(service.ConnectionString.CookieFilePath))
{
TempData[WellKnownTempData.SuccessMessage] = $"Seed successfully removed";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Seed successfully removed"].Value;
return RedirectToAction(nameof(Service), new { serviceName, cryptoCode });
}
else
{
TempData[WellKnownTempData.ErrorMessage] = $"Seed removal failed";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Seed removal failed"].Value;
return RedirectToAction(nameof(Services));
}
}
@ -725,7 +729,7 @@ namespace BTCPayServer.Controllers
{
if (!_dashBoard.IsFullySynched(cryptoCode, out _))
{
TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["{0} is not fully synched", cryptoCode].Value;
return RedirectToAction(nameof(Services));
}
var service = GetService(serviceName, cryptoCode);
@ -820,7 +824,7 @@ namespace BTCPayServer.Controllers
string errorMessage = await viewModel.Settings.SendUpdateRequest(HttpClientFactory.CreateClient());
if (errorMessage == null)
{
TempData[WellKnownTempData.SuccessMessage] = $"The Dynamic DNS has been successfully queried, your configuration is saved";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The Dynamic DNS has been successfully queried, your configuration is saved"].Value;
viewModel.Settings.LastUpdated = DateTimeOffset.UtcNow;
settings.Services.Add(viewModel.Settings);
await _SettingsRepository.UpdateSetting(settings);
@ -856,7 +860,7 @@ namespace BTCPayServer.Controllers
viewModel.Settings.Hostname = viewModel.Settings.Hostname.Trim().ToLowerInvariant();
if (!viewModel.Settings.Enabled)
{
TempData[WellKnownTempData.SuccessMessage] = $"The Dynamic DNS service has been disabled";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The Dynamic DNS service has been disabled"].Value;
viewModel.Settings.LastUpdated = null;
}
else
@ -864,7 +868,7 @@ namespace BTCPayServer.Controllers
string errorMessage = await viewModel.Settings.SendUpdateRequest(HttpClientFactory.CreateClient());
if (errorMessage == null)
{
TempData[WellKnownTempData.SuccessMessage] = $"The Dynamic DNS has been successfully queried, your configuration is saved";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The Dynamic DNS has been successfully queried, your configuration is saved"].Value;
viewModel.Settings.LastUpdated = DateTimeOffset.UtcNow;
}
else
@ -900,7 +904,7 @@ namespace BTCPayServer.Controllers
return NotFound();
settings.Services.RemoveAt(i);
await _SettingsRepository.UpdateSetting(settings);
TempData[WellKnownTempData.SuccessMessage] = "Dynamic DNS service successfully removed";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dynamic DNS service successfully removed"].Value;
RouteData.Values.Remove(nameof(hostname));
return RedirectToAction(nameof(DynamicDnsServices));
}
@ -974,7 +978,7 @@ namespace BTCPayServer.Controllers
try
{
await System.IO.File.WriteAllTextAsync(_Options.SSHSettings.AuthorizedKeysFile, newContent);
TempData[WellKnownTempData.SuccessMessage] = "authorized_keys has been updated";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["authorized_keys has been updated"].Value;
updated = true;
}
catch (Exception ex)
@ -1003,7 +1007,7 @@ namespace BTCPayServer.Controllers
if (exception is null)
{
TempData[WellKnownTempData.SuccessMessage] = "authorized_keys has been updated";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["authorized_keys has been updated"].Value;
}
else
{
@ -1032,7 +1036,7 @@ namespace BTCPayServer.Controllers
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
policies.DisableSSHService = true;
await _SettingsRepository.UpdateSetting(policies);
TempData[WellKnownTempData.SuccessMessage] = "Changes to the SSH settings are now permanently disabled in the BTCPay Server user interface";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Changes to the SSH settings are now permanently disabled in the BTCPay Server user interface"].Value;
return RedirectToAction(nameof(Services));
}
@ -1186,7 +1190,7 @@ namespace BTCPayServer.Controllers
if (settingsChanged)
{
await _SettingsRepository.UpdateSetting(theme);
TempData[WellKnownTempData.SuccessMessage] = "Settings updated successfully";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Settings updated successfully"].Value;
return RedirectToAction(nameof(Branding));
}
@ -1229,7 +1233,7 @@ namespace BTCPayServer.Controllers
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email sent to {0}. Please verify you received it.", model.TestEmail].Value;
}
catch (Exception ex)
{
@ -1249,14 +1253,14 @@ namespace BTCPayServer.Controllers
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
settings.Password = null;
await _SettingsRepository.UpdateSetting(settings);
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email server password reset"].Value;
return RedirectToAction(nameof(Emails));
}
// save
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
{
ModelState.AddModelError("Settings.From", "Invalid email");
ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]);
return View(model);
}
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
@ -1266,12 +1270,12 @@ namespace BTCPayServer.Controllers
}
await _SettingsRepository.UpdateSetting(model.Settings);
TempData[WellKnownTempData.SuccessMessage] = "Email settings saved";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email settings saved"].Value;
return RedirectToAction(nameof(Emails));
}
[Route("server/logs/{file?}")]
public async Task<IActionResult> LogsView(string? file = null, int offset = 0)
public async Task<IActionResult> LogsView(string? file = null, int offset = 0, bool download = false)
{
if (offset < 0)
{
@ -1282,16 +1286,14 @@ namespace BTCPayServer.Controllers
if (string.IsNullOrEmpty(_Options.LogFile))
{
TempData[WellKnownTempData.ErrorMessage] = "File Logging Option not specified. " +
"You need to set debuglog and optionally " +
"debugloglevel in the configuration or through runtime arguments";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["File Logging Option not specified. You need to set debuglog and optionally debugloglevel in the configuration or through runtime arguments"].Value;
}
else
{
var di = Directory.GetParent(_Options.LogFile);
if (di is null)
{
TempData[WellKnownTempData.ErrorMessage] = "Could not load log files";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Could not load log files"].Value;
return View("Logs", vm);
}
@ -1315,13 +1317,23 @@ namespace BTCPayServer.Controllers
return NotFound();
try
{
using var fileStream = new FileStream(
var fileStream = new FileStream(
fi.FullName,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite);
using var reader = new StreamReader(fileStream);
vm.Log = await reader.ReadToEndAsync();
if (download)
{
return new FileStreamResult(fileStream, "text/plain")
{
FileDownloadName = file
};
}
await using (fileStream)
{
using var reader = new StreamReader(fileStream);
vm.Log = await reader.ReadToEndAsync();
}
}
catch
{

View File

@ -18,8 +18,10 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json.Linq;
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
using PayoutData = BTCPayServer.Data.PayoutData;
@ -50,7 +52,10 @@ namespace BTCPayServer.Controllers
}
}
public IStringLocalizer StringLocalizer { get; }
public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider,
IStringLocalizer stringLocalizer,
PayoutMethodHandlerDictionary payoutHandlers,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
@ -62,6 +67,7 @@ namespace BTCPayServer.Controllers
IAuthorizationService authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
StringLocalizer = stringLocalizer;
_payoutHandlers = payoutHandlers;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
@ -85,7 +91,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a pull payment.",
Message = StringLocalizer["You must enable at least one payment method before creating a pull payment."].Value,
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
@ -119,25 +125,25 @@ namespace BTCPayServer.Controllers
// them here to reflect user's selection so that they can correct their mistake
model.PayoutMethodsItem =
paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), false));
ModelState.AddModelError(nameof(model.PayoutMethods), "You need at least one payout method");
ModelState.AddModelError(nameof(model.PayoutMethods), StringLocalizer["You need at least one payout method"]);
}
if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null)
{
ModelState.AddModelError(nameof(model.Currency), "Invalid currency");
ModelState.AddModelError(nameof(model.Currency), StringLocalizer["Invalid currency"]);
}
if (model.Amount <= 0.0m)
{
ModelState.AddModelError(nameof(model.Amount), "The amount should be more than zero");
ModelState.AddModelError(nameof(model.Amount), StringLocalizer["The amount should be more than zero"]);
}
if (model.Name.Length > 50)
{
ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters.");
ModelState.AddModelError(nameof(model.Name), StringLocalizer["The name should be maximum 50 characters."]);
}
var selectedPaymentMethodIds = model.PayoutMethods.Select(PayoutMethodId.Parse).ToArray();
if (!selectedPaymentMethodIds.All(id => paymentMethodOptions.Contains(id)))
{
ModelState.AddModelError(nameof(model.Name), "Not all payout methods are supported");
ModelState.AddModelError(nameof(model.Name), StringLocalizer["Not all payout methods are supported"]);
}
if (!ModelState.IsValid)
return View(model);
@ -156,7 +162,7 @@ namespace BTCPayServer.Controllers
});
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Pull payment request created",
Message = StringLocalizer["Pull payment request created"].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(PullPayments), new { storeId });
@ -198,7 +204,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a pull payment.",
Message = StringLocalizer["You must enable at least one payment method before creating a pull payment."].Value,
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
@ -260,7 +266,7 @@ namespace BTCPayServer.Controllers
string pullPaymentId)
{
return View("Confirm",
new ConfirmModel("Archive pull payment", "Do you really want to archive the pull payment?", "Archive"));
new ConfirmModel(StringLocalizer["Archive pull payment"], StringLocalizer["Do you really want to archive the pull payment?"], "Archive"));
}
[HttpPost("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
@ -269,9 +275,9 @@ namespace BTCPayServer.Controllers
string pullPaymentId)
{
await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(pullPaymentId));
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Pull payment archived",
Message = StringLocalizer["Pull payment archived"].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(PullPayments), new { storeId });
@ -296,9 +302,9 @@ namespace BTCPayServer.Controllers
var payoutIds = vm.GetSelectedPayouts(commandState);
if (payoutIds.Length == 0)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "No payout selected",
Message = StringLocalizer["No payout selected"].Value,
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts),
@ -339,9 +345,9 @@ namespace BTCPayServer.Controllers
var rateResult = await _pullPaymentService.GetRate(payout, null, cancellationToken);
if (rateResult.BidAsk == null)
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
Message = StringLocalizer["Rate unavailable: {0}", rateResult.EvaluatedRule].Value,
Severity = StatusMessageModel.StatusSeverity.Error
});
failed = true;
@ -349,7 +355,7 @@ namespace BTCPayServer.Controllers
}
var approveResult = await _pullPaymentService.Approve(
new HostedServices.PullPaymentHostedService.PayoutApproval()
new PullPaymentHostedService.PayoutApproval
{
PayoutId = payout.Id,
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
@ -357,7 +363,7 @@ namespace BTCPayServer.Controllers
});
if (approveResult.Result != PullPaymentHostedService.PayoutApproval.Result.Ok)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult.Result),
Severity = StatusMessageModel.StatusSeverity.Error
@ -377,9 +383,9 @@ namespace BTCPayServer.Controllers
goto case "pay";
}
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Payouts approved",
Message = StringLocalizer["Payouts approved"].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
break;
@ -389,9 +395,9 @@ namespace BTCPayServer.Controllers
{
if (handler is { })
return await handler.InitiatePayment(payoutIds);
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Paying via this payment method is not supported",
Message = StringLocalizer["Paying via this payment method is not supported"].Value,
Severity = StatusMessageModel.StatusSeverity.Error
});
break;
@ -410,10 +416,10 @@ namespace BTCPayServer.Controllers
continue;
var result =
await _pullPaymentService.MarkPaid(new MarkPayoutRequest() { PayoutId = payout.Id });
await _pullPaymentService.MarkPaid(new MarkPayoutRequest { PayoutId = payout.Id });
if (result != MarkPayoutRequest.PayoutPaidResult.Ok)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = MarkPayoutRequest.GetErrorMessage(result),
Severity = StatusMessageModel.StatusSeverity.Error
@ -428,9 +434,9 @@ namespace BTCPayServer.Controllers
}
}
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Payouts marked as paid",
Message = StringLocalizer["Payouts marked as paid"].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
break;
@ -439,9 +445,9 @@ namespace BTCPayServer.Controllers
case "cancel":
await _pullPaymentService.Cancel(
new PullPaymentHostedService.CancelRequest(payoutIds, new[] { storeId }));
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Payouts archived",
Message = StringLocalizer["Payouts archived"].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
break;
@ -482,7 +488,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a payout.",
Message = StringLocalizer["You must enable at least one payment method before creating a payout."].Value,
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });

View File

@ -146,6 +146,7 @@ public partial class UIStoresController
Crypto = network.CryptoCode,
PaymentMethodId = handler.PaymentMethodId,
WalletSupported = network.WalletSupported,
ReadonlyWallet = network.ReadonlyWallet,
Value = value,
WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null,

View File

@ -35,7 +35,8 @@ public partial class UIStoresController
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = $"You need to configure email settings before this feature works. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
Html = "You need to configure email settings before this feature works." +
$" <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
});
}
}
@ -76,11 +77,11 @@ public partial class UIStoresController
.Any(s => !MailboxAddressValidator.TryParse(s, out _)))
{
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
"Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname <test@example.com>'");
StringLocalizer["Invalid mailbox address provided. Valid formats are: '{0}' or '{1}'", "test@example.com", "Firstname Lastname <test@example.com>"]);
}
else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To))
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
"Either recipient or \"Send the email to the buyer\" is required");
StringLocalizer["Either recipient or \"Send the email to the buyer\" is required"]);
}
if (!ModelState.IsValid)
@ -101,7 +102,7 @@ public partial class UIStoresController
if (store.SetStoreBlob(blob))
{
await _storeRepo.UpdateStore(store);
message += "Store email rules saved. ";
message += StringLocalizer["Store email rules saved."] + " ";
}
if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase))
@ -122,16 +123,16 @@ public partial class UIStoresController
.ToArray();
emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body);
message += "Test email sent — please verify you received it.";
message += StringLocalizer["Test email sent — please verify you received it."];
}
else
{
message += "Complete the email setup to send test emails.";
message += StringLocalizer["Complete the email setup to send test emails."];
}
}
catch (Exception ex)
{
TempData[WellKnownTempData.ErrorMessage] = message + "Error sending test email: " + ex.Message;
TempData[WellKnownTempData.ErrorMessage] = message + StringLocalizer["Error sending test email: {0}", ex.Message].Value;
return RedirectToAction("StoreEmails", new { storeId });
}
}
@ -222,14 +223,14 @@ public partial class UIStoresController
return View(model);
var settings = useCustomSMTP ? model.Settings : model.FallbackSettings;
using var client = await settings.CreateSmtpClient();
var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false);
var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", StringLocalizer["You received it, the BTCPay Server SMTP settings work."], false);
await client.SendAsync(message);
await client.DisconnectAsync(true);
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email sent to {0}. Please verify you received it.", model.TestEmail].Value;
}
catch (Exception ex)
{
TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message;
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Error: {0}", ex.Message].Value;
}
return View(model);
}
@ -239,13 +240,13 @@ public partial class UIStoresController
storeBlob.EmailSettings.Password = null;
store.SetStoreBlob(storeBlob);
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email server password reset"].Value;
}
if (useCustomSMTP)
{
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
{
ModelState.AddModelError("Settings.From", "Invalid email");
ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]);
}
if (!ModelState.IsValid)
return View(model);
@ -257,7 +258,7 @@ public partial class UIStoresController
storeBlob.EmailSettings = model.Settings;
store.SetStoreBlob(storeBlob);
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = "Email settings modified";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email settings modified"].Value;
}
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
}

View File

@ -67,7 +67,7 @@ public partial class UIStoresController
if (webhook is null)
return NotFound();
return View("Confirm", new ConfirmModel("Delete webhook", "This webhook will be removed from this store. Are you sure?", "Delete"));
return View("Confirm", new ConfirmModel(StringLocalizer["Delete webhook"], StringLocalizer["This webhook will be removed from this store. Are you sure?"], StringLocalizer["Delete"]));
}
[HttpPost("{storeId}/webhooks/{webhookId}/remove")]
@ -79,7 +79,7 @@ public partial class UIStoresController
return NotFound();
await _storeRepo.DeleteWebhook(CurrentStore.Id, webhookId);
TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Webhook successfully deleted"].Value;
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
}
@ -91,7 +91,7 @@ public partial class UIStoresController
return View(nameof(ModifyWebhook), viewModel);
await _storeRepo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob());
TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The webhook has been created"].Value;
return RedirectToAction(nameof(Webhooks), new { storeId });
}
@ -123,7 +123,7 @@ public partial class UIStoresController
return View(nameof(ModifyWebhook), viewModel);
await _storeRepo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob());
TempData[WellKnownTempData.SuccessMessage] = "The webhook has been updated";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The webhook has been updated"].Value;
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
}
@ -146,11 +146,11 @@ public partial class UIStoresController
if (result.Success)
{
TempData[WellKnownTempData.SuccessMessage] = $"{viewModel.Type} event delivered successfully! Delivery ID is {result.DeliveryId}";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["{0} event delivered successfully! Delivery ID is {1}", viewModel.Type, result.DeliveryId!].Value;
}
else
{
TempData[WellKnownTempData.ErrorMessage] = $"{viewModel.Type} event could not be delivered. Error message received: {(result.ErrorMessage ?? "unknown")}";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["{0} event could not be delivered. Error message received: {1}", viewModel.Type, result.ErrorMessage ?? StringLocalizer["unknown"].Value].Value;
}
return View(nameof(TestWebhook));
@ -168,7 +168,7 @@ public partial class UIStoresController
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(ModifyWebhook),
new
{

View File

@ -115,7 +115,7 @@ public partial class UIStoresController
if (vm.CryptoCode == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
ModelState.AddModelError(nameof(vm.CryptoCode), StringLocalizer["Invalid network"]);
return View(vm);
}
@ -132,7 +132,7 @@ public partial class UIStoresController
{
if (string.IsNullOrEmpty(vm.ConnectionString))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string");
ModelState.AddModelError(nameof(vm.ConnectionString), StringLocalizer["Please provide a connection string"]);
return View(vm);
}
paymentMethod = new LightningPaymentMethodConfig { ConnectionString = vm.ConnectionString };
@ -143,7 +143,7 @@ public partial class UIStoresController
JToken.FromObject(paymentMethod, handler.Serializer), User, oldConf is null ? null : JToken.FromObject(oldConf, handler.Serializer));
await handler.ValidatePaymentMethodConfig(ctx);
if (ctx.MissingPermission is not null)
ModelState.AddModelError(nameof(vm.ConnectionString), "You do not have the permissions to change this settings");
ModelState.AddModelError(nameof(vm.ConnectionString), StringLocalizer["You do not have the permissions to change this settings"]);
if (!ModelState.IsValid)
return View(vm);
@ -159,7 +159,7 @@ public partial class UIStoresController
});
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["{0} Lightning node updated.", network.CryptoCode].Value;
return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode });
case "test":
@ -172,9 +172,10 @@ public partial class UIStoresController
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
await handler.TestConnection(info.First(), cts.Token);
}
TempData[WellKnownTempData.SuccessMessage] = "Connection to the Lightning node successful" + (hasPublicAddress
? $". Your node address: {info.First()}"
: ", but no public address has been configured");
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Connection to the Lightning node successful."].Value + " " +
(hasPublicAddress
? StringLocalizer["Your node address: {0}", info.First()].Value
: StringLocalizer["No public address has been configured."].Value);
}
catch (Exception ex)
{
@ -202,7 +203,7 @@ public partial class UIStoresController
var lightning = GetConfig<LightningPaymentMethodConfig>(lnId, store);
if (lightning == null)
{
TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings.";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["You need to connect to a Lightning node before adjusting its settings."].Value;
return RedirectToAction(nameof(SetupLightningNode), new { storeId, cryptoCode });
}
@ -241,7 +242,7 @@ public partial class UIStoresController
if (vm.CryptoCode == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
ModelState.AddModelError(nameof(vm.CryptoCode), StringLocalizer["Invalid network"]);
return View(vm);
}
@ -289,7 +290,7 @@ public partial class UIStoresController
{
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning settings successfully updated.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["{0} Lightning settings successfully updated.", network.CryptoCode].Value;
}
return RedirectToAction(nameof(LightningSettings), new { vm.StoreId, vm.CryptoCode });

View File

@ -108,7 +108,7 @@ public partial class UIStoresController
if (fileContent is null || !_onChainWalletParsers.TryParseWalletFile(fileContent, network, out strategy, out _))
{
ModelState.AddModelError(nameof(vm.WalletFile), $"Import failed, make sure you import a compatible wallet format");
ModelState.AddModelError(nameof(vm.WalletFile), StringLocalizer["Import failed, make sure you import a compatible wallet format"]);
return View(vm.ViewName, vm);
}
}
@ -116,7 +116,7 @@ public partial class UIStoresController
{
if (!_onChainWalletParsers.TryParseWalletFile(vm.WalletFileContent, network, out strategy, out var error))
{
ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
ModelState.AddModelError(nameof(vm.WalletFileContent), StringLocalizer["QR import failed: {0}", error]);
return View(vm.ViewName, vm);
}
}
@ -145,7 +145,7 @@ public partial class UIStoresController
}
catch (Exception ex)
{
ModelState.AddModelError(nameof(vm.DerivationScheme), $"Invalid wallet format: {ex.Message}");
ModelState.AddModelError(nameof(vm.DerivationScheme), StringLocalizer["Invalid wallet format: {0}", ex.Message]);
return View(vm.ViewName, vm);
}
}
@ -157,14 +157,14 @@ public partial class UIStoresController
}
catch
{
ModelState.AddModelError(nameof(vm.Config), "Config file was not in the correct format");
ModelState.AddModelError(nameof(vm.Config), StringLocalizer["Config file was not in the correct format"]);
return View(vm.ViewName, vm);
}
}
if (strategy is null)
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key");
ModelState.AddModelError(nameof(vm.DerivationScheme), StringLocalizer["Please provide your extended public key"]);
return View(vm.ViewName, vm);
}
@ -184,13 +184,13 @@ public partial class UIStoresController
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid derivation scheme");
ModelState.AddModelError(nameof(vm.DerivationScheme), StringLocalizer["Invalid derivation scheme"]);
return View(vm.ViewName, vm);
}
await _storeRepo.UpdateStore(store);
_eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) });
TempData[WellKnownTempData.SuccessMessage] = $"Wallet settings for {network.CryptoCode} have been updated.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Wallet settings for {0} have been updated.", network.CryptoCode].Value;
// This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(WalletSettings), new { storeId = vm.StoreId, cryptoCode = vm.CryptoCode });
@ -287,7 +287,7 @@ public partial class UIStoresController
if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic))
{
ModelState.AddModelError(nameof(request.ExistingMnemonic), "Please provide your existing seed");
ModelState.AddModelError(nameof(request.ExistingMnemonic), StringLocalizer["Please provide your existing seed"]);
return View(vm.ViewName, vm);
}
@ -305,7 +305,7 @@ public partial class UIStoresController
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"There was an error generating your wallet: {e.Message}"
Message = StringLocalizer["There was an error generating your wallet: {0}", e.Message].Value
});
return View(vm.ViewName, vm);
}
@ -343,7 +343,7 @@ public partial class UIStoresController
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = "<span class='text-centered'>Your wallet has been generated.</span>"
Html = "<span class='text-centered'>" + StringLocalizer["Your wallet has been generated."].Value + "</span>"
});
var seedVm = new RecoverySeedBackupViewModel
{
@ -363,7 +363,7 @@ public partial class UIStoresController
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = "Please check your addresses and confirm."
Message = StringLocalizer["Please check your addresses and confirm."].Value
});
return result;
}
@ -380,7 +380,7 @@ public partial class UIStoresController
return checkResult;
}
TempData[WellKnownTempData.SuccessMessage] = $"Wallet settings for {network.CryptoCode} have been updated.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Wallet settings for {0} have been updated.", network.CryptoCode].Value;
var walletId = new WalletId(storeId, cryptoCode);
return RedirectToAction(nameof(UIWalletsController.WalletTransactions), "UIWallets", new { walletId });
@ -608,7 +608,7 @@ public partial class UIStoresController
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "The seed was not found"
Message = StringLocalizer["The seed was not found"].Value
});
return RedirectToAction(nameof(WalletSettings));
@ -628,9 +628,9 @@ public partial class UIStoresController
return View("Confirm", new ConfirmModel
{
Title = $"Replace {network.CryptoCode} wallet",
Title = StringLocalizer["Replace {0} wallet", network.CryptoCode],
Description = WalletReplaceWarning(derivation.IsHotWallet),
Action = "Setup new wallet"
Action = StringLocalizer["Setup new wallet"]
});
}
@ -667,9 +667,9 @@ public partial class UIStoresController
return View("Confirm", new ConfirmModel
{
Title = $"Remove {network.CryptoCode} wallet",
Title = StringLocalizer["Remove {0} wallet", network.CryptoCode],
Description = WalletRemoveWarning(derivation.IsHotWallet, network.CryptoCode),
Action = "Remove"
Action = StringLocalizer["Remove"]
});
}

View File

@ -52,7 +52,7 @@ public partial class UIStoresController
}
catch
{
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), StringLocalizer["Invalid currency pairs (should be for example: {0})", "BTC_USD,BTC_CAD,BTC_JPY"]);
}
if (!ModelState.IsValid)
{
@ -71,7 +71,7 @@ public partial class UIStoresController
{
errors ??= [];
var errorString = string.Join(", ", errors.ToArray());
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
ModelState.AddModelError(nameof(model.Script), StringLocalizer["Parsing error: {0}", errorString]);
FillFromStore(model, blob);
return View(model);
}
@ -90,7 +90,7 @@ public partial class UIStoresController
{
if (string.IsNullOrWhiteSpace(model.ScriptTest))
{
ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)");
ModelState.AddModelError(nameof(model.ScriptTest), StringLocalizer["Fill out currency pair to test for (like {0})", "BTC_USD,BTC_CAD"]);
return View(model);
}
var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries);
@ -100,7 +100,7 @@ public partial class UIStoresController
{
if (!CurrencyPair.TryParse(pair, out var currencyPair))
{
ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)");
ModelState.AddModelError(nameof(model.ScriptTest), StringLocalizer["Invalid currency pair '{0}' (it should be formatted like {1})", pair, "BTC_USD,BTC_CAD"]);
return View(model);
}
pairs.Add(currencyPair);
@ -125,7 +125,7 @@ public partial class UIStoresController
if (model.PreferredExchange is not null && !model.AvailableExchanges.Any(a => a.Id == model.PreferredExchange))
{
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange");
ModelState.AddModelError(nameof(model.PreferredExchange), StringLocalizer["Unsupported exchange"]);
return View(model);
}
@ -147,11 +147,11 @@ public partial class UIStoresController
{
return View("Confirm", new ConfirmModel
{
Action = "Continue",
Title = "Rate rule scripting",
Description = scripting ?
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
Action = StringLocalizer["Continue"],
Title = StringLocalizer["Rate rule scripting"],
Description = scripting
? StringLocalizer["This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"]
: StringLocalizer["This action will delete your rate script. Are you sure to turn off rate rules scripting?"],
ButtonClass = scripting ? "btn-primary" : "btn-danger"
});
}
@ -176,7 +176,7 @@ public partial class UIStoresController
vm.AvailableExchanges = sources;
var exchange = storeBlob.GetPreferredExchange(_defaultRules);
var chosenSource = sources.First(r => r.Id == exchange);
vm.Exchanges = UIUserStoresController.GetExchangesSelectList(_rateFactory, _defaultRules, storeBlob);
vm.Exchanges = _userStoresController.GetExchangesSelectList(storeBlob);
vm.PreferredExchange = vm.Exchanges.SelectedValue as string;
vm.PreferredResolvedExchange = chosenSource.Id;
vm.RateSource = chosenSource.Url;

View File

@ -77,13 +77,13 @@ public partial class UIStoresController
StoreRoleId roleId;
if (role == "create")
{
successMessage = "Role created";
successMessage = StringLocalizer["Role created"];
role = viewModel.Role;
roleId = new StoreRoleId(storeId, role);
}
else
{
successMessage = "Role updated";
successMessage = StringLocalizer["Role updated"];
roleId = new StoreRoleId(storeId, role);
var storeRole = await storeRepository.GetStoreRole(roleId);
if (storeRole == null)
@ -98,15 +98,15 @@ public partial class UIStoresController
var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies);
if (r is null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Role could not be updated"
Message = StringLocalizer["Role could not be updated"].Value
});
return View(viewModel);
}
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = successMessage
@ -128,11 +128,11 @@ public partial class UIStoresController
return View("Confirm",
roleData.IsUsed is true
? new ConfirmModel("Delete role",
? new ConfirmModel(StringLocalizer["Delete role"],
$"Unable to proceed: The role <strong>{_html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
: new ConfirmModel("Delete role",
: new ConfirmModel(StringLocalizer["Delete role"],
$"The role <strong>{_html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
"Delete"));
StringLocalizer["Delete"]));
}
[HttpPost("{storeId}/roles/{role}/delete")]
@ -152,7 +152,7 @@ public partial class UIStoresController
}
await storeRepository.RemoveStoreRole(roleId);
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Role deleted"].Value;
return RedirectToAction(nameof(ListRoles), new { storeId });
}
}

View File

@ -89,7 +89,7 @@ public partial class UIStoresController
blob.MonitoringExpiration = TimeSpan.FromMinutes(model.MonitoringExpiration);
if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor))
{
ModelState.AddModelError(nameof(model.BrandColor), "The brand color needs to be a valid hex color code");
ModelState.AddModelError(nameof(model.BrandColor), StringLocalizer["The brand color needs to be a valid hex color code"]);
return View(model);
}
blob.BrandColor = model.BrandColor;
@ -103,18 +103,18 @@ public partial class UIStoresController
{
if (model.LogoFile.Length > 1_000_000)
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB");
ModelState.AddModelError(nameof(model.LogoFile), StringLocalizer["The uploaded logo file should be less than {0}", "1MB"]);
}
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
ModelState.AddModelError(nameof(model.LogoFile), StringLocalizer["The uploaded logo file needs to be an image"]);
}
else
{
var formFile = await model.LogoFile.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
ModelState.AddModelError(nameof(model.LogoFile), StringLocalizer["The uploaded logo file needs to be an image"]);
}
else
{
@ -127,7 +127,7 @@ public partial class UIStoresController
}
catch (Exception e)
{
ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}");
ModelState.AddModelError(nameof(model.LogoFile), StringLocalizer["Could not save logo: {0}", e.Message]);
}
}
}
@ -142,15 +142,15 @@ public partial class UIStoresController
{
if (model.CssFile.Length > 1_000_000)
{
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file should be less than 1MB");
ModelState.AddModelError(nameof(model.CssFile), StringLocalizer["The uploaded file should be less than {0}", "1MB"]);
}
else if (!model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
{
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file");
ModelState.AddModelError(nameof(model.CssFile), StringLocalizer["The uploaded file needs to be a CSS file"]);
}
else if (!model.CssFile.FileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase))
{
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file");
ModelState.AddModelError(nameof(model.CssFile), StringLocalizer["The uploaded file needs to be a CSS file"]);
}
else
{
@ -162,7 +162,7 @@ public partial class UIStoresController
}
catch (Exception e)
{
ModelState.AddModelError(nameof(model.CssFile), $"Could not save CSS file: {e.Message}");
ModelState.AddModelError(nameof(model.CssFile), StringLocalizer["Could not save CSS file: {0}", e.Message]);
}
}
}
@ -211,7 +211,7 @@ public partial class UIStoresController
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult DeleteStore(string storeId)
{
return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete"));
return View("Confirm", new ConfirmModel(StringLocalizer["Delete store"], StringLocalizer["The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?"], StringLocalizer["Delete"]));
}
[HttpPost("{storeId}/delete")]
@ -305,18 +305,18 @@ public partial class UIStoresController
{
if (model.SoundFile.Length > 1_000_000)
{
ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file should be less than 1MB");
ModelState.AddModelError(nameof(model.SoundFile), StringLocalizer["The uploaded sound file should be less than {0}", "1MB"]);
}
else if (!model.SoundFile.ContentType.StartsWith("audio/", StringComparison.InvariantCulture))
{
ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file");
ModelState.AddModelError(nameof(model.SoundFile), StringLocalizer["The uploaded sound file needs to be an audio file"]);
}
else
{
var formFile = await model.SoundFile.Bufferize();
if (!FileTypeDetector.IsAudio(formFile.Buffer, formFile.FileName))
{
ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file");
ModelState.AddModelError(nameof(model.SoundFile), StringLocalizer["The uploaded sound file needs to be an audio file"]);
}
else
{
@ -330,7 +330,7 @@ public partial class UIStoresController
}
catch (Exception e)
{
ModelState.AddModelError(nameof(model.SoundFile), $"Could not save sound: {e.Message}");
ModelState.AddModelError(nameof(model.SoundFile), StringLocalizer["Could not save sound: {0}", e.Message]);
}
}
}

View File

@ -44,7 +44,7 @@ public partial class UIStoresController
var token = await _tokenRepository.GetToken(tokenId);
if (token == null || token.StoreId != CurrentStore.Id)
return NotFound();
return View("Confirm", new ConfirmModel("Revoke the token", $"The access token with the label <strong>{_html.Encode(token.Label)}</strong> will be revoked. Do you wish to continue?", "Revoke"));
return View("Confirm", new ConfirmModel(StringLocalizer["Revoke the token"], $"The access token with the label <strong>{_html.Encode(token.Label)}</strong> will be revoked. Do you wish to continue?", "Revoke"));
}
[HttpPost("{storeId}/tokens/{tokenId}/revoke")]
@ -243,14 +243,14 @@ public partial class UIStoresController
StoreNotConfigured = store.GetPaymentMethodConfigs(_handlers).All(p => excludeFilter.Match(p.Key));
TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode;
TempData[WellKnownTempData.SuccessMessage] = $"Server initiated pairing code: {pairingCode}";
return RedirectToAction(nameof(ListTokens), new
{
storeId = store.Id, pairingCode
});
}
TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})";
TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed: {pairingResult}";
return RedirectToAction(nameof(ListTokens), new
{
storeId = store.Id

View File

@ -39,7 +39,7 @@ public partial class UIStoresController
var roles = await _storeRepo.GetStoreRoles(CurrentStore.Id);
if (roles.All(role => role.Id != vm.Role))
{
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
ModelState.AddModelError(nameof(vm.Role), StringLocalizer["Invalid role"]);
return View(vm);
}
@ -116,9 +116,9 @@ public partial class UIStoresController
var isOwner = user.StoreRole.Id == StoreRoleId.Owner.Id;
var isLastOwner = isOwner && storeUsers.Count(u => u.StoreRole.Id == StoreRoleId.Owner.Id) == 1;
if (isLastOwner && roleId != StoreRoleId.Owner)
TempData[WellKnownTempData.ErrorMessage] = $"User {user.Email} is the last owner. Their role cannot be changed.";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["User {0} is the last owner. Their role cannot be changed.", user.Email].Value;
else if (await _storeRepo.AddOrUpdateStoreUser(storeId, userId, roleId))
TempData[WellKnownTempData.SuccessMessage] = $"The role of {user.Email} has been changed to {vm.Role}.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The role of {0} has been changed to {1}.", user.Email, vm.Role].Value;
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
}
@ -127,9 +127,9 @@ public partial class UIStoresController
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
{
if (await _storeRepo.RemoveStoreUser(storeId, userId))
TempData[WellKnownTempData.SuccessMessage] = "User removed successfully.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["User removed successfully."].Value;
else
TempData[WellKnownTempData.ErrorMessage] = "Removing this user would result in the store having no owner.";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Removing this user would result in the store having no owner."].Value;
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
}

View File

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using StoreData = BTCPayServer.Data.StoreData;
@ -58,9 +59,11 @@ public partial class UIStoresController : Controller
DefaultRulesCollection defaultRules,
EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers,
UIUserStoresController userStoresController,
UriResolver uriResolver,
SettingsRepository settingsRepository,
CurrencyNameTable currencyNameTable,
IStringLocalizer stringLocalizer,
EventAggregator eventAggregator)
{
_rateFactory = rateFactory;
@ -82,6 +85,7 @@ public partial class UIStoresController : Controller
_externalServiceOptions = externalServiceOptions;
_emailSenderFactory = emailSenderFactory;
_onChainWalletParsers = onChainWalletParsers;
_userStoresController = userStoresController;
_uriResolver = uriResolver;
_settingsRepository = settingsRepository;
_currencyNameTable = currencyNameTable;
@ -91,6 +95,7 @@ public partial class UIStoresController : Controller
_dataProtector = dataProtector.CreateProtector("ConfigProtector");
_webhookNotificationManager = webhookNotificationManager;
_lightningNetworkOptions = lightningNetworkOptions.Value;
StringLocalizer = stringLocalizer;
}
private readonly BTCPayServerOptions _btcpayServerOptions;
@ -115,6 +120,7 @@ public partial class UIStoresController : Controller
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly WalletFileParsers _onChainWalletParsers;
private readonly UIUserStoresController _userStoresController;
private readonly UriResolver _uriResolver;
private readonly EventAggregator _eventAggregator;
private readonly IHtmlHelper _html;
@ -123,6 +129,7 @@ public partial class UIStoresController : Controller
private readonly IDataProtector _dataProtector;
public string? GeneratedPairingCode { get; set; }
public IStringLocalizer StringLocalizer { get; }
[TempData]
private bool StoreNotConfigured { get; set; }

View File

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
@ -13,6 +14,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Controllers
{
@ -21,6 +23,7 @@ namespace BTCPayServer.Controllers
public class UIUserStoresController : Controller
{
private readonly StoreRepository _repo;
private readonly IStringLocalizer StringLocalizer;
private readonly SettingsRepository _settingsRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly DefaultRulesCollection _defaultRules;
@ -31,10 +34,12 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
DefaultRulesCollection defaultRules,
StoreRepository storeRepository,
IStringLocalizer stringLocalizer,
RateFetcher rateFactory,
SettingsRepository settingsRepository)
{
_repo = storeRepository;
StringLocalizer = stringLocalizer;
_userManager = userManager;
_defaultRules = defaultRules;
_rateFactory = rateFactory;
@ -95,7 +100,7 @@ namespace BTCPayServer.Controllers
store.SetStoreBlob(blob);
await _repo.CreateStore(GetUserId(), store);
CreatedStoreId = store.Id;
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
TempData.SetStatusSuccess(StringLocalizer["Store successfully created"]);
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new
{
storeId = store.Id
@ -109,7 +114,7 @@ namespace BTCPayServer.Controllers
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel($"Delete store {store.StoreName}", "This store will still be accessible to users sharing it", "Delete"));
return View("Confirm", new ConfirmModel(StringLocalizer["Delete store {0}", store.StoreName], StringLocalizer["This store will still be accessible to users sharing it"], "Delete"));
}
[HttpPost("{storeId}/me/delete")]
@ -121,24 +126,23 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
await _repo.RemoveStore(storeId, userId);
TempData[WellKnownTempData.SuccessMessage] = "Store removed successfully";
TempData.SetStatusSuccess(StringLocalizer["Store removed successfully"]);
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
private string GetUserId() => _userManager.GetUserId(User);
private SelectList GetExchangesSelectList(StoreBlob storeBlob) => GetExchangesSelectList(_rateFactory, _defaultRules, storeBlob);
internal static SelectList GetExchangesSelectList(RateFetcher rateFetcher, DefaultRulesCollection defaultRules, StoreBlob storeBlob)
internal SelectList GetExchangesSelectList(StoreBlob storeBlob)
{
if (storeBlob is null)
storeBlob = new StoreBlob();
var defaultExchange = defaultRules.GetRecommendedExchange(storeBlob.DefaultCurrency);
var exchanges = rateFetcher.RateProviderFactory
var defaultExchange = _defaultRules.GetRecommendedExchange(storeBlob.DefaultCurrency);
var exchanges = _rateFactory.RateProviderFactory
.AvailableRateProviders
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase)
.ToList();
var exchange = exchanges.First(e => e.Id == defaultExchange);
exchanges.Insert(0, new(null, $"Recommendation ({exchange.DisplayName})", ""));
exchanges.Insert(0, new(null, StringLocalizer["Recommendation ({0})", exchange.DisplayName], ""));
var chosen = exchanges.FirstOrDefault(f => f.Id == storeBlob.PreferredExchange) ?? exchanges.First();
return new SelectList(exchanges, nameof(chosen.Id), nameof(chosen.DisplayName), chosen.Id);
}

View File

@ -58,7 +58,7 @@ namespace BTCPayServer.Controllers
var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
if (psbt == null)
throw new NotSupportedException("You need to update your version of NBXplorer");
throw new NotSupportedException(StringLocalizer["You need to update your version of NBXplorer"]);
// Not supported by coldcard, remove when they do support it
psbt.PSBT.GlobalXPubs.Clear();
return psbt;
@ -92,7 +92,7 @@ namespace BTCPayServer.Controllers
if (bumpableUTXOs.Length == 0)
{
TempData[WellKnownTempData.ErrorMessage] = "There isn't any UTXO available to bump fee";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["There isn't any UTXO available to bump fee"].Value;
return LocalRedirect(returnUrl);
}
Money bumpFee = Money.Zero;
@ -267,10 +267,10 @@ namespace BTCPayServer.Controllers
psbt = await ExplorerClientProvider.UpdatePSBT(derivationSchemeSettings, psbt);
if (psbt == null)
{
TempData[WellKnownTempData.ErrorMessage] = "You need to update your version of NBXplorer";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["You need to update your version of NBXplorer"].Value;
return View(vm);
}
TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["PSBT updated!"].Value;
return RedirectToWalletPSBT(new WalletPSBTViewModel
{
PSBT = psbt.ToBase64(),
@ -479,7 +479,7 @@ namespace BTCPayServer.Controllers
if (vm.InvalidPSBT || psbt is null)
{
if (vm.InvalidPSBT)
vm.Errors.Add("Invalid PSBT");
vm.Errors.Add(StringLocalizer["Invalid PSBT"]);
return View(nameof(WalletPSBT), vm);
}
DerivationSchemeSettings derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
@ -537,15 +537,15 @@ namespace BTCPayServer.Controllers
}
catch (PayjoinReceiverException ex)
{
error = $"The payjoin receiver could not complete the payjoin: {ex.Message}";
error = StringLocalizer["The payjoin receiver could not complete the payjoin: {0}", ex.Message];
}
catch (PayjoinSenderException ex)
{
error = $"We rejected the receiver's payjoin proposal: {ex.Message}";
error = StringLocalizer["We rejected the receiver's payjoin proposal: {0}", ex.Message];
}
catch (Exception ex)
{
error = $"Unexpected payjoin error: {ex.Message}";
error = StringLocalizer["Unexpected payjoin error: {0}", ex.Message];
}
//we possibly exposed the tx to the receiver, so we need to broadcast straight away
@ -554,9 +554,7 @@ namespace BTCPayServer.Controllers
{
Severity = StatusMessageModel.StatusSeverity.Warning,
AllowDismiss = false,
Html = $"The payjoin transaction could not be created.<br/>" +
$"The original transaction was broadcasted instead. ({psbt.ExtractTransaction().GetHash()})<br/><br/>" +
$"{error}"
Html = $"The payjoin transaction could not be created.<br/>The original transaction was broadcasted instead ({psbt.ExtractTransaction().GetHash()})<br/><br/>" + error
});
return await WalletPSBTReady(walletId, vm, "broadcast");
case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):
@ -576,14 +574,14 @@ namespace BTCPayServer.Controllers
{
Severity = StatusMessageModel.StatusSeverity.Warning,
AllowDismiss = false,
Html = $"The payjoin transaction could not be broadcasted.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>The transaction has been reverted back to its original format and has been broadcast."
Html = $"The payjoin transaction could not be broadcasted: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}<br/>The transaction has been reverted back to its original format and has been broadcast."
});
vm.SigningContext.PSBT = vm.SigningContext.OriginalPSBT;
vm.SigningContext.OriginalPSBT = null;
return await WalletPSBTReady(walletId, vm, "broadcast");
}
vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
vm.Errors.Add(StringLocalizer["RPC Error while broadcasting: {0}", $"{broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"]);
return View(nameof(WalletPSBT), vm);
}
else
@ -595,13 +593,13 @@ namespace BTCPayServer.Controllers
}
catch (Exception ex)
{
vm.Errors.Add("Error while broadcasting: " + ex.Message);
vm.Errors.Add(StringLocalizer["Error while broadcasting: {0}", ex.Message]);
return View(nameof(WalletPSBT), vm);
}
if (!TempData.HasStatusMessage())
{
TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Transaction broadcasted successfully ({0})", transaction.GetHash()].Value;
}
if (!string.IsNullOrEmpty(vm.ReturnUrl))
{
@ -620,7 +618,7 @@ namespace BTCPayServer.Controllers
await FetchTransactionDetails(walletId, derivationSchemeSettings, vm, network);
return View("WalletPSBTDecoded", vm);
default:
vm.Errors.Add("Unknown command");
vm.Errors.Add(StringLocalizer["Unknown command"]);
return View(nameof(WalletPSBT), vm);
}
}
@ -646,7 +644,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
sourcePSBT = sourcePSBT.Combine(psbt);
TempData[WellKnownTempData.SuccessMessage] = "PSBT Successfully combined!";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["PSBT Successfully combined!"].Value;
return RedirectToWalletPSBT(new WalletPSBTViewModel
{
PSBT = sourcePSBT.ToBase64(),

View File

@ -35,6 +35,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
@ -57,6 +58,7 @@ namespace BTCPayServer.Controllers
private ExplorerClientProvider ExplorerClientProvider { get; }
public IServiceProvider ServiceProvider { get; }
public RateFetcher RateFetcher { get; }
public IStringLocalizer StringLocalizer { get; }
private readonly UserManager<ApplicationUser> _userManager;
private readonly NBXplorerDashboard _dashboard;
@ -99,6 +101,7 @@ namespace BTCPayServer.Controllers
DefaultRulesCollection defaultRules,
PaymentMethodHandlerDictionary handlers,
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
IStringLocalizer stringLocalizer,
TransactionLinkProviders transactionLinkProviders)
{
_currencyTable = currencyTable;
@ -124,6 +127,7 @@ namespace BTCPayServer.Controllers
_pullPaymentHostedService = pullPaymentHostedService;
ServiceProvider = serviceProvider;
_walletHistogramService = walletHistogramService;
StringLocalizer = stringLocalizer;
}
[HttpPost]
@ -753,7 +757,7 @@ namespace BTCPayServer.Controllers
{
Destination = new AddressClaimDestination(
BitcoinAddress.Create(output.DestinationAddress, network.NBitcoinNetwork)),
Value = output.Amount,
ClaimedAmount = output.Amount,
PayoutMethodId = pmi,
StoreId = walletId.StoreId,
PreApprove = true,
@ -773,7 +777,7 @@ namespace BTCPayServer.Controllers
message = "Payouts scheduled:<br/>";
}
message += $"{claimRequest.Value} to {claimRequest.Destination.ToString()}<br/>";
message += $"{claimRequest.ClaimedAmount} to {claimRequest.Destination.ToString()}<br/>";
}
else
@ -787,10 +791,10 @@ namespace BTCPayServer.Controllers
switch (response.Result)
{
case ClaimRequest.ClaimResult.Duplicate:
errorMessage += $"{claimRequest.Value} to {claimRequest.Destination.ToString()} - address reuse<br/>";
errorMessage += $"{claimRequest.ClaimedAmount} to {claimRequest.Destination.ToString()} - address reuse<br/>";
break;
case ClaimRequest.ClaimResult.AmountTooLow:
errorMessage += $"{claimRequest.Value} to {claimRequest.Destination.ToString()} - amount too low<br/>";
errorMessage += $"{claimRequest.ClaimedAmount} to {claimRequest.Destination.ToString()} - amount too low<br/>";
break;
}
}
@ -908,18 +912,17 @@ namespace BTCPayServer.Controllers
try
{
address = BitcoinAddress.Create(bip21, network.NBitcoinNetwork);
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
vm.Outputs.Add(new WalletSendModel.TransactionOutput
{
DestinationAddress = address.ToString()
}
);
});
}
catch
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "The provided BIP21 payment URI was malformed"
Message = StringLocalizer["The provided BIP21 payment URI was malformed"].Value
});
}
}
@ -1256,7 +1259,7 @@ namespace BTCPayServer.Controllers
selectedTransactions ??= Array.Empty<string>();
if (selectedTransactions.Length == 0)
{
TempData[WellKnownTempData.ErrorMessage] = $"No transaction selected";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["No transaction selected"].Value;
return RedirectToAction(nameof(WalletTransactions), new { walletId });
}
@ -1287,12 +1290,12 @@ namespace BTCPayServer.Controllers
.PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);
if (result.TotalPruned == 0)
{
TempData[WellKnownTempData.SuccessMessage] = "The wallet is already pruned";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The wallet is already pruned"].Value;
}
else
{
TempData[WellKnownTempData.SuccessMessage] =
$"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)";
StringLocalizer["The wallet has been successfully pruned ({0} transactions have been removed from the history)", result.TotalPruned].Value;
}
return RedirectToAction(nameof(WalletTransactions), new { walletId });
@ -1455,11 +1458,11 @@ namespace BTCPayServer.Controllers
;
if (await WalletRepository.RemoveWalletLabels(walletId, labels))
{
TempData[WellKnownTempData.SuccessMessage] = "The label has been successfully removed.";
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The label has been successfully removed."].Value;
}
else
{
TempData[WellKnownTempData.ErrorMessage] = "The label could not be removed.";
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["The label could not be removed."].Value;
}
return RedirectToAction(nameof(WalletLabels), new { walletId });

View File

@ -237,7 +237,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
}
await context.SaveChangesAsync();
}
return new StatusMessageModel()
return new StatusMessageModel
{
Message = "Payout payments have been marked confirmed",
Severity = StatusMessageModel.StatusSeverity.Success

View File

@ -6,6 +6,5 @@ namespace BTCPayServer.Data
{
public string? Id { get; }
decimal? Amount { get; }
bool IsExplicitAmountMinimum => false;
}
}

View File

@ -23,6 +23,5 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public uint256 PaymentHash { get; }
public string Id => PaymentHash.ToString();
public decimal? Amount { get; }
public bool IsExplicitAmountMinimum => true;
}
};
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
@ -208,5 +209,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
"UILightningLikePayout", new { cryptoCode, payoutIds }));
}
public ResourceTracker<string> PayoutsPaymentProcessing { get; } = new ResourceTracker<string>();
}
}

View File

@ -12,6 +12,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Security;
using BTCPayServer.Services;
@ -32,6 +33,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public class UILightningLikePayoutController : Controller
{
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly LightningAutomatedPayoutSenderFactory _lightningAutomatedPayoutSenderFactory;
private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
@ -43,17 +45,19 @@ namespace BTCPayServer.Data.Payouts.LightningLike
private readonly StoreRepository _storeRepository;
public UILightningLikePayoutController(ApplicationDbContextFactory applicationDbContextFactory,
LightningAutomatedPayoutSenderFactory lightningAutomatedPayoutSenderFactory,
UserManager<ApplicationUser> userManager,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository,
LightningClientFactoryService lightningClientFactoryService,
IOptions<LightningNetworkOptions> options,
IOptions<LightningNetworkOptions> options,
IAuthorizationService authorizationService,
EventAggregator eventAggregator)
{
_applicationDbContextFactory = applicationDbContextFactory;
_lightningAutomatedPayoutSenderFactory = lightningAutomatedPayoutSenderFactory;
_userManager = userManager;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_payoutHandlers = payoutHandlers;
@ -132,248 +136,66 @@ namespace BTCPayServer.Data.Payouts.LightningLike
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var payoutHandler = (LightningLikePayoutHandler)_payoutHandlers.TryGet(pmi);
await using var ctx = _applicationDbContextFactory.CreateContext();
var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.StoreDataId);
IEnumerable<IGrouping<string, PayoutData>> payouts;
using (var ctx = _applicationDbContextFactory.CreateContext())
{
payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.StoreDataId);
}
var results = new List<ResultVM>();
//we group per store and init the transfers by each
var authorizedForInternalNode = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded;
foreach (var payoutDatas in payouts)
{
var store = payoutDatas.First().StoreData;
var authorized = await _authorizationService.AuthorizeAsync(User, store, new PolicyRequirement(Policies.CanUseLightningNodeInStore));
if (!authorized.Succeeded)
{
results.AddRange(FailAll(payoutDatas, "You need the 'btcpay.store.canuselightningnode' permission for this action"));
continue;
}
var lightningSupportedPaymentMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(paymentMethodId, _handlers);
if (lightningSupportedPaymentMethod.IsInternalNode && !authorizedForInternalNode)
{
foreach (PayoutData payoutData in payoutDatas)
{
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
results.Add(new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message = "You are currently using the internal Lightning node for this payout's store but you are not a server admin."
});
}
results.AddRange(FailAll(payoutDatas, "You are currently using the internal Lightning node for this payout's store but you are not a server admin."));
continue;
}
var processor = _lightningAutomatedPayoutSenderFactory.ConstructProcessor(new PayoutProcessorData()
{
Store = store,
StoreId = store.Id,
PayoutMethodId = pmi.ToString(),
Processor = LightningAutomatedPayoutSenderFactory.ProcessorName,
Id = Guid.NewGuid().ToString()
});
var client =
lightningSupportedPaymentMethod.CreateLightningClient(payoutHandler.Network, _options.Value,
_lightningClientFactoryService);
foreach (var payoutData in payoutDatas)
foreach (var payout in payoutDatas)
{
ResultVM result;
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
var claim = await payoutHandler.ParseClaimDestination(blob.Destination, cancellationToken);
try
{
switch (claim.destination)
{
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
var lnurlResult = await GetInvoiceFromLNURL(payoutData, payoutHandler, blob,
lnurlPayClaimDestinaton, payoutHandler.Network.NBitcoinNetwork, cancellationToken);
if (lnurlResult.Item2 is not null)
{
result = lnurlResult.Item2;
}
else
{
result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, cancellationToken);
}
break;
case BoltInvoiceClaimDestination item1:
result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, cancellationToken);
break;
default:
result = new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message = claim.error
};
break;
}
}
catch (Exception exception)
{
result = new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message = exception.Message
};
}
results.Add(result);
}
}
await ctx.SaveChangesAsync();
foreach (var payoutG in payouts)
{
foreach (PayoutData payout in payoutG)
{
if (payout.State != PayoutState.AwaitingPayment)
{
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payout));
}
results.Add(await processor.HandlePayout(payout, client, cancellationToken));
}
}
return View("LightningPayoutResult", results);
}
public static async Task<(BOLT11PaymentRequest, ResultVM)> GetInvoiceFromLNURL(PayoutData payoutData,
LightningLikePayoutHandler handler, PayoutBlob blob, LNURLPayClaimDestinaton lnurlPayClaimDestinaton, Network network, CancellationToken cancellationToken)
private ResultVM[] FailAll(IEnumerable<PayoutData> payouts, string message)
{
var endpoint = lnurlPayClaimDestinaton.LNURL.IsValidEmail()
? LNURL.LNURL.ExtractUriFromInternetIdentifier(lnurlPayClaimDestinaton.LNURL)
: LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out _);
var httpClient = handler.CreateClient(endpoint);
var lnurlInfo =
(LNURLPayRequest)await LNURL.LNURL.FetchInformation(endpoint, "payRequest",
httpClient, cancellationToken);
var lm = new LightMoney(payoutData.Amount.Value, LightMoneyUnit.BTC);
if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable)
{
payoutData.State = PayoutState.Cancelled;
return (null, new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message =
$"The LNURL provided would not generate an invoice of {lm.ToDecimal(LightMoneyUnit.Satoshi)} sats"
});
}
try
{
var lnurlPayRequestCallbackResponse =
await lnurlInfo.SendRequest(lm, network, httpClient, cancellationToken: cancellationToken);
return (lnurlPayRequestCallbackResponse.GetPaymentRequest(network), null);
}
catch (LNUrlException e)
{
return (null,
new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message = e.Message
});
}
return payouts.Select(p => Fail(p, message)).ToArray();
}
public static async Task<ResultVM> TrypayBolt(
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, CancellationToken cancellationToken)
private ResultVM Fail(PayoutData payoutData, string message)
{
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
if (boltAmount > payoutData.Amount)
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
return new ResultVM
{
payoutData.State = PayoutState.Cancelled;
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Message = $"The BOLT11 invoice amount ({boltAmount} {payoutData.Currency}) did not match the payout's amount ({payoutData.Amount.GetValueOrDefault()} {payoutData.Currency})",
Destination = payoutBlob.Destination
};
}
if (bolt11PaymentRequest.ExpiryDate < DateTimeOffset.Now)
{
payoutData.State = PayoutState.Cancelled;
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Message = $"The BOLT11 invoice expiry date ({bolt11PaymentRequest.ExpiryDate}) has expired",
Destination = payoutBlob.Destination
};
}
var proofBlob = new PayoutLightningBlob { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
try
{
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
new PayInvoiceParams()
{
// CLN does not support explicit amount param if it is the same as the invoice amount
Amount = payoutData.Amount == bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)? null: new LightMoney((decimal)payoutData.Amount, LightMoneyUnit.BTC)
}, cancellationToken);
if (result == null) throw new NoPaymentResultException();
string message = null;
if (result.Result == PayResult.Ok)
{
payoutData.State = result.Details?.Status switch
{
LightningPaymentStatus.Pending => PayoutState.InProgress,
_ => PayoutState.Completed,
};
if (payoutData.State == PayoutState.Completed)
{
message = result.Details?.TotalAmount != null
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}"
: null;
try
{
var payment = await lightningClient.GetPayment(bolt11PaymentRequest.PaymentHash.ToString(),
cancellationToken);
proofBlob.Preimage = payment.Preimage;
}
catch (Exception)
{
// ignored
}
}
}
else if (result.Result == PayResult.Unknown)
{
payoutData.State = PayoutState.InProgress;
}
if (payoutData.State == PayoutState.InProgress)
{
message = "The payment has been initiated but is still in-flight.";
}
payoutData.SetProofBlob(proofBlob, null);
return new ResultVM
{
PayoutId = payoutData.Id,
Result = result.Result,
Destination = payoutBlob.Destination,
Message = message
};
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException or NoPaymentResultException)
{
// Timeout, potentially caused by hold invoices
// Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
payoutData.State = PayoutState.InProgress;
payoutData.SetProofBlob(proofBlob, null);
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Ok,
Destination = payoutBlob.Destination,
Message = "The payment timed out. We will verify if it completed later."
};
}
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message = message
};
}
private async Task SetStoreContext()
@ -405,8 +227,4 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public decimal Amount { get; set; }
}
}
public class NoPaymentResultException : Exception
{
}
}

View File

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -10,10 +12,29 @@ namespace BTCPayServer.Data
public int MinimumConfirmation { get; set; } = 1;
public string Destination { get; set; }
public int Revision { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string[] DisabledProcessors { get; set; }
[JsonExtensionData]
public Dictionary<string, JToken> AdditionalData { get; set; } = new();
public JObject Metadata { get; set; }
public void DisableProcessor(string processorName)
{
DisabledProcessors ??= Array.Empty<string>();
DisabledProcessors = DisabledProcessors.Concat(new[] { processorName }).ToArray();
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? ErrorCount { get; set; }
public int IncrementErrorCount()
{
if (ErrorCount is { } c)
ErrorCount = c + 1;
else
ErrorCount = 1;
return ErrorCount.Value;
}
}
}

View File

@ -29,6 +29,7 @@ using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.Wallets;
@ -291,7 +292,22 @@ namespace BTCPayServer
}
}
#nullable enable
public static IServiceCollection AddDefaultTranslations(this IServiceCollection services, params string[] keyValues)
{
return services.AddDefaultTranslations(keyValues.Select(k => KeyValuePair.Create<string, string?>(k, string.Empty)).ToArray());
}
public static IServiceCollection AddDefaultPrettyName(this IServiceCollection services, PaymentMethodId paymentMethodId, string defaultPrettyName)
{
services.AddSingleton<PrettyNameProvider.UntranslatedPrettyName>(new PrettyNameProvider.UntranslatedPrettyName(paymentMethodId, defaultPrettyName));
return services.AddDefaultTranslations(KeyValuePair.Create<string, string?>(PrettyNameProvider.GetTranslationKey(paymentMethodId), defaultPrettyName));
}
public static IServiceCollection AddDefaultTranslations(this IServiceCollection services, params KeyValuePair<string, string?>[] keyValues)
{
services.AddSingleton<IDefaultTranslationProvider>(new InMemoryDefaultTranslationProvider(keyValues));
return services;
}
#nullable restore
public static IServiceCollection AddUIExtension(this IServiceCollection services, string location, string partialViewName)
{
#pragma warning disable CS0618 // Type or member is obsolete

View File

@ -1,6 +1,8 @@
#nullable enable
using System;
using System.Globalization;
using System.Linq;
using BTCPayServer.Plugins.Altcoins;
using NBitcoin;
using NBXplorer;
@ -8,38 +10,33 @@ namespace BTCPayServer
{
public static class MoneyExtensions
{
public static decimal GetValue(this IMoney m, BTCPayNetwork network)
public static decimal GetValue(this IMoney value, BTCPayNetwork network) =>
(network, value) switch
{
switch (m)
{
case null:
return 0m;
case Money money:
return money.ToDecimal(MoneyUnit.BTC);
case MoneyBag mb:
return mb.Select(money => money.GetValue(network)).Sum();
case AssetMoney assetMoney:
if (network is BTCPayServer.Plugins.Altcoins.ElementsBTCPayNetwork elementsBTCPayNetwork)
{
return elementsBTCPayNetwork.AssetId == assetMoney.AssetId
? Convert(assetMoney.Quantity, elementsBTCPayNetwork.Divisibility)
: 0;
}
throw new NotSupportedException("IMoney type not supported");
default:
throw new NotSupportedException("IMoney type not supported");
}
}
(not ElementsBTCPayNetwork, Money m) => m.ToDecimal(MoneyUnit.BTC),
(_, null) => 0m,
(ElementsBTCPayNetwork e, Money m) when e.IsNativeAsset => m.ToDecimal(MoneyUnit.BTC),
(_, MoneyBag mb) => mb.Select(money => money.GetValue(network)).Sum(),
(ElementsBTCPayNetwork e, AssetMoney m) when m.AssetId == e.AssetId => m.ToDecimal(e.Divisibility),
(ElementsBTCPayNetwork e, AssetMoney m) when m.AssetId != e.AssetId => 0m,
_ => throw new InvalidOperationException($"Cannot get an amount from {value} with network {network}")
};
public static uint256? GetAssetId(this IMoney value, BTCPayNetwork network) =>
(network, value) switch
{
(ElementsBTCPayNetwork e, AssetMoney m) when m.AssetId == e.AssetId => m.AssetId,
(ElementsBTCPayNetwork e, Money) when e.IsNativeAsset => e.AssetId,
_ => null
};
public static bool IsCompatible(this IMoney value, BTCPayNetwork network) =>
(network, value) switch
{
(not ElementsBTCPayNetwork, Money) => true,
(ElementsBTCPayNetwork e, Money) when e.IsNativeAsset => true,
(ElementsBTCPayNetwork e, AssetMoney m) when m.AssetId == e.AssetId => true,
_ => false
};
public static decimal Convert(long sats, int divisibility = 8)
{
var negative = sats < 0;
var amt = sats.ToString(CultureInfo.InvariantCulture)
.Replace("-", "", StringComparison.InvariantCulture)
.PadLeft(divisibility, '0');
amt = amt.Length == divisibility ? $"0.{amt}" : amt.Insert(amt.Length - divisibility, ".");
return decimal.Parse($"{(negative ? "-" : string.Empty)}{amt}", CultureInfo.InvariantCulture);
}
public static string ShowMoney(this IMoney money, BTCPayNetwork network)
{
return money.GetValue(network).ShowMoney(network.Divisibility);

View File

@ -10,6 +10,7 @@ using Fido2NetLib;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Fido2
@ -20,17 +21,22 @@ namespace BTCPayServer.Fido2
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly Fido2Service _fido2Service;
private IStringLocalizer StringLocalizer { get; }
public UIFido2Controller(UserManager<ApplicationUser> userManager, Fido2Service fido2Service)
public UIFido2Controller(
UserManager<ApplicationUser> userManager,
Fido2Service fido2Service,
IStringLocalizer stringLocalizer)
{
_userManager = userManager;
_fido2Service = fido2Service;
StringLocalizer = stringLocalizer;
}
[HttpGet("{id}/delete")]
public IActionResult Remove(string id)
{
return View("Confirm", new ConfirmModel("Remove security device", "Your account will no longer have this security device as an option for two-factor authentication.", "Remove"));
return View("Confirm", new ConfirmModel(StringLocalizer["Remove security device"], StringLocalizer["Your account will no longer have this security device as an option for two-factor authentication."], StringLocalizer["Remove"]));
}
[HttpPost("{id}/delete")]
@ -41,7 +47,7 @@ namespace BTCPayServer.Fido2
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = "The security device was removed successfully."
Html = StringLocalizer["The security device was removed successfully."].Value
});
return RedirectToList();
@ -56,7 +62,7 @@ namespace BTCPayServer.Fido2
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "The security device could not be registered."
Html = StringLocalizer["The security device could not be registered."].Value
});
return RedirectToList();
@ -75,7 +81,7 @@ namespace BTCPayServer.Fido2
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = "The security device was registered successfully."
Html = StringLocalizer["The security device was registered successfully."].Value
});
}
else
@ -83,7 +89,7 @@ namespace BTCPayServer.Fido2
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "The security device could not be registered."
Html = StringLocalizer["The security device could not be registered."].Value
});
}

View File

@ -46,6 +46,8 @@ namespace BTCPayServer.Filters
var uri = new UriBuilder(req.Scheme, redirectDomain);
if (req.Host.Port.HasValue)
uri.Port = req.Host.Port.Value;
if (req.QueryString.HasValue)
uri.Query = req.QueryString.Value!;
context.RouteContext.HttpContext.Response.Redirect(uri.ToString());
}
return true;

View File

@ -4,6 +4,7 @@ namespace BTCPayServer.Forms;
public class ModifyForm
{
[DisplayName("Name")]
public string Name { get; set; }
[DisplayName("Form configuration (JSON)")]

View File

@ -19,6 +19,7 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Forms;
@ -31,9 +32,11 @@ public class UIFormsController : Controller
private readonly IAuthorizationService _authorizationService;
private readonly StoreRepository _storeRepository;
private FormComponentProviders FormProviders { get; }
private IStringLocalizer StringLocalizer { get; }
public UIFormsController(FormComponentProviders formProviders, FormDataService formDataService,
UriResolver uriResolver,
IStringLocalizer stringLocalizer,
StoreRepository storeRepository, IAuthorizationService authorizationService)
{
FormProviders = formProviders;
@ -41,6 +44,7 @@ public class UIFormsController : Controller
_uriResolver = uriResolver;
_authorizationService = authorizationService;
_storeRepository = storeRepository;
StringLocalizer = stringLocalizer;
}
[HttpGet("~/stores/{storeId}/forms")]
@ -85,8 +89,7 @@ public class UIFormsController : Controller
if (!_formDataService.IsFormSchemaValid(modifyForm.FormConfig, out var form, out var error))
{
ModelState.AddModelError(nameof(modifyForm.FormConfig),
$"Form config was invalid: {error})");
ModelState.AddModelError(nameof(modifyForm.FormConfig), StringLocalizer["Form config was invalid: {0}", error!]);
}
else
{
@ -113,7 +116,9 @@ public class UIFormsController : Controller
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = $"Form {(isNew ? "created" : "updated")} successfully."
Message = isNew
? StringLocalizer["Form created successfully."].Value
: StringLocalizer["Form updated successfully."].Value
});
if (isNew)
{
@ -122,7 +127,7 @@ public class UIFormsController : Controller
}
catch (Exception e)
{
ModelState.AddModelError("", $"An error occurred while saving: {e.Message}");
ModelState.AddModelError("", StringLocalizer["An error occurred while saving: {0}", e.Message]);
}
return View(modifyForm);
@ -136,7 +141,7 @@ public class UIFormsController : Controller
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Form removed"
Message = StringLocalizer["Form removed"].Value
});
return RedirectToAction("FormsList", new { storeId });
}
@ -212,21 +217,20 @@ public class UIFormsController : Controller
try
{
var request = _formDataService.GenerateInvoiceParametersFromForm(form);
var inv = await invoiceController.CreateInvoiceCoreRaw(request, store, Request.GetAbsoluteRoot());
if (inv.Price == 0 && inv.Type == InvoiceType.Standard && inv.ReceiptOptions?.Enabled is not false)
{
return RedirectToAction("InvoiceReceipt", "UIInvoice", new { invoiceId = inv.Id });
}
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
var request = _formDataService.GenerateInvoiceParametersFromForm(form);
var inv = await invoiceController.CreateInvoiceCoreRaw(request, store, Request.GetAbsoluteRoot());
if (inv.Price == 0 && inv.Type == InvoiceType.Standard && inv.ReceiptOptions?.Enabled is not false)
{
return RedirectToAction("InvoiceReceipt", "UIInvoice", new { invoiceId = inv.Id });
}
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
}
catch (Exception e)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Could not generate invoice: "+ e.Message
Message = StringLocalizer["Could not generate invoice: {0}", e.Message].Value
});
return await GetFormView(formData, form);
}

View File

@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
@ -17,8 +18,8 @@ namespace BTCPayServer.HostedServices
public EventAggregator EventAggregator => _EventAggregator;
private List<IEventAggregatorSubscription> _Subscriptions;
private CancellationTokenSource _Cts;
private List<IEventAggregatorSubscription> _Subscriptions = new List<IEventAggregatorSubscription>();
private CancellationTokenSource _Cts = new CancellationTokenSource();
public CancellationToken CancellationToken => _Cts.Token;
public EventHostedServiceBase(EventAggregator eventAggregator, Logs logs)
{
@ -68,7 +69,7 @@ namespace BTCPayServer.HostedServices
protected void Subscribe<T>()
{
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e!)));
}
protected void PushEvent(object obj)
@ -78,9 +79,7 @@ namespace BTCPayServer.HostedServices
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_Subscriptions = new List<IEventAggregatorSubscription>();
SubscribeToEvents();
_Cts = new CancellationTokenSource();
_ProcessingEvents = ProcessEvents(_Cts.Token);
return Task.CompletedTask;
}
@ -88,8 +87,8 @@ namespace BTCPayServer.HostedServices
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
_Subscriptions?.ForEach(subscription => subscription.Dispose());
_Cts?.Cancel();
_Subscriptions.ForEach(subscription => subscription.Dispose());
_Cts.Cancel();
try
{
await _ProcessingEvents;

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using AngleSharp.Dom;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Data;
using BTCPayServer.Migrations;
using BTCPayServer.Services.Invoices;
using Dapper;
using Google.Apis.Logging;
@ -74,7 +75,7 @@ public class InvoiceBlobMigratorHostedService : BlobMigratorHostedService<Invoic
if (pay.PaymentMethodId != pay.MigratedPaymentMethodId)
{
ctx.Entry(pay).State = EntityState.Added;
ctx.Add(pay);
ctx.Payments.Remove(new PaymentData() { Id = pay.Id, PaymentMethodId = pay.MigratedPaymentMethodId });
}
}

View File

@ -13,6 +13,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
@ -22,15 +23,17 @@ namespace BTCPayServer.HostedServices
{
private const string TYPE = "pluginupdate";
internal class Handler(LinkGenerator linkGenerator, BTCPayServerOptions options) : NotificationHandler<PluginUpdateNotification>
internal class Handler(LinkGenerator linkGenerator, BTCPayServerOptions options, IStringLocalizer stringLocalizer) : NotificationHandler<PluginUpdateNotification>
{
private IStringLocalizer StringLocalizer { get; } = stringLocalizer;
public override string NotificationType => TYPE;
public override (string identifier, string name)[] Meta
{
get
{
return new (string identifier, string name)[] {(TYPE, "Plugin update")};
return new (string identifier, string name)[] {(TYPE, StringLocalizer["Plugin update"])};
}
}
@ -38,7 +41,7 @@ namespace BTCPayServer.HostedServices
{
vm.Identifier = notification.Identifier;
vm.Type = notification.NotificationType;
vm.Body = $"New {notification.Name} plugin version {notification.Version} released!";
vm.Body = StringLocalizer["New {0} plugin version {1} released!", notification.Name, notification.Version];
vm.ActionLink = linkGenerator.GetPathByAction(nameof(UIServerController.ListPlugins),
"UIServer",
new {plugin = notification.PluginIdentifier}, options.RootPath);
@ -82,7 +85,7 @@ namespace BTCPayServer.HostedServices
var installedPlugins =
pluginService.LoadedPlugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
var remotePlugins = await pluginService.GetRemotePlugins();
var remotePlugins = await pluginService.GetRemotePlugins(null);
//take the latest version of each plugin
var remotePluginsList = remotePlugins
.GroupBy(plugin => plugin.Identifier)

View File

@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
@ -18,11 +19,13 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Rates;
using Dapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PayoutData = BTCPayServer.Data.PayoutData;
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
@ -170,6 +173,12 @@ namespace BTCPayServer.HostedServices
public bool IncludePullPaymentData { get; set; }
public DateTimeOffset? From { get; set; }
public DateTimeOffset? To { get; set; }
/// <summary>
/// All payouts are elligible for every processors with matching payout method.
/// However, some processor may be disabled for some payouts.
/// Setting this field will filter out payouts that have the processor disabled.
/// </summary>
public string Processor { get; set; }
}
public async Task<List<PayoutData>> GetPayouts(PayoutQuery payoutQuery)
@ -262,6 +271,14 @@ namespace BTCPayServer.HostedServices
{
query = query.Where(data => data.Date <= payoutQuery.To);
}
if (payoutQuery.Processor is not null)
{
var q = new JObject()
{
["DisabledProcessors"] = new JArray(payoutQuery.Processor)
}.ToString();
query = query.Where(data => !EF.Functions.JsonContains(data.Blob, q));
}
return await query.ToListAsync(cancellationToken);
}
@ -534,6 +551,8 @@ namespace BTCPayServer.HostedServices
if (cryptoAmount < minimumCryptoAmount)
{
req.Completion.TrySetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.TooLowAmount, null));
payout.State = PayoutState.Cancelled;
await ctx.SaveChangesAsync();
return;
}
@ -583,6 +602,8 @@ namespace BTCPayServer.HostedServices
break;
}
payout.State = req.Request.State;
if (req.Request.UpdateBlob is { } b)
payout.SetBlob(b, _jsonSerializerSettings);
await ctx.SaveChangesAsync();
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payout));
req.Completion.SetResult(MarkPayoutRequest.PayoutPaidResult.Ok);
@ -657,13 +678,6 @@ namespace BTCPayServer.HostedServices
}
}
if (req.ClaimRequest.Value <
await payoutHandler.GetMinimumPayoutAmount(req.ClaimRequest.Destination))
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.AmountTooLow));
return;
}
var payoutsRaw = withoutPullPayment
? null
: await ctx.Payouts.Where(p => p.PullPaymentDataId == pp.Id)
@ -672,7 +686,7 @@ namespace BTCPayServer.HostedServices
var payouts = payoutsRaw?.Select(o => new { Entity = o, Blob = o.GetBlob(_jsonSerializerSettings) });
var limit = pp?.Limit ?? 0;
var totalPayout = payouts?.Select(p => p.Entity.OriginalAmount)?.Sum();
var claimed = req.ClaimRequest.Value is decimal v ? v : limit - (totalPayout ?? 0);
var claimed = req.ClaimRequest.ClaimedAmount is decimal v ? v : limit - (totalPayout ?? 0);
if (totalPayout is not null && totalPayout + claimed > limit)
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Overdraft));
@ -731,6 +745,22 @@ namespace BTCPayServer.HostedServices
payout.State = PayoutState.AwaitingPayment;
payout.Amount = approveResult.CryptoAmount;
}
else if (approveResult.Result == PayoutApproval.Result.TooLowAmount)
{
payout.State = PayoutState.Cancelled;
await ctx.SaveChangesAsync();
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.AmountTooLow));
return;
}
else
{
payout.State = PayoutState.Cancelled;
await ctx.SaveChangesAsync();
// We returns Ok even if the approval failed. This is expected.
// Because the claim worked, what didn't is the approval
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok));
return;
}
}
}
@ -923,6 +953,7 @@ namespace BTCPayServer.HostedServices
public string PayoutId { get; set; }
public JObject Proof { get; set; }
public PayoutState State { get; set; } = PayoutState.Completed;
public PayoutBlob UpdateBlob { get; internal set; }
public static string GetErrorMessage(PayoutPaidResult result)
{
@ -942,28 +973,40 @@ namespace BTCPayServer.HostedServices
public class ClaimRequest
{
public static (string error, decimal? amount) IsPayoutAmountOk(IClaimDestination destination, decimal? amount, string payoutCurrency = null, string ppCurrency = null)
public record ClaimedAmountResult
{
return amount switch
public record Error(string Message) : ClaimedAmountResult;
public record Success(decimal? Amount) : ClaimedAmountResult;
}
public static ClaimedAmountResult GetClaimedAmount(IClaimDestination destination, decimal? amount, string payoutCurrency, string ppCurrency)
{
var amountsComparable = false;
var destinationAmount = destination.Amount;
if (destinationAmount is not null &&
payoutCurrency == "BTC" &&
ppCurrency == "SATS")
{
null when destination.Amount is null && ppCurrency is null => ("Amount is not specified in destination or payout request", null),
null when destination.Amount is null => (null, null),
null when destination.Amount != null => (null, destination.Amount),
not null when destination.Amount is null => (null, amount),
not null when destination.Amount != null && amount != destination.Amount &&
destination.IsExplicitAmountMinimum &&
payoutCurrency == "BTC" && ppCurrency == "SATS" &&
new Money(amount.Value, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) < destination.Amount =>
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
not null when destination.Amount != null && amount != destination.Amount &&
destination.IsExplicitAmountMinimum &&
!(payoutCurrency == "BTC" && ppCurrency == "SATS") &&
amount < destination.Amount =>
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
not null when destination.Amount != null && amount != destination.Amount &&
!destination.IsExplicitAmountMinimum =>
($"Amount is implied in destination ({destination.Amount}) that does not match the payout amount provided {amount})", null),
_ => (null, amount)
destinationAmount = new LightMoney(destinationAmount.Value, LightMoneyUnit.BTC).ToUnit(LightMoneyUnit.Satoshi);
amountsComparable = true;
}
if (destinationAmount is not null && payoutCurrency == ppCurrency)
{
amountsComparable = true;
}
return (destinationAmount, amount) switch
{
(null, null) when ppCurrency is null => new ClaimedAmountResult.Error("Amount is not specified in destination or payout request"),
({ } a, null) when ppCurrency is null => new ClaimedAmountResult.Success(a),
(null, null) => new ClaimedAmountResult.Success(null),
({ } a, null) when amountsComparable => new ClaimedAmountResult.Success(a),
(null, { } b) => new ClaimedAmountResult.Success(b),
({ } a, { } b) when amountsComparable && a == b => new ClaimedAmountResult.Success(a),
({ } a, { } b) when amountsComparable && a > b => new ClaimedAmountResult.Error($"The destination's amount ({a} {ppCurrency}) is more than the claimed amount ({b} {ppCurrency})."),
({ } a, { } b) when amountsComparable && a < b => new ClaimedAmountResult.Success(a),
({ } a, { } b) when !amountsComparable => new ClaimedAmountResult.Success(b),
_ => new ClaimedAmountResult.Success(amount)
};
}
@ -1020,7 +1063,7 @@ namespace BTCPayServer.HostedServices
public PayoutMethodId PayoutMethodId { get; set; }
public string PullPaymentId { get; set; }
public decimal? Value { get; set; }
public decimal? ClaimedAmount { get; set; }
public IClaimDestination Destination { get; set; }
public string StoreId { get; set; }
public bool? PreApprove { get; set; }

View File

@ -624,6 +624,7 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
// BTC
{
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
services.AddDefaultPrettyName(pmi, network.DisplayName);
services.AddSingleton<BTCPayNetworkBase>(network);
services.AddSingleton<IPaymentMethodHandler>(provider =>
(BitcoinLikePaymentHandler)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinLikePaymentHandler), new object[] { network, pmi }));
@ -646,6 +647,10 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
// LN
{
var pmi = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
if (network.IsBTC)
services.AddDefaultPrettyName(pmi, "Lightning");
else
services.AddDefaultPrettyName(pmi, $"Lightning ({network.DisplayName})");
services.AddSingleton<IPaymentMethodHandler>(provider =>
(LightningLikePaymentHandler)ActivatorUtilities.CreateInstance(provider, typeof(LightningLikePaymentHandler), new object[] { network, pmi }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
@ -661,6 +666,10 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
// LNURL
{
var pmi = PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode);
if (network.IsBTC)
services.AddDefaultPrettyName(pmi, "Lightning (via LNURL)");
else
services.AddDefaultPrettyName(pmi, $"Lightning ({network.DisplayName} via LNURL)");
services.AddSingleton<IPaymentMethodHandler>(provider =>
(LNURLPayPaymentHandler)ActivatorUtilities.CreateInstance(provider, typeof(LNURLPayPaymentHandler), new object[] { network, pmi }));
services.AddSingleton<IPaymentLinkExtension>(provider =>

View File

@ -233,12 +233,6 @@ namespace BTCPayServer.Hosting
private async Task MigrateStoreExcludedPaymentMethods()
{
HashSet<string> oldPaymentIds = new()
{
"LightningLike",
"BTCLike",
"LNURLPAY"
};
await using var ctx = _DBContextFactory.CreateContext();
var stores = await ctx.Stores.ToArrayAsync();
foreach (var store in stores)

View File

@ -8,9 +8,11 @@ namespace BTCPayServer.Models.ManageViewModels
[Required]
[EmailAddress]
[MaxLength(50)]
[Display(Name = "Email")]
public string Email { get; set; }
public bool EmailConfirmed { get; set; }
public bool RequiresEmailConfirmation { get; set; }
[Display(Name = "Name")]
public string Name { get; set; }
[Display(Name = "Profile Picture")]

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