Compare commits
79 Commits
v2.0.0-bet
...
v2.0.1
Author | SHA1 | Date | |
---|---|---|---|
8a5a160645 | |||
5cbadc09f9 | |||
7aa87d397e | |||
693eceb80f | |||
7d8fc14159 | |||
4687bb95cb | |||
e3ec07da76 | |||
910801d305 | |||
5ad0b128aa | |||
5cbeea4fb3 | |||
a6e18736d6 | |||
373b90e3b5 | |||
92f9b226fe | |||
0ac6553840 | |||
41a2241ae1 | |||
9bb1a5b80a | |||
0e59107eee | |||
c9fe68b812 | |||
e7b9688602 | |||
a962e60de9 | |||
e5611f9165 | |||
540ad13265 | |||
2849426092 | |||
c4a2b4e975 | |||
d508f5dc09 | |||
81ce8b0469 | |||
5a3a661e91 | |||
bb5c6bd68d | |||
9dfabeab52 | |||
ad07330bf1 | |||
74011e50e3 | |||
3dfdbf544a | |||
4bf0b79c2a | |||
cc0ea0b3f8 | |||
62d765125d | |||
b5b45d9a27 | |||
8e098710c1 | |||
6dfb369b55 | |||
817522ff97 | |||
8b5b90d247 | |||
b670097592 | |||
7b6a115adc | |||
77fba4aee3 | |||
7e1712c8cd | |||
b7affb1d34 | |||
d7fd90c4c3 | |||
1d94782463 | |||
c7a05c3f09 | |||
2dc58a82b7 | |||
755dbbab00 | |||
b470fe22f1 | |||
5b2560ddf7 | |||
be429c527c | |||
65fd537200 | |||
6e43c7f06f | |||
5867b5c000 | |||
05887cf8b0 | |||
c43721d489 | |||
0bf75d52d7 | |||
c35af2dc69 | |||
73a9835a27 | |||
87ab15f754 | |||
6bc608c081 | |||
cbea1d8691 | |||
58f21a69aa | |||
426c5b9a24 | |||
511e90efd1 | |||
bc7b856654 | |||
d50d2f9ca0 | |||
1b53defab3 | |||
3e612921f3 | |||
ec51d43490 | |||
80dc5028f7 | |||
2329c4a75f | |||
ae76cc1ca2 | |||
e4f79f046a | |||
622d837ea1 | |||
c0aa9a8bd4 | |||
9b1052f023 |
@ -8,6 +8,14 @@ namespace BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
public static class SetStatusMessageModelExtensions
|
||||
{
|
||||
public static void SetStatusSuccess(this ITempDataDictionary tempData, string statusMessage)
|
||||
{
|
||||
tempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = statusMessage
|
||||
});
|
||||
}
|
||||
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
|
||||
{
|
||||
if (statusMessage == null)
|
||||
@ -26,19 +34,14 @@ public static class SetStatusMessageModelExtensions
|
||||
tempData.TryGetValue("StatusMessageModel", out var model);
|
||||
if (successMessage != null || errorMessage != null)
|
||||
{
|
||||
var parsedModel = new StatusMessageModel();
|
||||
parsedModel.Message = (string)successMessage ?? (string)errorMessage;
|
||||
if (successMessage != null)
|
||||
var parsedModel = new StatusMessageModel
|
||||
{
|
||||
parsedModel.Severity = StatusMessageModel.StatusSeverity.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedModel.Severity = StatusMessageModel.StatusSeverity.Error;
|
||||
}
|
||||
Message = (string)successMessage ?? (string)errorMessage,
|
||||
Severity = successMessage != null ? StatusMessageModel.StatusSeverity.Success : StatusMessageModel.StatusSeverity.Error
|
||||
};
|
||||
return parsedModel;
|
||||
}
|
||||
else if (model != null && model is string str)
|
||||
if (model is string str)
|
||||
{
|
||||
return JObject.Parse(str).ToObject<StatusMessageModel>();
|
||||
}
|
||||
|
@ -14,14 +14,6 @@ namespace BTCPayServer.Abstractions.Models
|
||||
|
||||
public string SeverityCSS => ToString(Severity);
|
||||
|
||||
private void ParseNonJsonStatus(string s)
|
||||
{
|
||||
Message = s;
|
||||
Severity = s.StartsWith("Error", StringComparison.InvariantCultureIgnoreCase)
|
||||
? StatusSeverity.Error
|
||||
: StatusSeverity.Success;
|
||||
}
|
||||
|
||||
public static string ToString(StatusSeverity severity)
|
||||
{
|
||||
switch (severity)
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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
|
||||
|
@ -92,7 +92,7 @@ namespace BTCPayServer.Data
|
||||
blob.Remove("output");
|
||||
blob.Remove("outpoint");
|
||||
// Convert from sats to btc
|
||||
if (cryptoData["value"] is not (null or { Type: JTokenType.Null }))
|
||||
if (cryptoData["value"] is not (null or { Type: JTokenType.Null } or { Type: JTokenType.Object }))
|
||||
{
|
||||
var v = cryptoData["value"].Value<long>();
|
||||
Amount = (decimal)v / (decimal)Money.COIN;
|
||||
@ -103,7 +103,22 @@ namespace BTCPayServer.Data
|
||||
blob.ConvertNumberToString("paymentMethodFee");
|
||||
blob.Remove("networkFee");
|
||||
blob.RemoveIfNull("paymentMethodFee");
|
||||
}
|
||||
}
|
||||
// Liquid
|
||||
else if (cryptoData["value"] is { Type: JTokenType.Object })
|
||||
{
|
||||
var v = cryptoData["value"]["value"].Value<long>();
|
||||
var assetId = cryptoData["value"]["assetId"].Value<string>();
|
||||
divisibility = GetDivisibility(assetId) ?? 8;
|
||||
Amount = (decimal)v / (decimal)Math.Pow(10.0, divisibility);
|
||||
cryptoData.Remove("value");
|
||||
cryptoData["assetId"] = assetId;
|
||||
blob["paymentMethodFee"] = blob["networkFee"];
|
||||
blob.RemoveIfValue<decimal>("paymentMethodFee", 0.0m);
|
||||
blob.ConvertNumberToString("paymentMethodFee");
|
||||
blob.Remove("networkFee");
|
||||
blob.RemoveIfNull("paymentMethodFee");
|
||||
}
|
||||
// Convert from millisats to btc
|
||||
else if (cryptoData["amount"] is not (null or { Type: JTokenType.Null }))
|
||||
{
|
||||
@ -164,6 +179,17 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return true;
|
||||
}
|
||||
|
||||
private int? GetDivisibility(string assetId) =>
|
||||
assetId switch
|
||||
{
|
||||
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2" => 8,
|
||||
"aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf" => 2,
|
||||
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a" => 8,
|
||||
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d" => 8,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
[NotMapped]
|
||||
|
@ -1,11 +1,9 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -73,7 +71,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasColumnType("JSONB");
|
||||
.HasColumnType("jsonb");
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Proof)
|
||||
.HasColumnType("JSONB");
|
||||
|
@ -560,7 +560,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Blob")
|
||||
.HasColumnType("JSONB");
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.HasColumnType("text");
|
||||
|
@ -63,7 +63,6 @@ namespace BTCPayServer.Tests
|
||||
//no tether on our regtest, lets create it and set it
|
||||
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
|
||||
var lbtc = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("LBTC");
|
||||
var etb = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("ETB");
|
||||
var issueAssetResult = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
|
||||
tether.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString());
|
||||
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network)
|
||||
@ -71,15 +70,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(tether.AssetId, tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT").AssetId);
|
||||
Assert.Equal(tether.AssetId, ((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network).AssetId);
|
||||
|
||||
var issueAssetResult2 = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
|
||||
etb.AssetId = uint256.Parse(issueAssetResult2.Result["asset"].ToString());
|
||||
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("ETB").Network)
|
||||
.AssetId = etb.AssetId;
|
||||
|
||||
|
||||
user.RegisterDerivationScheme("LBTC");
|
||||
user.RegisterDerivationScheme("USDT");
|
||||
user.RegisterDerivationScheme("ETB");
|
||||
|
||||
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
|
||||
@ -109,11 +103,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT", StringComparison.InvariantCultureIgnoreCase)).Payments);
|
||||
});
|
||||
|
||||
//test precision based on https://github.com/ElementsProject/elements/issues/805#issuecomment-601277606
|
||||
var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21, etb.NBitcoinNetwork);
|
||||
//precision = 2, 1ETB = 0.00000100
|
||||
Assert.Equal(100, etbBip21.Amount.Satoshi);
|
||||
|
||||
|
||||
var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21, lbtc.NBitcoinNetwork);
|
||||
//precision = 8, 0.1 = 0.1
|
||||
|
@ -679,6 +679,26 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(utxo54, utxos[53]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResourceTrackerTest()
|
||||
{
|
||||
var tracker = new ResourceTracker<string>();
|
||||
var t1 = tracker.StartTracking();
|
||||
Assert.True(t1.TryTrack("1"));
|
||||
Assert.False(t1.TryTrack("1"));
|
||||
var t2 = tracker.StartTracking();
|
||||
Assert.True(t2.TryTrack("2"));
|
||||
Assert.False(t2.TryTrack("1"));
|
||||
Assert.True(t1.Contains("1"));
|
||||
Assert.True(t2.Contains("2"));
|
||||
Assert.True(tracker.Contains("1"));
|
||||
Assert.True(tracker.Contains("2"));
|
||||
t1.Dispose();
|
||||
Assert.False(tracker.Contains("1"));
|
||||
Assert.True(tracker.Contains("2"));
|
||||
Assert.True(t2.TryTrack("1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanAcceptInvoiceWithTolerance()
|
||||
{
|
||||
|
@ -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)]
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -727,5 +727,39 @@
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "payment",
|
||||
"input": {
|
||||
"output": null,
|
||||
"version": 1,
|
||||
"outpoint": null,
|
||||
"accounted": true,
|
||||
"cryptoCode": "XMR",
|
||||
"networkFee": 0.0000000019,
|
||||
"receivedTimeMs": 1705500405468,
|
||||
"cryptoPaymentData": "{\"Amount\":62700000000,\"Address\":\"85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t\",\"SubaddressIndex\":23,\"SubaccountIndex\":0,\"BlockHeight\":3063946,\"ConfirmationCount\":10,\"TransactionId\":\"cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06\",\"LockTime\":0}",
|
||||
"cryptoPaymentDataType": "MoneroLike"
|
||||
},
|
||||
"expected": {
|
||||
"divisibility": 12,
|
||||
"destination": "85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t",
|
||||
"details": {
|
||||
"blockHeight": 3063946,
|
||||
"confirmationCount": 10,
|
||||
"lockTime": 0,
|
||||
"subaccountIndex": 0,
|
||||
"subaddressIndex": 23,
|
||||
"transactionId": "cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06"
|
||||
},
|
||||
"version": 2
|
||||
},
|
||||
"expectedProperties": {
|
||||
"Amount": "0.0627",
|
||||
"PaymentMethodId": "XMR-CHAIN",
|
||||
"Currency": "XMR",
|
||||
"Status": "Settled",
|
||||
"Accounted": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -1,5 +1,5 @@
|
||||
Id,Blob,Created,ExceptionStatus,Status,StoreDataId,Archived,Blob2
|
||||
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Q7RqoHLngK9svM4MgRyi9y,\x1f8b080000000000000ac454cb76da400cfd97595302b1c1981d6012124c0e8f104e12b2f043b627d833663c767073f2655df493fa0bd50049714fbae9262b1f4ba3ab2be94abf7efc7c25d4275d3233e65b3eb2593836b362a24fc27949cd92d44826b9802bf544cbc2e5602097fdb0bde8d8d71be1fa416fe566d7936dbfbcb08362b17dbe6cefc6fdd5d0c7402e7c102a90e5718c3829803fe531f54ad26dd4887024906ebb65b6eb9a6e187adbd0b4965e2394159c7a704b1374375b5a4733b596765e23b04b290651ce4e7dbaae7c3ea43ca3b2e7fb02b24c71ed9b634b338ca56d19c3fb6075afcd4661b0ea5ce8fd17695ba14d91612ab89f7bf28a055c247b64d2c57648482cc83c44710104be539601f7e1bd94342a33ea3931e9064e9c810242c6a46bd4b12e2f1702982a922c171679ab11372fb111d5247bdb8d937c60ee0dc3c4a171c532e03993a2acd81e685af95fc87d2b4fa3a8ac861c1b73fe99b159314e23ce8e60481dbb6a39d2797f8103a5c5be868554f30b5595bb3477db56cf0da3209f0f56edd95668e360613dd38dbe192e69d1db049177399a6cee86a0bb0fdb967637fb6e981b5d1ff2596b62a5f6f578dc9b8e5bc5dd5d7bba3396cdb966c28cbacc8dac6c65e6c35973ec47e6c0729370feed313dcfa227f2091b0a6af4af6bd2bf1dac4977fd45d4d6e46dbf378ecc151f062ff80b3b0fd203d783fd2825a74c8049fc7f7cc29d802067bee3c6f021ad836972a20b013e15e0c9e5dc46f448ca34eb9e9d254e264130542926de5016d63d9e605eb9bb00b52c0d946680f1375cd200c5aba8605629724cc24e8c7bd8e3ca8228402c455cc943190359f721e175577a58c0e1599d8b10f379a24c253f88e6550d427dfeb5ebc7ea2720238e87e215151700ccf7af9b55eeffb3e108677f3103c44b38a378437124c38f0bf67ebd3a0d75bd22aa8eacba284770a5e3c3089c0227af0471f48c9c2cfae3859d04e683ffd7508fd281e2a0ac8ad26e790cc261ea5c35eb8db7df000000ffff0300d1083b8e01060000,2018-10-01 11:32:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
|
|
@ -9,3 +9,4 @@ dc130d025a4bd2e7ee83b7707d850e7c1a52872c222a5bc2e05d3357e79aa762-1,\x1f8b0800000
|
||||
afc39884e024cbb3a48ca997b7e878b199d85fe97f90f94471364cc866ba83ad-1,\x1f8b080000000000000374904d6ac3301085f73945d03a14c992f593655cb20aa5942cbd1949a362d258c191534cc8c9bae8917a854a2e0ddd7476ef3de6d3e87d7d7c5e17cb25b9e070ee624fd64bb62a7a4087dd05fdbe3b6231eb9ad5a21246ce698fe93d0e872d968c3ed032d59cc4319d62d7a7ec13f09a5b90d269e1b8544c88600c55014d1dbc36c632a5515b651418ed4058ee6c25906a91258740d90ca6e44e1e672eb5402b47efc3f23e13351a270cc7bc2aa407164ce0ca04e1a9ac38b34885e65c4aabd50f0f9c8b639fd067641a469c4d374ca7149be8cbc7c866df903ff6334c47ecd3232428e9b5254dec43371c21e5ea9a426bc95ae59e562d79d96cb398c12d79baf795bddc57b676f80a6eca32c0db196fffbcb39f4ebf97ecba0392c5ed1b0000ffff030018cc54a1b1010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
6f2b8513ebfdb14b41d4de235f050cd028400cb01fde700d72c6bedc2ad8af1e-5,\x1f8b08000000000000037490bf6ac3301087f73c45d01cc249966425635c3285528a472ffa732a26b5151c39c5843c59873e525fa192434397de22ddef83efa4fbfefcba2e964b72c1e1dc869e6c977495fb012db6177475db610e85908a6d24c04c7b8c1f6138ee313358432e369330c65368fb987242517ba79876d6a0952503578243ea0d80e5c0944b2708e159e1d0714eb931d43b34b4508219e941cc62200ff378f752c719c2a368a937942b445158855e6967b8b05c0a2e9934c0b59585960c0b55d272a3cabb4f5b1bc63ea24b4aafdfcf38a776984e3154c1e59f915d5d913ff18b9e3aece3938e3ad36b43aad0fb76e8744cbbabb2ae215b5835e475b74fb7388c989ae7c7b6325d677ec0376da7d4ce936fff0ca9a7d3ef330eed11c9e2f6030000ffff0300b09cf40daf010000,Q7RqoHLngK9svM4MgRyi9y,f,,
|
||||
3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61,\x1f8b08000000000000037490bd6ec3201485f73c45c41c5580f973c6b8ca14555595d10be65e222b8d891c9cca8af2641dfa487d8582ab585d7a27ce39f001e7fbf3ebb6582ec915fb4b1b3ab25eb255d63d3a6caf08fbf684d994525121a85653da61fc08fd718b39a34f340f9f9230c47368bb987c22256fb4f09e7bc79d914a000aea8c6bd083c1461bcfd2524241593a5f3a250aef8d2991c952a9c21630812999c9c3c4d55615cad37998b625131eb8e5c080496c941525958c97b2708c838286022ac5b56060f42fcf3a17862e222464ec079c4cd78fe718aa00f96364b3afc81ffbd58e27ece2b38d36a7b79a54a1f36d7fb2315557655a4dd64c30b1aac9db669b84b7ef174cea652e2c99a9b064edf060ddf8d873ffe7a2fd787e3c65d71e912cee3f000000ffff0300a991d8c4b2010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
26c879f3d27a894a62f8730c84205ac9dec38b7bbc0a11ccc0c196d1259b25aa-1,\x1f8b08000000000000036c90cf6ed4301087dfc5e7086c27f19fdccaa2b0b4dbaa452b964ab94cec314d378943e204d26a9f8c038fc42be02eea01099f3c9faddfcc7cbf7ffe7a260b8e53e37b52b0848c68b059d0ee9b0eafa78804cfa9d69aa954c884f418befbf158229282bea109f173187cd3075210009eeb9ae796696198a1c63046a136b256b5492d1a9d03a72aa346a64e099e81d2122c77a9d4467141193d1ff237758e99fddcb6090163fcdc07b4a408e38c0931e33a04bff1364e4176eff61bf2ca6e61edb00fef21407c7aae88f1bd6bc60e425c70f3925291422415196b176f0eda096305d68e384d9154e4f3e5ae94878b61f1eeca680fdf862ffb3be5ecc5aacb9b0f87fce89e9aedfd63d64cf869fbb46c1fe67ab854dd74d5fd58efef4ad61c9c6bacce1eafa1bbc1beac486c70c4f516c2c3b9017d1b3da567bc403b6384715098260c1fedf98770944b8d1a2dcd585427a347ed7269a9a15c73953191096ab25a720e9866b904c6104daa98c9b9b0ff647325b253ac5bfc0a667d5dfaf43f65fb7578311a85ee9a2392d31f000000ffff03004a65a9b61e020000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
|
|
@ -488,10 +488,14 @@ retry:
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@{version}/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
// This test is flaky probably because of the CDN sending the wrong file's version in some regions.
|
||||
// https://app.circleci.com/pipelines/github/btcpayserver/btcpayserver/13750/workflows/44aaf31d-0057-4fd8-a5bb-1a2c47fc530f/jobs/42963
|
||||
// It works locally depending on where you live.
|
||||
|
||||
//actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
//version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
//expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
|
||||
//EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
|
||||
|
@ -648,7 +648,7 @@ namespace BTCPayServer.Tests
|
||||
var store2 = acc.GetController<UIStoresController>();
|
||||
await store2.Pair(pairingCode.ToString(), store2.CurrentStore.Id);
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey),
|
||||
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
|
||||
store2.TempData[WellKnownTempData.ErrorMessage].ToString(), StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout * 2)]
|
||||
@ -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
|
||||
|
@ -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>();
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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" />
|
||||
|
@ -4,10 +4,12 @@
|
||||
@using BTCPayServer.Services.Notifications;
|
||||
@using Microsoft.AspNetCore.Identity;
|
||||
@using Microsoft.AspNetCore.Routing;
|
||||
@using Microsoft.Extensions.Localization
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider _AuthenticationStateProvider
|
||||
@inject NotificationManager _NotificationManager
|
||||
@inject UserManager<ApplicationUser> _UserManager
|
||||
@inject IStringLocalizer StringLocalizer
|
||||
@inject IJSRuntime _JSRuntime
|
||||
@inject LinkGenerator _LinkGenerator
|
||||
@inject BTCPayServerOptions _BTCPayServerOptions
|
||||
@ -16,13 +18,13 @@
|
||||
<div id="Notifications">
|
||||
@if (UnseenCount == "0")
|
||||
{
|
||||
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
|
||||
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="@StringLocalizer["Notifications"]">
|
||||
<Icon Symbol="nav-notifications" />
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
|
||||
<button id="NotificationsHandle" class="mainMenuButton" title="@StringLocalizer["Notifications"]" type="button" data-bs-toggle="dropdown">
|
||||
<Icon Symbol="nav-notifications" />
|
||||
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
|
||||
</button>
|
||||
@ -31,8 +33,8 @@
|
||||
{
|
||||
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
||||
<h5 class="m-0">Notifications</h5>
|
||||
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen">Mark all as seen</a>
|
||||
<h5 class="m-0" text-translate="true">Notifications</h5>
|
||||
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen" text-translate="true">Mark all as seen</a>
|
||||
</div>
|
||||
<div id="NotificationsList" v-pre>
|
||||
@foreach (var n in Last5)
|
||||
@ -54,7 +56,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<a href="@NotificationsUrl">View all</a>
|
||||
<a href="@NotificationsUrl" text-translate="true">View all</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
@using Microsoft.AspNetCore.Http
|
||||
|
||||
@inject IHttpContextAccessor HttpContextAccessor;
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
|
||||
@if (Users?.Any() is true)
|
||||
{
|
||||
<div @attributes="Attrs" class="@CssClass">
|
||||
<label for="SignedInUser" class="form-label">Signed in user</label>
|
||||
<label for="SignedInUser" class="form-label" text-translate="true">Signed in user</label>
|
||||
<select id="SignedInUser" class="form-select" value="@_userId" @onchange="@(e => _userId = e.Value?.ToString())">
|
||||
<option value="">None, just open the URL</option>
|
||||
<option value="" text-translate="true">None, just open the URL</option>
|
||||
@foreach (var u in Users)
|
||||
{
|
||||
<option value="@u.Key">@u.Value</option>
|
||||
|
@ -5,11 +5,13 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Microsoft.AspNetCore.Mvc
|
||||
@using Microsoft.AspNetCore.Routing
|
||||
@using Microsoft.Extensions.Localization
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject UserManager<ApplicationUser> UserManager;
|
||||
@inject UserLoginCodeService UserLoginCodeService;
|
||||
@inject LinkGenerator LinkGenerator;
|
||||
@inject IHttpContextAccessor HttpContextAccessor;
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
@inject UserLoginCodeService UserLoginCodeService
|
||||
@inject LinkGenerator LinkGenerator
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
@inject IStringLocalizer StringLocalizer
|
||||
@implements IDisposable
|
||||
|
||||
@if (!string.IsNullOrEmpty(_data))
|
||||
@ -18,7 +20,7 @@
|
||||
<div class="qr-container mb-2">
|
||||
<QrCode Data="@_data" Size="Size"/>
|
||||
</div>
|
||||
<p class="text-center text-muted mb-1" id="progress">Valid for @_seconds seconds</p>
|
||||
<p class="text-center text-muted mb-1" id="progress">@StringLocalizer["Valid for {0} seconds", _seconds]</p>
|
||||
<div class="progress only-for-js" data-bs-toggle="tooltip" data-bs-placement="top">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated @(Percent < 15 ? "bg-warning" : null)" role="progressbar" style="width:@Percent%" id="progressbar"></div>
|
||||
</div>
|
||||
|
@ -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" : "")>
|
||||
|
@ -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>
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
@if (Model.IsArchived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
<span class="badge bg-warning" text-translate="true">archived</span>
|
||||
}
|
||||
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
|
||||
@if (canMark)
|
||||
@ -21,13 +21,13 @@
|
||||
<div class="dropdown-menu">
|
||||
@if (Model.State.CanMarkInvalid())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid">
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid" text-translate="true">
|
||||
Mark as invalid
|
||||
</button>
|
||||
}
|
||||
@if (Model.State.CanMarkComplete())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled">
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled" text-translate="true">
|
||||
Mark as settled
|
||||
</button>
|
||||
}
|
||||
@ -62,6 +62,6 @@
|
||||
}
|
||||
@if (Model.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">Refund</span>
|
||||
<span class="badge bg-warning" text-translate="true">Refund</span>
|
||||
}
|
||||
</div>
|
||||
|
@ -11,7 +11,7 @@
|
||||
walletId = Model.WalletObjectId.WalletId
|
||||
}): string.Empty;
|
||||
}
|
||||
<input id="@elementId" placeholder="Select labels" autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
|
||||
<input id="@elementId" placeholder=@StringLocalizer["Select labels"] autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
|
||||
class="only-for-js form-control label-manager ts-wrapper @(Model.DisplayInline ? "ts-inline" : "")"
|
||||
data-fetch-url="@fetchUrl"
|
||||
data-update-url="@updateUrl"
|
||||
|
@ -106,14 +106,19 @@
|
||||
</li>
|
||||
@if (ViewData.IsCategoryActive(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsPageActive([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsPageActive([StoreNavPages.OnchainSettings], categoryId))
|
||||
{
|
||||
@if (!scheme.ReadonlyWallet)
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId" text-translate="true">Send</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId">Send</a>
|
||||
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId" text-translate="true">Receive</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId">Receive</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Settings" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Settings, scheme.WalletId.ToString()) @ViewData.ActivePageClass(StoreNavPages.OnchainSettings, categoryId)" asp-area="" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.WalletId.CryptoCode" asp-route-storeId="@scheme.WalletId.StoreId">Settings</a>
|
||||
<a id="WalletNav-Settings" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Settings, scheme.WalletId.ToString()) @ViewData.ActivePageClass(StoreNavPages.OnchainSettings, categoryId)" asp-area="" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.WalletId.CryptoCode" asp-route-storeId="@scheme.WalletId.StoreId" text-translate="true">Settings</a>
|
||||
</li>
|
||||
<vc:ui-extension-point location="wallet-nav" model="@Model" />
|
||||
}
|
||||
@ -143,7 +148,7 @@
|
||||
@if (ViewData.IsPageActive([StoreNavPages.Lightning, StoreNavPages.LightningSettings], $"{Model.Store.Id}-{scheme.CryptoCode}"))
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode">Settings</a>
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode" text-translate="true">Settings</a>
|
||||
</li>
|
||||
<vc:ui-extension-point location="lightning-nav" model="@Model"/>
|
||||
}
|
||||
@ -322,8 +327,9 @@
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Files" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Files)" asp-action="Files" text-translate="true">Files</a>
|
||||
</li>
|
||||
|
||||
<vc:ui-extension-point location="server-nav" model="@Model"/>
|
||||
}
|
||||
<vc:ui-extension-point location="server-nav" model="@Model"/>
|
||||
<li class="nav-item dropup">
|
||||
<a class="nav-link @ViewData.ActivePageClass(ManageNavPages.Index)" role="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" id="Nav-Account">
|
||||
<vc:icon symbol="nav-account"/>
|
||||
|
@ -1,3 +1,5 @@
|
||||
@using System.Web
|
||||
@using BTCPayServer.TagHelpers
|
||||
@model BasePagingViewModel
|
||||
|
||||
@{
|
||||
@ -13,7 +15,7 @@
|
||||
@if (Model.Skip > 0)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">Prev</a>
|
||||
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)" text-translate="true">Prev</a>
|
||||
</li>
|
||||
}
|
||||
<li class="page-item disabled">
|
||||
@ -35,7 +37,7 @@
|
||||
@if ((Model.Total is null && Model.CurrentPageCount >= Model.Count) || (Model.Total is not null && Model.Total.Value > Model.Skip + Model.Count))
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="@NavigatePages(1, Model.Count)">Next</a>
|
||||
<a class="page-link" href="@NavigatePages(1, Model.Count)" text-translate="true">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@ -45,7 +47,7 @@
|
||||
{
|
||||
<ul class="pagination ms-auto">
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page Size</span>
|
||||
<span class="page-link" text-translate="true">Page Size</span>
|
||||
</li>
|
||||
@foreach (var pageSize in pageSizeOptions)
|
||||
{
|
||||
@ -85,10 +87,26 @@
|
||||
{
|
||||
// merge both, preferring the `query` properties in case of duplicate keys
|
||||
query = query.Concat(Model.PaginationQuery)
|
||||
.Where(e => e.Value != null)
|
||||
.GroupBy(e => e.Key)
|
||||
.ToDictionary(g => g.Key, g => g.First().Value);
|
||||
}
|
||||
|
||||
return Url.Action(null, query);
|
||||
return ReplaceQueryParameters(query);
|
||||
}
|
||||
|
||||
string ReplaceQueryParameters(Dictionary<string, object> query)
|
||||
{
|
||||
var uri = new Uri(ViewContext.HttpContext.Request.GetCurrentUrlWithQueryString());
|
||||
var queryParams = HttpUtility.ParseQueryString(uri.Query);
|
||||
foreach (var (key, value) in query)
|
||||
{
|
||||
if (value != null) queryParams[key] = value?.ToString();
|
||||
}
|
||||
var uriBuilder = new UriBuilder(uri)
|
||||
{
|
||||
Query = queryParams.ToString()!
|
||||
};
|
||||
return uriBuilder.ToString();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>.
|
||||
|
@ -3,8 +3,8 @@
|
||||
<div class="btcpay-theme-switch @Model.CssClass">
|
||||
<span class="btcpay-theme-switch-label" text-translate="true">Theme</span>
|
||||
<div class="btcpay-theme-switch-themes">
|
||||
<button type="button" title="System" data-theme="system"><vc:icon symbol="themes-system"/></button>
|
||||
<button type="button" title="Light" data-theme="light"><vc:icon symbol="themes-light"/></button>
|
||||
<button type="button" title="Dark" data-theme="dark"><vc:icon symbol="themes-dark"/></button>
|
||||
<button type="button" title="@StringLocalizer["System"]" data-theme="system"><vc:icon symbol="themes-system"/></button>
|
||||
<button type="button" title="@StringLocalizer["Light"]" data-theme="light"><vc:icon symbol="themes-light"/></button>
|
||||
<button type="button" title="@StringLocalizer["Dark"]" data-theme="dark"><vc:icon symbol="themes-dark"/></button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,27 +1,15 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Secp256k1;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BTCPayServer.Components.WalletNav
|
||||
{
|
||||
@ -33,6 +21,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly DefaultRulesCollection _defaultRules;
|
||||
private readonly RateFetcher _rateFetcher;
|
||||
private IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public WalletNav(
|
||||
BTCPayWalletProvider walletProvider,
|
||||
@ -40,6 +29,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
UIWalletsController walletsController,
|
||||
CurrencyNameTable currencies,
|
||||
DefaultRulesCollection defaultRules,
|
||||
IStringLocalizer stringLocalizer,
|
||||
RateFetcher rateFetcher)
|
||||
{
|
||||
_walletProvider = walletProvider;
|
||||
@ -48,6 +38,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
_currencies = currencies;
|
||||
_defaultRules = defaultRules;
|
||||
_rateFetcher = rateFetcher;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(WalletId walletId)
|
||||
@ -71,7 +62,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
Network = network,
|
||||
Balance = balance.ShowMoney(network),
|
||||
DefaultCurrency = defaultCurrency,
|
||||
Label = derivation?.Label ?? $"{store.StoreName} {walletId.CryptoCode} Wallet"
|
||||
Label = derivation?.Label ?? $"{store.StoreName} {StringLocalizer["{0} Wallet", walletId.CryptoCode]}"
|
||||
};
|
||||
|
||||
if (defaultCurrency != network.CryptoCode)
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -47,6 +48,7 @@ namespace BTCPayServer.Controllers
|
||||
readonly ILogger _logger;
|
||||
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
public Logs Logs { get; }
|
||||
|
||||
public UIAccountController(
|
||||
@ -62,6 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
LnurlAuthService lnurlAuthService,
|
||||
LinkGenerator linkGenerator,
|
||||
IStringLocalizer stringLocalizer,
|
||||
Logs logs)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@ -78,6 +81,7 @@ namespace BTCPayServer.Controllers
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logs.PayServer;
|
||||
Logs = logs;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
@ -149,7 +153,7 @@ namespace BTCPayServer.Controllers
|
||||
var userId = _userLoginCodeService.Verify(code);
|
||||
if (userId is null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Login code was invalid";
|
||||
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Login code was invalid"].Value;
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
|
||||
@ -187,7 +191,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
// Require the user to pass basic checks (approval, confirmed email, not disabled) before they can log on
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
const string errorMessage = "Invalid login attempt.";
|
||||
var errorMessage = StringLocalizer["Invalid login attempt."].Value;
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
@ -311,7 +315,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var errorMessage = "Invalid login attempt.";
|
||||
var errorMessage = StringLocalizer["Invalid login attempt."].Value;
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
@ -629,7 +633,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
RegisteredUserId = user.Id;
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Account created.";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account created."].Value;
|
||||
var requiresConfirmedEmail = policies.RequiresConfirmedEmail && !user.EmailConfirmed;
|
||||
var requiresUserApproval = policies.RequiresUserApproval && !user.Approved;
|
||||
if (requiresConfirmedEmail)
|
||||
@ -704,7 +708,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
Message = StringLocalizer["Your email has been confirmed."].Value
|
||||
});
|
||||
await FinalizeInvitationIfApplicable(user);
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
@ -713,7 +717,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your email has been confirmed. Please set your password."
|
||||
Message = StringLocalizer["Your email has been confirmed. Please set your password."].Value
|
||||
});
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
@ -811,7 +815,9 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = hasPassword ? "Password successfully set." : "Account successfully created."
|
||||
Message = hasPassword
|
||||
? StringLocalizer["Password successfully set."].Value
|
||||
: StringLocalizer["Account successfully created."].Value
|
||||
});
|
||||
if (!hasPassword) await FinalizeInvitationIfApplicable(user);
|
||||
return RedirectToAction(nameof(Login));
|
||||
@ -848,7 +854,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Invitation accepted. Please set your password."
|
||||
Message = StringLocalizer["Invitation accepted. Please set your password."].Value
|
||||
});
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
@ -857,7 +863,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your password has been set by the user who invited you."
|
||||
Message = StringLocalizer["Your password has been set by the user who invited you."].Value
|
||||
});
|
||||
|
||||
await FinalizeInvitationIfApplicable(user);
|
||||
@ -930,7 +936,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "You cannot login over an insecure connection. Please use HTTPS or Tor."
|
||||
Message = StringLocalizer["You cannot login over an insecure connection. Please use HTTPS or Tor."].Value
|
||||
});
|
||||
|
||||
ViewData["disabled"] = true;
|
||||
|
@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository,
|
||||
IFileService fileService,
|
||||
AppService appService,
|
||||
IStringLocalizer stringLocalizer,
|
||||
IHtmlHelper html)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@ -39,6 +41,7 @@ namespace BTCPayServer.Controllers
|
||||
_fileService = fileService;
|
||||
_appService = appService;
|
||||
Html = html;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
@ -50,6 +53,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public string CreatedAppId { get; set; }
|
||||
public IHtmlHelper Html { get; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public class AppUpdated
|
||||
{
|
||||
@ -158,7 +162,7 @@ namespace BTCPayServer.Controllers
|
||||
var type = _appService.GetAppType(vm.AppType ?? vm.SelectedAppType);
|
||||
if (type is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
|
||||
ModelState.AddModelError(nameof(vm.SelectedAppType), StringLocalizer["Invalid App Type"]);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
@ -177,7 +181,7 @@ namespace BTCPayServer.Controllers
|
||||
await _appService.SetDefaultSettings(appData, defaultCurrency);
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["App successfully created"].Value;
|
||||
CreatedAppId = appData.Id;
|
||||
|
||||
var url = await type.ConfigureLink(appData);
|
||||
@ -192,7 +196,7 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{Html.Encode(app.Name)}</strong> and its settings will be permanently deleted. Are you sure?", "Delete"));
|
||||
return View("Confirm", new ConfirmModel(StringLocalizer["Delete app"], $"The app <strong>{Html.Encode(app.Name)}</strong> and its settings will be permanently deleted. Are you sure?", StringLocalizer["Delete"]));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -204,7 +208,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
if (await _appService.DeleteApp(app))
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App deleted successfully.";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["App deleted successfully."].Value;
|
||||
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
|
||||
}
|
||||
@ -227,12 +231,14 @@ namespace BTCPayServer.Controllers
|
||||
if (await _appService.SetArchived(app, archived))
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = archived
|
||||
? "The app has been archived and will no longer appear in the apps list by default."
|
||||
: "The app has been unarchived and will appear in the apps list by default again.";
|
||||
? StringLocalizer["The app has been archived and will no longer appear in the apps list by default."].Value
|
||||
: StringLocalizer["The app has been unarchived and will appear in the apps list by default again."].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Failed to {(archived ? "archive" : "unarchive")} the app.";
|
||||
TempData[WellKnownTempData.ErrorMessage] = archived
|
||||
? StringLocalizer["Failed to archive the app."].Value
|
||||
: StringLocalizer["Failed to unarchive the app."].Value;
|
||||
}
|
||||
|
||||
var url = await type.ConfigureLink(app);
|
||||
|
@ -65,6 +65,6 @@ public class UIBoltcardController : Controller
|
||||
if (!cardKey.CheckSunMac(c, piccData))
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", cancellationToken);
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", true, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ namespace BTCPayServer.Controllers
|
||||
var newDeliveryId = await WebhookNotificationManager.Redeliver(deliveryId);
|
||||
if (newDeliveryId is null)
|
||||
return NotFound();
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Successfully planned a redelivery"].Value;
|
||||
return RedirectToAction(nameof(Invoice),
|
||||
new
|
||||
{
|
||||
@ -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
|
||||
|
@ -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)
|
||||
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -23,22 +24,24 @@ namespace BTCPayServer
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly LnurlAuthService _lnurlAuthService;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UILNURLAuthController(UserManager<ApplicationUser> userManager, LnurlAuthService lnurlAuthService,
|
||||
LinkGenerator linkGenerator)
|
||||
IStringLocalizer stringLocalizer, LinkGenerator linkGenerator)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_lnurlAuthService = lnurlAuthService;
|
||||
_linkGenerator = linkGenerator;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/delete")]
|
||||
public IActionResult Remove(string id)
|
||||
{
|
||||
return View("Confirm",
|
||||
new ConfirmModel("Remove LNURL Auth link",
|
||||
"Your account will no longer have this Lightning wallet as an option for two-factor authentication.",
|
||||
"Remove"));
|
||||
new ConfirmModel(StringLocalizer["Remove LNURL Auth link"],
|
||||
StringLocalizer["Your account will no longer have this Lightning wallet as an option for two-factor authentication."],
|
||||
StringLocalizer["Remove"]));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
@ -49,7 +52,7 @@ namespace BTCPayServer
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = "LNURL Auth was removed successfully."
|
||||
Message = StringLocalizer["LNURL Auth was removed successfully."].Value
|
||||
});
|
||||
|
||||
return RedirectToList();
|
||||
@ -65,7 +68,7 @@ namespace BTCPayServer
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Html = "The Lightning node could not be registered."
|
||||
Html = StringLocalizer["The Lightning node could not be registered."].Value
|
||||
});
|
||||
|
||||
return RedirectToList();
|
||||
|
@ -19,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)
|
||||
{
|
||||
|
@ -58,10 +58,10 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
await _apiKeyRepository.Remove(id, _userManager.GetUserId(User));
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "API Key removed"
|
||||
Message = StringLocalizer["API Key removed"].Value
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
@ -71,10 +71,10 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure(HttpContext))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot generate api keys while not on https or tor"
|
||||
Message = StringLocalizer["Cannot generate API keys while not using HTTPS or Tor"].Value
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
@ -91,7 +91,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot generate API keys while not on https or using Tor"
|
||||
Message = StringLocalizer["Cannot generate API keys while not using HTTPS or Tor"].Value
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
@ -199,7 +199,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code class='alert-link'>{key.Id}</code>"
|
||||
Html = StringLocalizer["API key generated!"].Value + $" <code class='alert-link'>{key.Id}</code>"
|
||||
});
|
||||
|
||||
return RedirectToAction("APIKeys", new { key = key.Id });
|
||||
@ -242,7 +242,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code class='alert-link'>{key.Id}</code>"
|
||||
Html = StringLocalizer["API key generated!"].Value + $" <code class='alert-link'>{key.Id}</code>"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
@ -56,7 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
await _userManager.UpdateAsync(user);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = "Updated successfully.",
|
||||
Message = StringLocalizer["Updated successfully."].Value,
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
return RedirectToAction("NotificationSettings");
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 { }
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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 });
|
||||
|
@ -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,
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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 });
|
||||
|
@ -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"]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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 });
|
||||
|
@ -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
|
||||
|
@ -6,6 +6,5 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
public string? Id { get; }
|
||||
decimal? Amount { get; }
|
||||
bool IsExplicitAmountMinimum => false;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -4,6 +4,7 @@ namespace BTCPayServer.Forms;
|
||||
|
||||
public class ModifyForm
|
||||
{
|
||||
[DisplayName("Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[DisplayName("Form configuration (JSON)")]
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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; }
|
||||
|
@ -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 =>
|
||||
|
@ -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)
|
||||
|
@ -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
Reference in New Issue
Block a user