Compare commits
132 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 | |||
c77c2f8bd6 | |||
402eaa8f12 | |||
212e8c3654 | |||
7c77b16517 | |||
e5bb0bcba3 | |||
ca4a7d8771 | |||
663f97265a | |||
b91f3048ef | |||
4fe0bf1236 | |||
dd35af3c55 | |||
68f24e47cd | |||
968223a953 | |||
2f287874e3 | |||
c35e7406cd | |||
34b2cca492 | |||
e1bfc04451 | |||
ef0ba7b0c4 | |||
0a2d8880ba | |||
8dcd7e6966 | |||
b744fd6167 | |||
5bcc5c919a | |||
01e12329e9 | |||
471bf57835 | |||
b246beab3e | |||
abc8161a08 | |||
64ba8248d2 | |||
206d222455 | |||
2e114d7c29 | |||
5190c25be0 | |||
5704919b3a | |||
c3e51f51b6 | |||
8c35edb6e8 | |||
2f2b4094f6 | |||
413a9b4269 | |||
a698aa8a5b | |||
0f79526566 | |||
1ffbab7338 | |||
3a71c45a89 | |||
8f062f918b | |||
2f05d00219 | |||
b48ca92675 | |||
4a31cf0a09 | |||
82620ee327 | |||
6d284b4124 | |||
83fa8cbf0f | |||
9ba4b030ed | |||
272cc3d3c9 | |||
b5590a38fe | |||
443a350bad | |||
7013e618de | |||
363b60385b | |||
90635ffc4e | |||
056f850268 |
@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
@ -14,4 +15,5 @@ public interface IFileService
|
||||
Task<string?> GetTemporaryFileUrl(Uri baseUri, string fileId, DateTimeOffset expiry,
|
||||
bool isDownload);
|
||||
Task RemoveFile(string fileId, string userId);
|
||||
Task<UploadImageResultModel> UploadImage(IFormFile file, string userId, long maxFileSizeInBytes = 1_000_000);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
|
||||
public interface ISyncStatus
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string PaymentMethodId { get; set; }
|
||||
public bool Available { get; }
|
||||
}
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
viewData[ACTIVE_CATEGORY_KEY] = activeCategory;
|
||||
}
|
||||
|
||||
public static bool IsActiveCategory(this ViewDataDictionary viewData, string category, object id = null)
|
||||
public static bool IsCategoryActive(this ViewDataDictionary viewData, string category, object id = null)
|
||||
{
|
||||
if (!viewData.ContainsKey(ACTIVE_CATEGORY_KEY)) return false;
|
||||
var activeId = viewData[ACTIVE_ID_KEY];
|
||||
@ -65,12 +65,12 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
return categoryMatch && idMatch;
|
||||
}
|
||||
|
||||
public static bool IsActiveCategory<T>(this ViewDataDictionary viewData, T category, object id = null)
|
||||
public static bool IsCategoryActive<T>(this ViewDataDictionary viewData, T category, object id = null)
|
||||
{
|
||||
return IsActiveCategory(viewData, category.ToString(), id);
|
||||
return IsCategoryActive(viewData, category.ToString(), id);
|
||||
}
|
||||
|
||||
public static bool IsActivePage(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
public static bool IsPageActive(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
{
|
||||
if (!viewData.ContainsKey(ACTIVE_PAGE_KEY)) return false;
|
||||
var activeId = viewData[ACTIVE_ID_KEY];
|
||||
@ -82,7 +82,7 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
return categoryAndPageMatch && idMatch;
|
||||
}
|
||||
|
||||
public static bool IsActivePage<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null)
|
||||
public static bool IsPageActive<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null)
|
||||
where T : IConvertible
|
||||
{
|
||||
return pages.Any(page => ActivePageClass(viewData, page.ToString(), page.GetType().ToString(), id) == ACTIVE_CLASS);
|
||||
@ -95,7 +95,7 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
|
||||
public static string ActiveCategoryClass(this ViewDataDictionary viewData, string category, object id = null)
|
||||
{
|
||||
return IsActiveCategory(viewData, category, id) ? ACTIVE_CLASS : null;
|
||||
return IsCategoryActive(viewData, category, id) ? ACTIVE_CLASS : null;
|
||||
}
|
||||
|
||||
public static string ActivePageClass<T>(this ViewDataDictionary viewData, T page, object id = null)
|
||||
@ -106,12 +106,42 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
|
||||
public static string ActivePageClass(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
{
|
||||
return IsActivePage(viewData, page, category, id) ? ACTIVE_CLASS : null;
|
||||
return IsPageActive(viewData, page, category, id) ? ACTIVE_CLASS : null;
|
||||
}
|
||||
|
||||
|
||||
public static string ActivePageClass<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null) where T : IConvertible
|
||||
{
|
||||
return IsActivePage(viewData, pages, id) ? ACTIVE_CLASS : null;
|
||||
return IsPageActive(viewData, pages, id) ? ACTIVE_CLASS : null;
|
||||
}
|
||||
|
||||
[Obsolete("Use ActiveCategoryClass instead")]
|
||||
public static string IsActiveCategory<T>(this ViewDataDictionary viewData, T category, object id = null)
|
||||
{
|
||||
return ActiveCategoryClass(viewData, category, id);
|
||||
}
|
||||
|
||||
[Obsolete("Use ActiveCategoryClass instead")]
|
||||
public static string IsActiveCategory(this ViewDataDictionary viewData, string category, object id = null)
|
||||
{
|
||||
return ActiveCategoryClass(viewData, category, id);
|
||||
}
|
||||
|
||||
[Obsolete("Use ActivePageClass instead")]
|
||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page, object id = null) where T : IConvertible
|
||||
{
|
||||
return ActivePageClass(viewData, page, id);
|
||||
}
|
||||
|
||||
[Obsolete("Use ActivePageClass instead")]
|
||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null) where T : IConvertible
|
||||
{
|
||||
return ActivePageClass(viewData, pages, id);
|
||||
}
|
||||
|
||||
[Obsolete("Use ActivePageClass instead")]
|
||||
public static string IsActivePage(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
{
|
||||
return ActivePageClass(viewData, page, category, id);
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate(this DateTimeOffset date, string netFormat, string jsDateFormat = "short", string jsTimeFormat = "short")
|
||||
|
@ -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
BTCPayServer.Abstractions/Models/UploadImageResultModel.cs
Normal file
16
BTCPayServer.Abstractions/Models/UploadImageResultModel.cs
Normal file
@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Models;
|
||||
|
||||
public class UploadImageResultModel
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Response { get; set; } = string.Empty;
|
||||
public IStoredFile? StoredFile { get; set; }
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Services
|
||||
{
|
||||
public class UIExtension : IUIExtension
|
||||
{
|
||||
[Obsolete("Use extension method BTCPayServer.Extensions.AddUIExtension(this IServiceCollection services, string location, string partialViewName) instead")]
|
||||
public UIExtension(string partial, string location)
|
||||
{
|
||||
Partial = partial;
|
||||
|
@ -16,7 +16,7 @@
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">2.0.0</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
|
@ -46,9 +46,15 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<InvoiceData>($"api/v1/stores/{storeId}/invoices/{invoiceId}", null, HttpMethod.Get, token);
|
||||
}
|
||||
public virtual async Task<InvoicePaymentMethodDataModel[]> GetInvoicePaymentMethods(string storeId, string invoiceId,
|
||||
bool onlyAccountedPayments = true, bool includeSensitive = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<InvoicePaymentMethodDataModel[]>($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods", null, HttpMethod.Get, token);
|
||||
var queryPayload = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(onlyAccountedPayments), onlyAccountedPayments },
|
||||
{ nameof(includeSensitive), includeSensitive }
|
||||
};
|
||||
return await SendHttpRequest<InvoicePaymentMethodDataModel[]>($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task ArchiveInvoice(string storeId, string invoiceId,
|
||||
|
@ -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)
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? StartsAt { get; set; }
|
||||
public string[] PaymentMethods { get; set; }
|
||||
public string[] PayoutMethods { get; set; }
|
||||
public bool AutoApproveClaims { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -22,11 +22,12 @@ namespace BTCPayServer.Client.Models
|
||||
public string PullPaymentId { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public decimal OriginalAmount { get; set; }
|
||||
public string OriginalCurrency { get; set; }
|
||||
public string PayoutCurrency { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? PaymentMethodAmount { get; set; }
|
||||
public decimal? PayoutAmount { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PayoutState State { get; set; }
|
||||
public int Revision { get; set; }
|
||||
|
@ -4,6 +4,6 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FriendlyName { get; set; }
|
||||
public string[] PaymentMethods { get; set; }
|
||||
public string[] PayoutMethods { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public class SyncStatus
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string PaymentMethodId { get; set; }
|
||||
public virtual bool Available { 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)
|
||||
};
|
||||
}
|
||||
@ -147,6 +148,23 @@ namespace BTCPayServer.Data
|
||||
return $"{splitted[0]}-CHAIN";
|
||||
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
|
||||
}
|
||||
public static string TryMigratePaymentMethodId(string paymentMethodId)
|
||||
{
|
||||
var splitted = paymentMethodId.Split(new[] { '_', '-' });
|
||||
if (splitted is [var cryptoCode, var paymentType])
|
||||
{
|
||||
return paymentType switch
|
||||
{
|
||||
"BTCLike" or "MoneroLike" or "ZcashLike" => $"{cryptoCode}-CHAIN",
|
||||
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
|
||||
"LNURLPAY" => $"{cryptoCode}-LNURL",
|
||||
_ => paymentMethodId
|
||||
};
|
||||
}
|
||||
if (splitted.Length == 1)
|
||||
return $"{splitted[0]}-CHAIN";
|
||||
return paymentMethodId;
|
||||
}
|
||||
|
||||
// Make postgres happy
|
||||
public static string SanitizeJSON(string json) => json.Replace("\\u0000", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
@ -92,7 +92,7 @@ namespace BTCPayServer.Data
|
||||
blob.Remove("output");
|
||||
blob.Remove("outpoint");
|
||||
// Convert from sats to btc
|
||||
if (cryptoData["value"] is not (null or { Type: JTokenType.Null }))
|
||||
if (cryptoData["value"] is not (null or { Type: JTokenType.Null } or { Type: JTokenType.Object }))
|
||||
{
|
||||
var v = cryptoData["value"].Value<long>();
|
||||
Amount = (decimal)v / (decimal)Money.COIN;
|
||||
@ -103,7 +103,22 @@ namespace BTCPayServer.Data
|
||||
blob.ConvertNumberToString("paymentMethodFee");
|
||||
blob.Remove("networkFee");
|
||||
blob.RemoveIfNull("paymentMethodFee");
|
||||
}
|
||||
}
|
||||
// Liquid
|
||||
else if (cryptoData["value"] is { Type: JTokenType.Object })
|
||||
{
|
||||
var v = cryptoData["value"]["value"].Value<long>();
|
||||
var assetId = cryptoData["value"]["assetId"].Value<string>();
|
||||
divisibility = GetDivisibility(assetId) ?? 8;
|
||||
Amount = (decimal)v / (decimal)Math.Pow(10.0, divisibility);
|
||||
cryptoData.Remove("value");
|
||||
cryptoData["assetId"] = assetId;
|
||||
blob["paymentMethodFee"] = blob["networkFee"];
|
||||
blob.RemoveIfValue<decimal>("paymentMethodFee", 0.0m);
|
||||
blob.ConvertNumberToString("paymentMethodFee");
|
||||
blob.Remove("networkFee");
|
||||
blob.RemoveIfNull("paymentMethodFee");
|
||||
}
|
||||
// Convert from millisats to btc
|
||||
else if (cryptoData["amount"] is not (null or { Type: JTokenType.Null }))
|
||||
{
|
||||
@ -164,6 +179,17 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return true;
|
||||
}
|
||||
|
||||
private int? GetDivisibility(string assetId) =>
|
||||
assetId switch
|
||||
{
|
||||
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2" => 8,
|
||||
"aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf" => 2,
|
||||
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a" => 8,
|
||||
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d" => 8,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
[NotMapped]
|
||||
|
39
BTCPayServer.Data/Data/PaymentRequestData.Migration.cs
Normal file
39
BTCPayServer.Data/Data/PaymentRequestData.Migration.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class PaymentRequestData : MigrationInterceptor.IHasMigration
|
||||
{
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
|
||||
public bool TryMigrate()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (Blob is null && Blob2 is not null)
|
||||
return false;
|
||||
if (Blob2 is null)
|
||||
{
|
||||
Blob2 = Blob is not (null or { Length: 0 }) ? MigrationExtensions.Unzip(Blob) : "{}";
|
||||
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
|
||||
}
|
||||
Blob = null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
var jobj = JObject.Parse(Blob2);
|
||||
// Fixup some legacy payment requests
|
||||
if (jobj["expiryDate"].Type == JTokenType.Date)
|
||||
{
|
||||
jobj["expiryDate"] = new JValue(NBitcoin.Utils.DateTimeToUnixTime(jobj["expiryDate"].Value<DateTime>()));
|
||||
Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class PaymentRequestData : IHasBlobUntyped
|
||||
public partial class PaymentRequestData : IHasBlobUntyped
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
|
@ -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");
|
||||
|
@ -1175,13 +1175,6 @@
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"USDt",
|
||||
"code":"USDT",
|
||||
"divisibility":8,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"LCAD",
|
||||
"code":"LCAD",
|
||||
@ -1315,13 +1308,6 @@
|
||||
"symbol": null,
|
||||
"crypto": true
|
||||
},
|
||||
{
|
||||
"name":"USDt",
|
||||
"code":"USDT20",
|
||||
"divisibility":6,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"FaucetToken",
|
||||
"code":"FAU",
|
||||
|
@ -5,6 +5,9 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -18,14 +21,74 @@ namespace BTCPayServer.Services.Rates
|
||||
public string Symbol { get; set; }
|
||||
public bool Crypto { get; set; }
|
||||
}
|
||||
public class CurrencyNameTable
|
||||
public interface CurrencyDataProvider
|
||||
{
|
||||
public static CurrencyNameTable Instance = new();
|
||||
public CurrencyNameTable()
|
||||
Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken);
|
||||
}
|
||||
public class InMemoryCurrencyDataProvider : CurrencyDataProvider
|
||||
{
|
||||
private readonly CurrencyData[] _currencyData;
|
||||
|
||||
public InMemoryCurrencyDataProvider(CurrencyData[] currencyData)
|
||||
{
|
||||
_Currencies = LoadCurrency().ToDictionary(k => k.Code, StringComparer.InvariantCultureIgnoreCase);
|
||||
_currencyData = currencyData;
|
||||
}
|
||||
|
||||
public Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken) => Task.FromResult(_currencyData);
|
||||
}
|
||||
public class AssemblyCurrencyDataProvider : CurrencyDataProvider
|
||||
{
|
||||
private readonly Assembly _assembly;
|
||||
private readonly string _manifestResourceStream;
|
||||
|
||||
public AssemblyCurrencyDataProvider(Assembly assembly, string manifestResourceStream)
|
||||
{
|
||||
_assembly = assembly;
|
||||
_manifestResourceStream = manifestResourceStream;
|
||||
}
|
||||
public Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken)
|
||||
{
|
||||
var stream = _assembly.GetManifestResourceStream(_manifestResourceStream);
|
||||
if (stream is null)
|
||||
throw new InvalidOperationException("Unknown manifestResourceStream");
|
||||
string content = null;
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
content = reader.ReadToEnd();
|
||||
}
|
||||
|
||||
var currencies = JsonConvert.DeserializeObject<CurrencyData[]>(content);
|
||||
return Task.FromResult(currencies.ToArray());
|
||||
}
|
||||
}
|
||||
public class CurrencyNameTable
|
||||
{
|
||||
public CurrencyNameTable(IEnumerable<CurrencyDataProvider> currencyDataProviders, ILogger<CurrencyNameTable> logger)
|
||||
{
|
||||
_currencyDataProviders = currencyDataProviders;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task ReloadCurrencyData(CancellationToken cancellationToken)
|
||||
{
|
||||
var currencies = new Dictionary<string, CurrencyData>(StringComparer.InvariantCultureIgnoreCase);
|
||||
var loadings = _currencyDataProviders.Select(c => (Task: c.LoadCurrencyData(cancellationToken), Prov: c)).ToList();
|
||||
foreach (var loading in loadings)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var curr in await loading.Task)
|
||||
{
|
||||
currencies.TryAdd(curr.Code, curr);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error loading currency data for " + loading.Prov.GetType().FullName);
|
||||
}
|
||||
}
|
||||
_Currencies = currencies;
|
||||
}
|
||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new();
|
||||
|
||||
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
|
||||
@ -123,20 +186,9 @@ namespace BTCPayServer.Services.Rates
|
||||
currencyProviders.TryAdd(code, number);
|
||||
}
|
||||
|
||||
readonly Dictionary<string, CurrencyData> _Currencies;
|
||||
|
||||
static CurrencyData[] LoadCurrency()
|
||||
{
|
||||
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Rating.Currencies.json");
|
||||
string content = null;
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
content = reader.ReadToEnd();
|
||||
}
|
||||
|
||||
var currencies = JsonConvert.DeserializeObject<CurrencyData[]>(content);
|
||||
return currencies;
|
||||
}
|
||||
Dictionary<string, CurrencyData> _Currencies = new();
|
||||
private readonly IEnumerable<CurrencyDataProvider> _currencyDataProviders;
|
||||
private readonly ILogger<CurrencyNameTable> _logger;
|
||||
|
||||
public IEnumerable<CurrencyData> Currencies => _Currencies.Values;
|
||||
|
||||
|
@ -1,11 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public class CurrencyPair
|
||||
{
|
||||
private static readonly HashSet<string> _knownCurrencies;
|
||||
|
||||
static CurrencyPair()
|
||||
{
|
||||
var prov = new AssemblyCurrencyDataProvider(typeof(BTCPayServer.Rating.BidAsk).Assembly, "BTCPayServer.Rating.Currencies.json");
|
||||
// It's OK this is sync function
|
||||
_knownCurrencies = prov.LoadCurrencyData(default).GetAwaiter().GetResult()
|
||||
.Select(c => c.Code).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
public CurrencyPair(string left, string right)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(right);
|
||||
@ -49,10 +60,9 @@ namespace BTCPayServer.Rating
|
||||
for (int i = 3; i < 5; i++)
|
||||
{
|
||||
var potentialCryptoName = currencyPair.Substring(0, i);
|
||||
var currency = CurrencyNameTable.Instance.GetCurrencyData(potentialCryptoName, false);
|
||||
if (currency != null)
|
||||
if (_knownCurrencies.Contains(potentialCryptoName))
|
||||
{
|
||||
value = new CurrencyPair(currency.Code, currencyPair.Substring(i));
|
||||
value = new CurrencyPair(potentialCryptoName, currencyPair.Substring(i));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -320,10 +320,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
|
||||
var checkout =
|
||||
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
|
||||
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id)
|
||||
.GetAwaiter().GetResult()).Value;
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
Assert.Single(checkout.AvailablePaymentMethods);
|
||||
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
|
||||
|
||||
//////////////////////
|
||||
|
||||
@ -337,7 +337,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
|
||||
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id)
|
||||
.GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal("Processing", checkout.Status);
|
||||
});
|
||||
@ -475,10 +475,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
|
||||
var checkout =
|
||||
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null)
|
||||
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null)
|
||||
.GetAwaiter().GetResult()).Value;
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("BTC", checkout.CryptoCode);
|
||||
Assert.Single(checkout.AvailablePaymentMethods);
|
||||
Assert.Equal("BTC", checkout.PaymentMethodCurrency);
|
||||
|
||||
Assert.Single(invoice.PaymentCodes);
|
||||
Assert.Single(invoice.SupportedTransactionCurrencies);
|
||||
@ -536,10 +536,10 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
|
||||
controller = tester.PayTester.GetController<UIInvoiceController>(null);
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
|
||||
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
|
||||
.GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal(2, checkout.AvailableCryptos.Count);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
Assert.Equal(2, checkout.AvailablePaymentMethods.Count);
|
||||
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
|
||||
|
||||
Assert.Equal(2, invoice.PaymentCodes.Count());
|
||||
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
|
||||
|
@ -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
|
||||
|
@ -7,6 +7,15 @@
|
||||
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="BTCPayServer.Tests.OutputPathAttribute">
|
||||
<!-- _Parameter1, _Parameter2, etc. correspond to the
|
||||
matching parameter of a constructor of that .NET attribute type -->
|
||||
<_Parameter1>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(OutputPath)'))</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
|
||||
<Target Name="CopyAditionalFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
|
||||
<ItemGroup>
|
||||
@ -61,4 +70,7 @@
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="obj\Debug\net8.0\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -162,6 +162,8 @@ namespace BTCPayServer.Tests
|
||||
HttpClient.BaseAddress = ServerUri;
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var confBuilder = new DefaultConfiguration() { Logger = LoggerProvider.CreateLogger("Console") }.CreateConfigurationBuilder(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", DisableRegistration ? "true" : "false" });
|
||||
// This make sure that tests work outside of this assembly (ie, test project it a plugin)
|
||||
confBuilder.SetBasePath(TestUtils.TestDirectory);
|
||||
#if DEBUG
|
||||
confBuilder.AddJsonFile("appsettings.dev.json", true, false);
|
||||
#endif
|
||||
@ -265,7 +267,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
private string FindBTCPayServerDirectory()
|
||||
{
|
||||
var solutionDirectory = TestUtils.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
|
||||
var solutionDirectory = TestUtils.TryGetSolutionDirectoryInfo();
|
||||
return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,8 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
await user2.RegisterDerivationSchemeAsync("BTC");
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
@ -79,7 +81,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(app.Archived);
|
||||
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
crowdfundViewModel.Enabled = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
|
||||
// Delete
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
|
||||
@ -119,7 +121,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.Enabled = false;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
|
||||
var crowdfundController = user.GetController<UICrowdfundController>();
|
||||
@ -144,7 +146,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
@ -155,7 +157,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
@ -168,7 +170,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.TargetAmount = 1;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
@ -212,7 +214,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
var publicApps = user.GetController<UICrowdfundController>();
|
||||
|
||||
@ -266,7 +268,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
|
||||
|
||||
crowdfundViewModel.UseAllStoreInvoices = false;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
|
||||
@ -285,7 +287,7 @@ namespace BTCPayServer.Tests
|
||||
TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
|
||||
crowdfundViewModel.EnforceTargetAmount = false;
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
|
||||
{
|
||||
Buyer = new Buyer { email = "test@fwf.com" },
|
||||
@ -354,7 +356,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.FormId = lstForms[0].Id;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01).AssertViewModelAsync<FormViewModel>();
|
||||
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "", vm2);
|
||||
@ -409,7 +411,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.PerksTemplate = "[{\"id\": \"xxx\",\"title\": \"Perk 1\",\"priceType\": \"Fixed\",\"price\": \"0.001\",\"image\": \"\",\"description\": \"\",\"categories\": [],\"disabled\": false}]";
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01, "xxx").AssertViewModelAsync<FormViewModel>();
|
||||
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "xxx", vm2);
|
||||
|
@ -40,6 +40,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -678,10 +679,29 @@ 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()
|
||||
{
|
||||
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
|
||||
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
@ -738,10 +758,29 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF3, 0xE4, 0x64, 0x00, 0x20, 0xAD, 0xBD, 0x04, 0x00 }, "music.mp3"));
|
||||
}
|
||||
|
||||
CurrencyNameTable GetCurrencyNameTable()
|
||||
{
|
||||
ServiceCollection services = new ServiceCollection();
|
||||
services.AddLogging(o => o.AddProvider(this.TestLogProvider));
|
||||
BTCPayServerServices.RegisterCurrencyData(services);
|
||||
// One test fail without.
|
||||
services.AddCurrencyData(new CurrencyData()
|
||||
{
|
||||
Code = "USDt",
|
||||
Name = "USDt",
|
||||
Divisibility = 8,
|
||||
Symbol = null,
|
||||
Crypto = true
|
||||
});
|
||||
var table = services.BuildServiceProvider().GetRequiredService<CurrencyNameTable>();
|
||||
table.ReloadCurrencyData(default).GetAwaiter().GetResult();
|
||||
return table;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundupCurrenciesCorrectly()
|
||||
{
|
||||
DisplayFormatter displayFormatter = new(CurrencyNameTable.Instance);
|
||||
DisplayFormatter displayFormatter = new(GetCurrencyNameTable());
|
||||
foreach (var test in new[]
|
||||
{
|
||||
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
|
||||
@ -754,8 +793,8 @@ namespace BTCPayServer.Tests
|
||||
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
|
||||
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("COP").CurrencyDecimalDigits);
|
||||
Assert.Equal(0, GetCurrencyNameTable().GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
|
||||
Assert.Equal(0, GetCurrencyNameTable().GetNumberFormatInfo("COP").CurrencyDecimalDigits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1377,7 +1416,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
var btcPayNetworkProvider = CreateNetworkProvider(ChainName.Regtest);
|
||||
foreach (var network in btcPayNetworkProvider.GetAll())
|
||||
{
|
||||
var cd = CurrencyNameTable.Instance.GetCurrencyData(network.CryptoCode, false);
|
||||
var cd = GetCurrencyNameTable().GetCurrencyData(network.CryptoCode, false);
|
||||
Assert.NotNull(cd);
|
||||
Assert.Equal(network.Divisibility, cd.Divisibility);
|
||||
Assert.True(cd.Crypto);
|
||||
@ -1445,8 +1484,8 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
Assert.True(CurrencyValue.TryParse("1usd", out result));
|
||||
Assert.Equal("1 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1.501 usd", out result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.False(CurrencyValue.TryParse("1.501 WTFF", out result));
|
||||
Assert.Equal("1.501 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1.501 WTFF", out result));
|
||||
Assert.False(CurrencyValue.TryParse("1,501 usd", out result));
|
||||
Assert.False(CurrencyValue.TryParse("1.501", out result));
|
||||
}
|
||||
|
@ -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;
|
||||
@ -97,6 +98,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(e.APIError.Message);
|
||||
GreenfieldPermissionAPIError permissionError = Assert.IsType<GreenfieldPermissionAPIError>(e.APIError);
|
||||
Assert.Equal(Policies.CanModifyStoreSettings, permissionError.MissingPermission);
|
||||
|
||||
var client = await user.CreateClient(Policies.CanViewStoreSettings);
|
||||
await AssertAPIError("unsupported-in-v2", () => client.SendHttpRequest<object>($"api/v1/stores/{user.StoreId}/payment-methods/LightningNetwork"));
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -368,6 +372,27 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
)
|
||||
);
|
||||
var template = @"[
|
||||
{
|
||||
""description"": ""Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years."",
|
||||
""id"": ""green-tea"",
|
||||
""image"": ""~/img/pos-sample/green-tea.jpg"",
|
||||
""priceType"": ""Fixed"",
|
||||
""price"": ""1"",
|
||||
""title"": ""Green Tea"",
|
||||
""disabled"": false
|
||||
}
|
||||
]";
|
||||
await AssertValidationError(new[] { "Template" },
|
||||
async () => await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new PointOfSaleAppRequest
|
||||
{
|
||||
AppName = "good name",
|
||||
Template = template.Replace(@"""id"": ""green-tea"",", "")
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Test creating a POS app successfully
|
||||
var app = await client.CreatePointOfSaleApp(
|
||||
@ -376,7 +401,8 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
AppName = "test app from API",
|
||||
Currency = "JPY",
|
||||
Title = "test app title"
|
||||
Title = "test app title",
|
||||
Template = template
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app from API", app.AppName);
|
||||
@ -559,6 +585,27 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
)
|
||||
);
|
||||
var template = @"[
|
||||
{
|
||||
""description"": ""Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years."",
|
||||
""id"": ""green-tea"",
|
||||
""image"": ""~/img/pos-sample/green-tea.jpg"",
|
||||
""priceType"": ""Fixed"",
|
||||
""price"": ""1"",
|
||||
""title"": ""Green Tea"",
|
||||
""disabled"": false
|
||||
}
|
||||
]";
|
||||
await AssertValidationError(new[] { "PerksTemplate" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CrowdfundAppRequest
|
||||
{
|
||||
AppName = "good name",
|
||||
PerksTemplate = template.Replace(@"""id"": ""green-tea"",", "")
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Test creating a crowdfund app
|
||||
var app = await client.CreateCrowdfundApp(
|
||||
@ -566,7 +613,8 @@ namespace BTCPayServer.Tests
|
||||
new CrowdfundAppRequest
|
||||
{
|
||||
AppName = "test app from API",
|
||||
Title = "test app title"
|
||||
Title = "test app title",
|
||||
PerksTemplate = template
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app from API", app.AppName);
|
||||
@ -1104,7 +1152,7 @@ namespace BTCPayServer.Tests
|
||||
Description = "Test description",
|
||||
Amount = 12.3m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
void VerifyResult()
|
||||
@ -1135,7 +1183,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 2",
|
||||
Amount = 12.3m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
BOLT11Expiration = TimeSpan.FromDays(31.0)
|
||||
});
|
||||
Assert.Equal(TimeSpan.FromDays(31.0), test2.BOLT11Expiration);
|
||||
@ -1182,13 +1230,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
payouts = await unauthenticated.GetPayouts(pps[0].Id);
|
||||
var payout2 = Assert.Single(payouts);
|
||||
Assert.Equal(payout.Amount, payout2.Amount);
|
||||
Assert.Equal(payout.OriginalAmount, payout2.OriginalAmount);
|
||||
Assert.Equal(payout.Id, payout2.Id);
|
||||
Assert.Equal(destination, payout2.Destination);
|
||||
Assert.Equal(PayoutState.AwaitingApproval, payout.State);
|
||||
Assert.Equal("BTC-CHAIN", payout2.PayoutMethodId);
|
||||
Assert.Equal("BTC", payout2.CryptoCode);
|
||||
Assert.Null(payout.PaymentMethodAmount);
|
||||
Assert.Equal("BTC", payout2.PayoutCurrency);
|
||||
Assert.Null(payout.PayoutAmount);
|
||||
|
||||
TestLogs.LogInformation("Can't overdraft");
|
||||
|
||||
@ -1230,7 +1278,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 12.3m,
|
||||
StartsAt = start,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
Assert.Equal(start, inFuture.StartsAt);
|
||||
Assert.Null(inFuture.ExpiresAt);
|
||||
@ -1248,7 +1296,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 12.3m,
|
||||
ExpiresAt = expires,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
await this.AssertAPIError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest()
|
||||
{
|
||||
@ -1272,7 +1320,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test USD",
|
||||
Amount = 5000m,
|
||||
Currency = "USD",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
await this.AssertAPIError("lnurl-not-supported", async () => await unauthenticated.GetPullPaymentLNURL(pp.Id));
|
||||
@ -1297,8 +1345,8 @@ namespace BTCPayServer.Tests
|
||||
Revision = payout.Revision
|
||||
});
|
||||
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
|
||||
Assert.NotNull(payout.PaymentMethodAmount);
|
||||
Assert.Equal(1.0m, payout.PaymentMethodAmount); // 1 BTC == 5000 USD in tests
|
||||
Assert.NotNull(payout.PayoutAmount);
|
||||
Assert.Equal(1.0m, payout.PayoutAmount); // 1 BTC == 5000 USD in tests
|
||||
await this.AssertAPIError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
|
||||
{
|
||||
Revision = payout.Revision
|
||||
@ -1310,7 +1358,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 2",
|
||||
Amount = 12.303228134m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
|
||||
payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
|
||||
@ -1320,8 +1368,8 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
|
||||
// The payout should round the value of the payment down to the network of the payment method
|
||||
Assert.Equal(12.30322814m, payout.PaymentMethodAmount);
|
||||
Assert.Equal(12.303228134m, payout.Amount);
|
||||
Assert.Equal(12.30322814m, payout.PayoutAmount);
|
||||
Assert.Equal(12.303228134m, payout.OriginalAmount);
|
||||
|
||||
await client.MarkPayoutPaid(storeId, payout.Id);
|
||||
payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
|
||||
@ -1334,7 +1382,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 3",
|
||||
Amount = 12.303228134m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
@ -1409,7 +1457,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test SATS",
|
||||
Amount = 21000,
|
||||
Currency = "SATS",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
@ -1427,7 +1475,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
AutoApproveClaims = true
|
||||
});
|
||||
});
|
||||
@ -1447,7 +1495,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
AutoApproveClaims = true
|
||||
});
|
||||
|
||||
@ -1932,7 +1980,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("BTC-CHAIN", serverInfoData.SupportedPaymentMethods);
|
||||
Assert.Contains("BTC-LN", serverInfoData.SupportedPaymentMethods);
|
||||
Assert.NotNull(serverInfoData.SyncStatus);
|
||||
Assert.Single(serverInfoData.SyncStatus.Select(s => s.CryptoCode == "BTC"));
|
||||
Assert.Single(serverInfoData.SyncStatus.Select(s => s.PaymentMethodId == "BTC-CHAIN"));
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -2374,6 +2422,14 @@ namespace BTCPayServer.Tests
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
|
||||
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||
method = methods.First();
|
||||
Assert.Equal(JTokenType.Null, method.AdditionalData["accountDerivation"].Type);
|
||||
Assert.NotNull(method.AdditionalData["keyPath"]);
|
||||
|
||||
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id, includeSensitive: true);
|
||||
method = methods.First();
|
||||
Assert.Equal(JTokenType.String, method.AdditionalData["accountDerivation"].Type);
|
||||
var clientViewOnly = await user.CreateClient(Policies.CanViewInvoices);
|
||||
await AssertApiError(403, "missing-permission", () => clientViewOnly.GetInvoicePaymentMethods(user.StoreId, invoice.Id, includeSensitive: true));
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
@ -2678,7 +2734,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.EndsWith($"/i/{newInvoice.Id}", newInvoice.CheckoutLink);
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
|
||||
var model = (PaymentModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
|
||||
var model = (CheckoutModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
|
||||
Assert.Equal("it-IT", model.DefaultLang);
|
||||
Assert.Equal("http://toto.com/lol", model.MerchantRefLink);
|
||||
|
||||
@ -2925,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);
|
||||
@ -2991,7 +3048,7 @@ namespace BTCPayServer.Tests
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = "USD",
|
||||
Amount = 100,
|
||||
Amount = 0.1m,
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions
|
||||
{
|
||||
PaymentMethods = new[] { "BTC-LN" },
|
||||
@ -3721,7 +3778,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
|
||||
await tester.ExplorerNode.GenerateAsync(1);
|
||||
}, bevent => bevent.CryptoCode.Equals("BTC", StringComparison.Ordinal));
|
||||
}, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
|
||||
|
||||
Assert.Contains(
|
||||
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
|
||||
@ -4116,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);
|
||||
@ -4146,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,
|
||||
@ -4188,7 +4249,37 @@ namespace BTCPayServer.Tests
|
||||
PayoutMethodId = "BTC_LightningNetwork",
|
||||
Destination = customerInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
Assert.Equal(payout2.OriginalAmount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
|
||||
// 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)]
|
||||
@ -4232,7 +4323,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
var notapprovedPayoutWithPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
@ -4258,7 +4349,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Equal(3, payouts.Length);
|
||||
Assert.Empty(payouts.Where(data => data.State == PayoutState.AwaitingApproval));
|
||||
Assert.Empty(payouts.Where(data => data.PaymentMethodAmount is null));
|
||||
Assert.Empty(payouts.Where(data => data.PayoutAmount is null));
|
||||
|
||||
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
|
||||
|
||||
@ -4271,12 +4362,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
|
||||
|
||||
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PaymentMethods));
|
||||
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PayoutMethods));
|
||||
//still too poor to process any payouts
|
||||
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
|
||||
|
||||
|
||||
await adminClient.RemovePayoutProcessor(admin.StoreId, tpGen.Name, tpGen.PaymentMethods.First());
|
||||
await adminClient.RemovePayoutProcessor(admin.StoreId, tpGen.Name, tpGen.PayoutMethods.First());
|
||||
|
||||
Assert.Empty(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
|
13
BTCPayServer.Tests/OutputPathAttribute.cs
Normal file
13
BTCPayServer.Tests/OutputPathAttribute.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class OutputPathAttribute : Attribute
|
||||
{
|
||||
public OutputPathAttribute(string builtPath)
|
||||
{
|
||||
BuiltPath = builtPath;
|
||||
}
|
||||
public string BuiltPath { get; }
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
@ -90,6 +91,54 @@ fruit tea:
|
||||
Assert.Null( parsedDefault[4].AdditionalData);
|
||||
Assert.Null( parsedDefault[4].PaymentMethods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseAppTemplate()
|
||||
{
|
||||
var template = @"[
|
||||
{
|
||||
""description"": ""Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years."",
|
||||
""id"": ""green-tea"",
|
||||
""image"": ""~/img/pos-sample/green-tea.jpg"",
|
||||
""priceType"": ""Fixed"",
|
||||
""price"": ""1"",
|
||||
""title"": ""Green Tea"",
|
||||
""disabled"": false
|
||||
},
|
||||
{
|
||||
""description"": ""Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available."",
|
||||
""id"": ""black-tea"",
|
||||
""image"": ""~/img/pos-sample/black-tea.jpg"",
|
||||
""priceType"": ""Fixed"",
|
||||
""price"": ""1"",
|
||||
""title"": ""Black Tea"",
|
||||
""disabled"": false
|
||||
}
|
||||
]";
|
||||
|
||||
var items = AppService.Parse(template);
|
||||
Assert.Equal(2, items.Length);
|
||||
Assert.Equal("green-tea", items[0].Id);
|
||||
Assert.Equal("black-tea", items[1].Id);
|
||||
|
||||
// Fails gracefully for missing ID
|
||||
var missingId = template.Replace(@"""id"": ""green-tea"",", "");
|
||||
items = AppService.Parse(missingId);
|
||||
Assert.Single(items);
|
||||
Assert.Equal("black-tea", items[0].Id);
|
||||
|
||||
// Throws for missing ID
|
||||
Assert.Throws<ArgumentException>(() => AppService.Parse(missingId, true, true));
|
||||
|
||||
// Fails gracefully for duplicate IDs
|
||||
var duplicateId = template.Replace(@"""id"": ""green-tea"",", @"""id"": ""black-tea"",");
|
||||
items = AppService.Parse(duplicateId);
|
||||
Assert.Empty(items);
|
||||
|
||||
// Throws for duplicate IDs
|
||||
Assert.Throws<ArgumentException>(() => AppService.Parse(duplicateId, true, true));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
|
@ -45,7 +45,7 @@ namespace BTCPayServer.Tests
|
||||
var runInBrowser = config["RunSeleniumInBrowser"] == "true";
|
||||
// Reset this using `dotnet user-secrets remove RunSeleniumInBrowser`
|
||||
|
||||
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory());
|
||||
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : TestUtils.TestDirectory);
|
||||
|
||||
var options = new ChromeOptions();
|
||||
if (!runInBrowser)
|
||||
@ -132,11 +132,11 @@ retry:
|
||||
/// Because for some reason, the selenium container can't resolve the tests container domain name
|
||||
/// </summary>
|
||||
public Uri ServerUri;
|
||||
internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||
public IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||
{
|
||||
return FindAlertMessage(new[] { severity });
|
||||
}
|
||||
internal IWebElement FindAlertMessage(params StatusMessageModel.StatusSeverity[] severity)
|
||||
public IWebElement FindAlertMessage(params StatusMessageModel.StatusSeverity[] severity)
|
||||
{
|
||||
var className = string.Join(", ", severity.Select(statusSeverity => $".alert-{StatusMessageModel.ToString(statusSeverity)}"));
|
||||
IWebElement el;
|
||||
@ -182,13 +182,18 @@ retry:
|
||||
Driver.FindElement(By.Id("RegisterButton")).Click();
|
||||
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)
|
||||
|
@ -1238,6 +1238,7 @@ namespace BTCPayServer.Tests
|
||||
await s.StartAsync();
|
||||
var userId = s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet();
|
||||
(_, string appId) = s.CreateApp("PointOfSale");
|
||||
s.Driver.FindElement(By.Id("Title")).Clear();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Tea shop");
|
||||
@ -1249,10 +1250,20 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
|
||||
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
|
||||
Assert.Matches("\"categories\": \\[\r?\n\\s*\"Drinks\"\\s*\\]", template);
|
||||
|
||||
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.ScrollTo(By.Id("CodeTabButton"));
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
s.Driver.FindElement(By.Id("TemplateConfig")).Clear();
|
||||
s.Driver.FindElement(By.Id("TemplateConfig")).SendKeys(template.Replace(@"""id"": ""green-tea"",", ""));
|
||||
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Invalid template: Missing ID for item \"Green Tea\".", s.Driver.FindElement(By.CssSelector(".validation-summary-errors")).Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
@ -2476,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);
|
||||
@ -2566,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);
|
||||
@ -3253,6 +3275,7 @@ namespace BTCPayServer.Tests
|
||||
public async Task CanUseLNAddress()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.DeleteStore = false;
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
@ -3405,7 +3428,13 @@ namespace BTCPayServer.Tests
|
||||
var succ = JsonConvert.DeserializeObject<LNURLPayRequest.LNURLPayRequestCallbackResponse>(str);
|
||||
Assert.NotNull(succ.Pr);
|
||||
Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
|
||||
await s.Server.CustomerLightningD.Pay(succ.Pr);
|
||||
}
|
||||
|
||||
// Can we find our comment and address in the payment list?
|
||||
s.GoToInvoices();
|
||||
var source = s.Driver.PageSource;
|
||||
Assert.Contains(lnUsername, source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -3417,11 +3446,14 @@ namespace BTCPayServer.Tests
|
||||
var user = s.RegisterNewUser();
|
||||
s.GoToHome();
|
||||
s.GoToProfile(ManageNavPages.LoginCodes);
|
||||
var code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
s.ClickPagePrimary();
|
||||
Assert.NotEqual(code, s.Driver.FindElement(By.Id("logincode")).GetAttribute("value"));
|
||||
|
||||
code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
string code = null;
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
string prevCode = code;
|
||||
await s.Driver.Navigate().RefreshAsync();
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
Assert.NotEqual(prevCode, code);
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.Driver.SetAttribute("LoginCode", "value", "bad code");
|
||||
|
@ -146,19 +146,27 @@ namespace BTCPayServer.Tests
|
||||
public async Task ModifyPayment(Action<GeneralSettingsViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<UIStoresController>();
|
||||
var response = await storeController.GeneralSettings();
|
||||
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model;
|
||||
var response = await storeController.GeneralSettings(StoreId);
|
||||
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model!;
|
||||
modify(settings);
|
||||
await storeController.GeneralSettings(settings);
|
||||
}
|
||||
|
||||
public async Task ModifyGeneralSettings(Action<GeneralSettingsViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<UIStoresController>();
|
||||
var response = await storeController.GeneralSettings(StoreId);
|
||||
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model!;
|
||||
modify(settings);
|
||||
storeController.GeneralSettings(settings).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task ModifyOnchainPaymentSettings(Action<WalletSettingsViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<UIStoresController>();
|
||||
var response = await storeController.WalletSettings(StoreId, "BTC");
|
||||
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
|
||||
modify(walletSettings);
|
||||
storeController.UpdatePaymentSettings(walletSettings).GetAwaiter().GetResult();
|
||||
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
@ -601,9 +609,7 @@ retry:
|
||||
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
|
||||
var method = methods.First(m => m.PaymentMethodId == $"{cryptoCode}-LN");
|
||||
var bolt11 = method.Destination;
|
||||
TestLogs.LogInformation("PAYING");
|
||||
await parent.CustomerLightningD.Pay(bolt11);
|
||||
TestLogs.LogInformation("PAID");
|
||||
await WaitInvoicePaid(invoiceId);
|
||||
}
|
||||
|
||||
|
@ -727,5 +727,39 @@
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "payment",
|
||||
"input": {
|
||||
"output": null,
|
||||
"version": 1,
|
||||
"outpoint": null,
|
||||
"accounted": true,
|
||||
"cryptoCode": "XMR",
|
||||
"networkFee": 0.0000000019,
|
||||
"receivedTimeMs": 1705500405468,
|
||||
"cryptoPaymentData": "{\"Amount\":62700000000,\"Address\":\"85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t\",\"SubaddressIndex\":23,\"SubaccountIndex\":0,\"BlockHeight\":3063946,\"ConfirmationCount\":10,\"TransactionId\":\"cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06\",\"LockTime\":0}",
|
||||
"cryptoPaymentDataType": "MoneroLike"
|
||||
},
|
||||
"expected": {
|
||||
"divisibility": 12,
|
||||
"destination": "85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t",
|
||||
"details": {
|
||||
"blockHeight": 3063946,
|
||||
"confirmationCount": 10,
|
||||
"lockTime": 0,
|
||||
"subaccountIndex": 0,
|
||||
"subaddressIndex": 23,
|
||||
"transactionId": "cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06"
|
||||
},
|
||||
"version": 2
|
||||
},
|
||||
"expectedProperties": {
|
||||
"Amount": "0.0627",
|
||||
"PaymentMethodId": "XMR-CHAIN",
|
||||
"Currency": "XMR",
|
||||
"Status": "Settled",
|
||||
"Accounted": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -1,3 +1,5 @@
|
||||
Password => Cyphercode
|
||||
Email address => Cypher ID
|
||||
Welcome to {0} => Yo at {0}
|
||||
{
|
||||
"Password" : "Cyphercode",
|
||||
"Email address" : "Cypher ID",
|
||||
"Welcome to {0}" : "Yo at {0}"
|
||||
}
|
||||
|
@ -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,,
|
||||
|
|
@ -20,10 +20,9 @@ namespace BTCPayServer.Tests
|
||||
#else
|
||||
public const int TestTimeout = 90_000;
|
||||
#endif
|
||||
public static DirectoryInfo TryGetSolutionDirectoryInfo(string currentPath = null)
|
||||
public static DirectoryInfo TryGetSolutionDirectoryInfo()
|
||||
{
|
||||
var directory = new DirectoryInfo(
|
||||
currentPath ?? Directory.GetCurrentDirectory());
|
||||
var directory = new DirectoryInfo(TestDirectory);
|
||||
while (directory != null && !directory.GetFiles("*.sln").Any())
|
||||
{
|
||||
directory = directory.Parent;
|
||||
@ -31,10 +30,15 @@ namespace BTCPayServer.Tests
|
||||
return directory;
|
||||
}
|
||||
|
||||
static TestUtils()
|
||||
{
|
||||
TestDirectory = ((OutputPathAttribute)typeof(TestUtils).Assembly.GetCustomAttributes(typeof(OutputPathAttribute), true)[0]).BuiltPath;
|
||||
}
|
||||
public readonly static string TestDirectory;
|
||||
|
||||
public static string GetTestDataFullPath(string relativeFilePath)
|
||||
{
|
||||
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
|
||||
var directory = new DirectoryInfo(TestDirectory);
|
||||
while (directory != null && !directory.GetFiles("*.csproj").Any())
|
||||
{
|
||||
directory = directory.Parent;
|
||||
|
@ -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;
|
||||
|
@ -303,7 +303,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Set tolerance to 50%
|
||||
var stores = user.GetController<UIStoresController>();
|
||||
var response = await stores.GeneralSettings();
|
||||
var response = await stores.GeneralSettings(user.StoreId);
|
||||
var vm = Assert.IsType<GeneralSettingsViewModel>(Assert.IsType<ViewResult>(response).Model);
|
||||
Assert.Equal(0.0, vm.PaymentTolerance);
|
||||
vm.PaymentTolerance = 50.0;
|
||||
@ -385,7 +385,7 @@ namespace BTCPayServer.Tests
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
|
||||
await user.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
|
||||
await user.ModifyGeneralSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
|
||||
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
|
||||
{
|
||||
@ -445,7 +445,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
var storeController = user.GetController<UIStoresController>();
|
||||
var storeResponse = await storeController.GeneralSettings();
|
||||
var storeResponse = await storeController.GeneralSettings(user.StoreId);
|
||||
Assert.IsType<ViewResult>(storeResponse);
|
||||
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
|
||||
|
||||
@ -568,10 +568,10 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
await acc.GrantAccessAsync();
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
await acc.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice
|
||||
await acc.ModifyGeneralSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
|
||||
var invoice = await acc.BitPay.CreateInvoiceAsync(new Invoice
|
||||
{
|
||||
Price = 5.0m,
|
||||
Currency = "USD",
|
||||
@ -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)]
|
||||
@ -1463,7 +1463,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Currency = "BTC",
|
||||
Amount = 1.0m,
|
||||
PaymentMethods = [ "BTC-CHAIN" ]
|
||||
PayoutMethods = [ "BTC-CHAIN" ]
|
||||
});
|
||||
var controller = user.GetController<UIInvoiceController>();
|
||||
var invoice = await controller.CreateInvoiceCoreRaw(new()
|
||||
@ -1479,7 +1479,7 @@ namespace BTCPayServer.Tests
|
||||
var payout = Assert.Single(payouts);
|
||||
Assert.Equal("TOPUP", payout.PayoutMethodId);
|
||||
Assert.Equal(invoice.Id, payout.Destination);
|
||||
Assert.Equal(-0.5m, payout.Amount);
|
||||
Assert.Equal(-0.5m, payout.OriginalAmount);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1543,7 +1543,7 @@ namespace BTCPayServer.Tests
|
||||
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
|
||||
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
|
||||
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
|
||||
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), criteria.PaymentMethod);
|
||||
Assert.Equal(btcMethod.ToString(), criteria.PaymentMethod);
|
||||
criteria.Value = "5 USD";
|
||||
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
|
||||
@ -1587,14 +1587,14 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
var checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
|
||||
var checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<CheckoutModel>();
|
||||
Assert.Equal(lnMethod, checkout.PaymentMethodId);
|
||||
|
||||
// If we change store's default, it should change the checkout's default
|
||||
vm.DefaultPaymentMethod = btcMethod;
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
|
||||
.Result);
|
||||
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
|
||||
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<CheckoutModel>();
|
||||
Assert.Equal(btcMethod, checkout.PaymentMethodId);
|
||||
}
|
||||
|
||||
@ -1625,7 +1625,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// validate that invoice data model doesn't have lightning string initially
|
||||
var res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
|
||||
var paymentMethodFirst = Assert.IsType<PaymentModel>(
|
||||
var paymentMethodFirst = Assert.IsType<CheckoutModel>(
|
||||
Assert.IsType<ViewResult>(res).Model
|
||||
);
|
||||
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
|
||||
@ -1641,7 +1641,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// validate that QR code now has both onchain and offchain payment urls
|
||||
res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
|
||||
var paymentMethodUnified = Assert.IsType<PaymentModel>(
|
||||
var paymentMethodUnified = Assert.IsType<CheckoutModel>(
|
||||
Assert.IsType<ViewResult>(res).Model
|
||||
);
|
||||
Assert.StartsWith("bitcoin:bcrt", paymentMethodUnified.InvoiceBitcoinUrl);
|
||||
@ -1655,8 +1655,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Standard for all uppercase characters in QR codes is still not implemented in all wallets
|
||||
// But we're proceeding with BECH32 being uppercase
|
||||
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
|
||||
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
|
||||
Assert.Equal($"bitcoin:{paymentMethodUnified.Address}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
|
||||
Assert.Equal($"bitcoin:{paymentMethodUnified.Address.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
|
||||
|
||||
// Fallback lightning invoice should be uppercase inside the QR code, lowercase in payment URI
|
||||
var lightningFallback = paymentMethodUnified.InvoiceBitcoinUrl.Split(new[] { "&lightning=" }, StringSplitOptions.None)[1];
|
||||
@ -1844,6 +1844,8 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
await user2.RegisterDerivationSchemeAsync("BTC");
|
||||
var stores = user.GetController<UIStoresController>();
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
@ -2617,7 +2619,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
var controller = tester.PayTester.GetController<UIStoresController>(user.UserId, user.StoreId);
|
||||
var vm = await controller.GeneralSettings().AssertViewModelAsync<GeneralSettingsViewModel>();
|
||||
var vm = await controller.GeneralSettings(user.StoreId).AssertViewModelAsync<GeneralSettingsViewModel>();
|
||||
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/8f890691-87f9-4c65-80e5-3b7ffaa3551f-store.png", vm.LogoUrl);
|
||||
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/2a51c49a-9d54-4013-80a2-3f6e69d08523-store.css", vm.CssUrl);
|
||||
|
||||
@ -2907,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)
|
||||
@ -3244,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,29 +351,26 @@ retry:
|
||||
{
|
||||
defaultTranslatedKeys.Add(k);
|
||||
}
|
||||
|
||||
AddLocalizers(defaultTranslatedKeys, txt);
|
||||
}
|
||||
|
||||
// Go through all cshtml file, search for text-translate or ViewLocalizer usage
|
||||
using (var tester = CreateServerTester())
|
||||
using (var tester = CreateServerTester(newDb: true))
|
||||
{
|
||||
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);
|
||||
if (txt.Contains("ViewLocalizer"))
|
||||
{
|
||||
var matches = Regex.Matches(txt, "ViewLocalizer\\[\"(.*?)\"[\\],]");
|
||||
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);
|
||||
@ -379,15 +378,39 @@ retry:
|
||||
|
||||
}
|
||||
defaultTranslatedKeys = defaultTranslatedKeys.Select(d => d.Trim()).Distinct().OrderBy(o => o).ToList();
|
||||
JObject obj = new JObject();
|
||||
foreach (var v in defaultTranslatedKeys)
|
||||
{
|
||||
obj.Add(v, "");
|
||||
}
|
||||
|
||||
var path = Path.Combine(soldir.FullName, "BTCPayServer/Services/Translations.Default.cs");
|
||||
var defaultTranslation = File.ReadAllText(path);
|
||||
var startIdx = defaultTranslation.IndexOf("\"\"\"");
|
||||
var endIdx = defaultTranslation.LastIndexOf("\"\"\"");
|
||||
var content = defaultTranslation.Substring(0, startIdx + 3);
|
||||
content += "\n" + String.Join('\n', defaultTranslatedKeys) + "\n";
|
||||
content += "\n" + obj.ToString(Formatting.Indented) + "\n";
|
||||
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>();
|
||||
|
@ -12,7 +12,6 @@ services:
|
||||
args:
|
||||
CONFIGURATION_NAME: Release
|
||||
environment:
|
||||
TESTS_EXPERIMENTALV2_CONFIRM: "true"
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
@ -163,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:
|
||||
@ -191,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:
|
||||
@ -228,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"
|
||||
@ -263,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"
|
||||
|
@ -12,7 +12,6 @@ services:
|
||||
args:
|
||||
CONFIGURATION_NAME: Release
|
||||
environment:
|
||||
TESTS_EXPERIMENTALV2_CONFIRM: "true"
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=btcpayserver
|
||||
@ -149,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:
|
||||
@ -177,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:
|
||||
@ -214,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"
|
||||
@ -251,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" />
|
||||
|
@ -7,12 +7,13 @@
|
||||
<use href="@GetPathTo(Symbol)"></use>
|
||||
</svg>
|
||||
@code {
|
||||
public string GetPathTo(string symbol)
|
||||
[Parameter, EditorRequired]
|
||||
public string Symbol { get; set; }
|
||||
|
||||
private string GetPathTo(string symbol)
|
||||
{
|
||||
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
|
||||
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
|
||||
return $"{rootPath}{versioned}#{Symbol}";
|
||||
return $"{rootPath}{versioned}#{symbol}";
|
||||
}
|
||||
[Parameter]
|
||||
public string Symbol { get; set; }
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
42
BTCPayServer/Blazor/PosLoginCode.razor
Normal file
42
BTCPayServer/Blazor/PosLoginCode.razor
Normal file
@ -0,0 +1,42 @@
|
||||
@using Microsoft.AspNetCore.Http
|
||||
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
|
||||
@if (Users?.Any() is true)
|
||||
{
|
||||
<div @attributes="Attrs" class="@CssClass">
|
||||
<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="" text-translate="true">None, just open the URL</option>
|
||||
@foreach (var u in Users)
|
||||
{
|
||||
<option value="@u.Key">@u.Value</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (string.IsNullOrEmpty(_userId))
|
||||
{
|
||||
<QrCode Data="@PosUrl" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<UserLoginCode UserId="@_userId" RedirectUrl="@PosPath" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string PosPath { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string,string> Users { get; set; }
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private string _userId;
|
||||
private string PosUrl => Request.GetAbsoluteRoot() + PosPath;
|
||||
private HttpRequest Request => HttpContextAccessor.HttpContext?.Request;
|
||||
private string CssClass => $"form-group {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
29
BTCPayServer/Blazor/QrCode.razor
Normal file
29
BTCPayServer/Blazor/QrCode.razor
Normal file
@ -0,0 +1,29 @@
|
||||
@using QRCoder
|
||||
|
||||
@if (!string.IsNullOrEmpty(Data))
|
||||
{
|
||||
<img @attributes="Attrs" style="image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:@(Size)px;min-height:@(Size)px" src="data:image/png;base64,@(GetBase64(Data))" class="@CssClass" alt="@Data" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string Data { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Size { get; set; } = 256;
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private static readonly QRCodeGenerator QrGenerator = new();
|
||||
|
||||
private string GetBase64(string data)
|
||||
{
|
||||
var qrCodeData = QrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
|
||||
var qrCode = new PngByteQRCode(qrCodeData);
|
||||
var bytes = qrCode.GetGraphic(5, [0, 0, 0, 255], [0xf5, 0xf5, 0xf7, 255]);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private string CssClass => $"qr-code {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
102
BTCPayServer/Blazor/UserLoginCode.razor
Normal file
102
BTCPayServer/Blazor/UserLoginCode.razor
Normal file
@ -0,0 +1,102 @@
|
||||
@using System.Timers
|
||||
@using BTCPayServer.Data
|
||||
@using BTCPayServer.Fido2
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@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 IStringLocalizer StringLocalizer
|
||||
@implements IDisposable
|
||||
|
||||
@if (!string.IsNullOrEmpty(_data))
|
||||
{
|
||||
<div @attributes="Attrs" class="@CssClass" style="width:@(Size)px">
|
||||
<div class="qr-container mb-2">
|
||||
<QrCode Data="@_data" Size="Size"/>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string RedirectUrl { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Size { get; set; } = 256;
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private static readonly double Seconds = UserLoginCodeService.ExpirationTime.TotalSeconds;
|
||||
private double _seconds = Seconds;
|
||||
private string _data;
|
||||
private ApplicationUser _user;
|
||||
private Timer _timer;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
UserId ??= await GetUserId();
|
||||
if (!string.IsNullOrEmpty(UserId)) _user = await UserManager.FindByIdAsync(UserId);
|
||||
if (_user == null) return;
|
||||
|
||||
GenerateCodeAndStartTimer();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
|
||||
private void GenerateCodeAndStartTimer()
|
||||
{
|
||||
var loginCode = UserLoginCodeService.GetOrGenerate(_user.Id);
|
||||
_data = GetData(loginCode);
|
||||
_seconds = Seconds;
|
||||
_timer?.Dispose();
|
||||
_timer = new Timer(1000);
|
||||
_timer.Elapsed += CountDownTimer;
|
||||
_timer.Enabled = true;
|
||||
}
|
||||
|
||||
private void CountDownTimer(object source, ElapsedEventArgs e)
|
||||
{
|
||||
if (_seconds > 0)
|
||||
_seconds -= 1;
|
||||
else
|
||||
GenerateCodeAndStartTimer();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task<string> GetUserId()
|
||||
{
|
||||
var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
return state.User.Identity?.IsAuthenticated is true
|
||||
? UserManager.GetUserId(state.User)
|
||||
: null;
|
||||
}
|
||||
|
||||
private string GetData(string loginCode)
|
||||
{
|
||||
var req = HttpContextAccessor.HttpContext?.Request;
|
||||
if (req == null) return loginCode;
|
||||
return !string.IsNullOrEmpty(RedirectUrl)
|
||||
? LinkGenerator.LoginCodeLink(loginCode, RedirectUrl, req.Scheme, req.Host, req.PathBase)
|
||||
: $"{loginCode};{LinkGenerator.IndexLink(req.Scheme, req.Host, req.PathBase)};{_user.Email}";
|
||||
}
|
||||
|
||||
private double Percent => Math.Round(_seconds / Seconds * 100);
|
||||
private string CssClass => $"user-login-code d-inline-flex flex-column {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
@ -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>
|
||||
|
@ -1,6 +1,6 @@
|
||||
@using BTCPayServer.Payments
|
||||
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
|
||||
@inject Dictionary<PaymentMethodId, IPaymentModelExtension> Extensions
|
||||
@inject Dictionary<PaymentMethodId, ICheckoutModelExtension> Extensions
|
||||
|
||||
@{
|
||||
var state = Model.State.ToString();
|
||||
@ -10,7 +10,7 @@
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
@if (Model.IsArchived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
<span class="badge bg-warning" text-translate="true">archived</span>
|
||||
}
|
||||
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
|
||||
@if (canMark)
|
||||
@ -21,13 +21,13 @@
|
||||
<div class="dropdown-menu">
|
||||
@if (Model.State.CanMarkInvalid())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid">
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid" text-translate="true">
|
||||
Mark as invalid
|
||||
</button>
|
||||
}
|
||||
@if (Model.State.CanMarkComplete())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled">
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled" text-translate="true">
|
||||
Mark as settled
|
||||
</button>
|
||||
}
|
||||
@ -62,6 +62,6 @@
|
||||
}
|
||||
@if (Model.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">Refund</span>
|
||||
<span class="badge bg-warning" text-translate="true">Refund</span>
|
||||
}
|
||||
</div>
|
||||
|
@ -11,7 +11,7 @@
|
||||
walletId = Model.WalletObjectId.WalletId
|
||||
}): string.Empty;
|
||||
}
|
||||
<input id="@elementId" placeholder="Select labels" autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
|
||||
<input id="@elementId" placeholder=@StringLocalizer["Select labels"] autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
|
||||
class="only-for-js form-control label-manager ts-wrapper @(Model.DisplayInline ? "ts-inline" : "")"
|
||||
data-fetch-url="@fetchUrl"
|
||||
data-update-url="@updateUrl"
|
||||
|
@ -43,7 +43,7 @@
|
||||
<span text-translate="true">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
@if (ViewData.IsActivePage([StoreNavPages.General, StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Roles, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails, StoreNavPages.Forms]))
|
||||
@if (ViewData.IsPageActive([StoreNavPages.General, StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Roles, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails, StoreNavPages.Forms]))
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Model.Store.Id" text-translate="true">Rates</a>
|
||||
@ -104,16 +104,21 @@
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
@if (ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsActivePage([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsActivePage([StoreNavPages.OnchainSettings], categoryId))
|
||||
@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" />
|
||||
}
|
||||
@ -140,10 +145,10 @@
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
@if (ViewData.IsActivePage([StoreNavPages.Lightning, StoreNavPages.LightningSettings], $"{Model.Store.Id}-{scheme.CryptoCode}"))
|
||||
@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"/>
|
||||
}
|
||||
@ -290,7 +295,7 @@
|
||||
<span text-translate="true">Server Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
@if (ViewData.IsActiveCategory(typeof(ServerNavPages)) && !ViewData.IsActivePage([ServerNavPages.Plugins]))
|
||||
@if (ViewData.IsCategoryActive(typeof(ServerNavPages)) && !ViewData.IsPageActive([ServerNavPages.Plugins]))
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Users" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Users)" asp-action="ListUsers" text-translate="true">Users</a>
|
||||
@ -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"/>
|
||||
@ -379,7 +385,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (ViewData.IsActiveCategory(typeof(ManageNavPages)) || ViewData.IsActivePage([ManageNavPages.ChangePassword]))
|
||||
@if (ViewData.IsCategoryActive(typeof(ManageNavPages)) || ViewData.IsPageActive([ManageNavPages.ChangePassword]))
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="SectionNav-@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.ActivePageClass(ManageNavPages.ChangePassword)" asp-controller="UIManage" asp-action="ChangePassword" text-translate="true">Password</a>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ public class StoreRecentInvoices : ViewComponent
|
||||
Details = new InvoiceDetailsModel
|
||||
{
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(false)
|
||||
Payments = invoice.GetPayments(false)
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Client;
|
||||
using static BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel;
|
||||
|
||||
@ -80,7 +81,7 @@ public class StoreRecentTransactions : ViewComponent
|
||||
Balance = tx.BalanceChange.ShowMoney(network),
|
||||
Currency = vm.CryptoCode,
|
||||
IsConfirmed = tx.Confirmations != 0,
|
||||
Link = _transactionLinkProviders.GetTransactionLink(network.CryptoCode, tx.TransactionId.ToString()),
|
||||
Link = _transactionLinkProviders.GetTransactionLink(pmi, tx.TransactionId.ToString()),
|
||||
Timestamp = tx.SeenAt,
|
||||
Labels = labels
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -34,10 +34,8 @@ public class TruncateCenter : ViewComponent
|
||||
};
|
||||
if (!vm.IsVue)
|
||||
{
|
||||
vm.Start = vm.IsTruncated ? text[..padding] : text;
|
||||
vm.Start = vm.IsTruncated && !vm.Elastic ? $"{text[..padding]}…" : text;
|
||||
vm.End = vm.IsTruncated ? text[^padding..] : string.Empty;
|
||||
if (!vm.Elastic && vm.IsTruncated)
|
||||
vm.Start = $"{vm.Start}…";
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -9,6 +9,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -28,12 +29,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public class GreenfieldAppsController : ControllerBase
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public GreenfieldAppsController(
|
||||
AppService appService,
|
||||
UriResolver uriResolver,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
CurrencyNameTable currencies,
|
||||
@ -41,6 +44,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
)
|
||||
{
|
||||
_appService = appService;
|
||||
_uriResolver = uriResolver;
|
||||
_storeRepository = storeRepository;
|
||||
_currencies = currencies;
|
||||
_userManager = userManager;
|
||||
@ -72,12 +76,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Archived = request.Archived ?? false
|
||||
};
|
||||
|
||||
var settings = ToCrowdfundSettings(request, new CrowdfundSettings { Title = request.Title ?? request.AppName });
|
||||
var settings = ToCrowdfundSettings(request);
|
||||
appData.SetSettings(settings);
|
||||
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
return Ok(ToCrowdfundModel(appData));
|
||||
var model = await ToCrowdfundModel(appData);
|
||||
return Ok(model);
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
|
||||
@ -208,7 +212,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return AppNotFound();
|
||||
}
|
||||
|
||||
return Ok(ToCrowdfundModel(app));
|
||||
var model = await ToCrowdfundModel(app);
|
||||
return Ok(model);
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/apps/{appId}")]
|
||||
@ -255,7 +260,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
||||
}
|
||||
|
||||
private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request, CrowdfundSettings settings)
|
||||
private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request)
|
||||
{
|
||||
var parsedSounds = ValidateStringArray(request.Sounds);
|
||||
var parsedColors = ValidateStringArray(request.AnimationColors);
|
||||
@ -271,7 +276,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Description = request.Description?.Trim(),
|
||||
EndDate = request.EndDate?.UtcDateTime,
|
||||
TargetAmount = request.TargetAmount,
|
||||
MainImageUrl = request.MainImageUrl?.Trim(),
|
||||
MainImageUrl = request.MainImageUrl == null ? null : UnresolvedUri.Create(request.MainImageUrl),
|
||||
NotificationUrl = request.NotificationUrl?.Trim(),
|
||||
Tagline = request.Tagline?.Trim(),
|
||||
PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
|
||||
@ -402,16 +407,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
try
|
||||
{
|
||||
// Just checking if we can serialize
|
||||
AppService.SerializeTemplate(AppService.Parse(request.Template));
|
||||
AppService.SerializeTemplate(AppService.Parse(request.Template, true, true));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Template), "Invalid template");
|
||||
ModelState.AddModelError(nameof(request.Template), ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CrowdfundAppData ToCrowdfundModel(AppData appData)
|
||||
private async Task<CrowdfundAppData> ToCrowdfundModel(AppData appData)
|
||||
{
|
||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||
Enum.TryParse<CrowdfundResetEvery>(settings.ResetEvery.ToString(), true, out var resetEvery);
|
||||
@ -432,7 +437,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Description = settings.Description,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
Tagline = settings.Tagline,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
@ -486,11 +491,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
try
|
||||
{
|
||||
// Just checking if we can serialize
|
||||
AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate));
|
||||
AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate, true, true));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PerksTemplate), "Invalid template");
|
||||
ModelState.AddModelError(nameof(request.PerksTemplate), $"Invalid template: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -14,6 +15,7 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payouts;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -25,6 +27,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
@ -48,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; }
|
||||
@ -62,6 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions,
|
||||
PayoutMethodHandlerDictionary payoutHandlers,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
DefaultRulesCollection defaultRules)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
@ -76,6 +81,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_paymentLinkExtensions = paymentLinkExtensions;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_handlers = handlers;
|
||||
_networkProvider = networkProvider;
|
||||
_defaultRules = defaultRules;
|
||||
LanguageService = languageService;
|
||||
}
|
||||
@ -96,11 +102,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[FromQuery] int? take = null
|
||||
)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
var store = HttpContext.GetStoreData()!;
|
||||
if (startDate is DateTimeOffset s &&
|
||||
endDate is DateTimeOffset e &&
|
||||
s > e)
|
||||
@ -133,17 +135,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> GetInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
@ -153,16 +147,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> ArchiveInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
|
||||
return Ok();
|
||||
}
|
||||
@ -172,19 +159,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata);
|
||||
if (result != null)
|
||||
{
|
||||
return Ok(ToModel(result));
|
||||
}
|
||||
|
||||
return InvoiceNotFound();
|
||||
if (!BelongsToThisStore(result))
|
||||
return InvoiceNotFound();
|
||||
return Ok(ToModel(result));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateInvoice,
|
||||
@ -192,12 +170,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices")]
|
||||
public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
|
||||
var store = HttpContext.GetStoreData()!;
|
||||
if (request.Amount < 0.0m)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
|
||||
@ -271,17 +244,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> MarkInvoiceStatus(string storeId, string invoiceId,
|
||||
MarkInvoiceStatusRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
|
||||
{
|
||||
@ -300,17 +265,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")]
|
||||
public async Task<IActionResult> UnarchiveInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (!invoice.Archived)
|
||||
{
|
||||
@ -328,21 +285,23 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
|
||||
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true)
|
||||
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true, bool includeSensitive = false)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments));
|
||||
if (includeSensitive && !await _authorizationService.CanModifyStore(User))
|
||||
return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings);
|
||||
|
||||
return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments, includeSensitive));
|
||||
}
|
||||
|
||||
bool BelongsToThisStore([NotNullWhen(true)] InvoiceEntity invoice) => BelongsToThisStore(invoice, out _);
|
||||
private bool BelongsToThisStore([NotNullWhen(true)] InvoiceEntity invoice, [MaybeNullWhen(false)] out Data.StoreData store)
|
||||
{
|
||||
store = this.HttpContext.GetStoreData();
|
||||
return invoice?.StoreId is not null && store.Id == invoice.StoreId;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
@ -350,17 +309,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate")]
|
||||
public async Task<IActionResult> ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
|
||||
{
|
||||
@ -381,28 +332,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice, out var store))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
{
|
||||
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
|
||||
}
|
||||
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);
|
||||
@ -446,7 +387,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Name = request.Name ?? $"Refund {invoice.Id}",
|
||||
Description = request.Description,
|
||||
StoreId = storeId,
|
||||
PayoutMethodIds = new[] { payoutMethodId },
|
||||
PayoutMethods = new[] { payoutMethodId },
|
||||
};
|
||||
|
||||
if (request.RefundVariant != RefundVariant.Custom)
|
||||
@ -588,12 +529,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");
|
||||
}
|
||||
private IActionResult StoreNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
}
|
||||
|
||||
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly)
|
||||
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly, bool includeSensitive)
|
||||
{
|
||||
return entity.GetPaymentPrompts().Select(
|
||||
prompt =>
|
||||
@ -606,7 +543,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
var details = prompt.Details;
|
||||
if (handler is not null && prompt.Activated)
|
||||
details = JToken.FromObject(handler.ParsePaymentPromptDetails(details), handler.Serializer.ForAPI());
|
||||
{
|
||||
var detailsObj = handler.ParsePaymentPromptDetails(details);
|
||||
if (!includeSensitive)
|
||||
handler.StripDetailsForNonOwner(detailsObj);
|
||||
details = JToken.FromObject(detailsObj, handler.Serializer.ForAPI());
|
||||
}
|
||||
return new InvoicePaymentMethodDataModel
|
||||
{
|
||||
Activated = prompt.Activated,
|
||||
@ -621,7 +563,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
PaymentMethodFee = accounting?.PaymentMethodFee ?? 0m,
|
||||
PaymentLink = (prompt.Activated ? paymentLinkExtension?.GetPaymentLink(prompt, Url) : null) ?? string.Empty,
|
||||
Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList(),
|
||||
AdditionalData = prompt.Details
|
||||
AdditionalData = details
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public class GreenfieldObsoleteController : ControllerBase
|
||||
{
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURL")]
|
||||
public IActionResult Obsolete1(string storeId)
|
||||
{
|
||||
return Obsolete();
|
||||
}
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
|
||||
public IActionResult Obsolete2(string storeId, string cryptoCode)
|
||||
{
|
||||
return Obsolete();
|
||||
}
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork")]
|
||||
public IActionResult Obsolete3(string storeId)
|
||||
{
|
||||
return Obsolete();
|
||||
}
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
public IActionResult Obsolete4(string storeId, string cryptoCode)
|
||||
{
|
||||
return Obsolete();
|
||||
}
|
||||
private IActionResult Obsolete()
|
||||
{
|
||||
return this.CreateAPIError(410, "unsupported-in-v2", "This route isn't supported by BTCPay Server 2.0 and newer. Please update your integration.");
|
||||
}
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Name = factory.Processor,
|
||||
FriendlyName = factory.FriendlyName,
|
||||
PaymentMethods = factory.GetSupportedPayoutMethods().Select(id => id.ToString())
|
||||
PayoutMethods = factory.GetSupportedPayoutMethods().Select(id => id.ToString())
|
||||
.ToArray()
|
||||
}));
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.BOLT11Expiration), $"The BOLT11 expiration should be positive");
|
||||
}
|
||||
PayoutMethodId?[]? payoutMethods = null;
|
||||
if (request.PaymentMethods is { } payoutMethodsStr)
|
||||
if (request.PayoutMethods is { } payoutMethodsStr)
|
||||
{
|
||||
payoutMethods = payoutMethodsStr.Select(s =>
|
||||
{
|
||||
@ -144,13 +144,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
if (!supported.Contains(payoutMethods[i]))
|
||||
{
|
||||
request.AddModelError(paymentRequest => paymentRequest.PaymentMethods[i], "Invalid or unsupported payment method", this);
|
||||
request.AddModelError(paymentRequest => paymentRequest.PayoutMethods[i], "Invalid or unsupported payment method", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PaymentMethods), "This field is required");
|
||||
ModelState.AddModelError(nameof(request.PayoutMethods), "This field is required");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
@ -364,16 +364,17 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Id = p.Id,
|
||||
PullPaymentId = p.PullPaymentDataId,
|
||||
Date = p.Date,
|
||||
Amount = p.OriginalAmount,
|
||||
PaymentMethodAmount = p.Amount,
|
||||
OriginalCurrency = p.OriginalCurrency,
|
||||
OriginalAmount = p.OriginalAmount,
|
||||
PayoutCurrency = p.Currency,
|
||||
PayoutAmount = p.Amount,
|
||||
Revision = blob.Revision,
|
||||
State = p.State,
|
||||
PayoutMethodId = p.PayoutMethodId,
|
||||
PaymentProof = p.GetProofBlobJson(),
|
||||
Destination = blob.Destination,
|
||||
Metadata = blob.Metadata?? new JObject(),
|
||||
};
|
||||
model.Destination = blob.Destination;
|
||||
model.PayoutMethodId = p.PayoutMethodId;
|
||||
model.CryptoCode = p.Currency;
|
||||
model.PaymentProof = p.GetProofBlobJson();
|
||||
return model;
|
||||
}
|
||||
|
||||
@ -406,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")]
|
||||
@ -455,6 +462,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
|
||||
PullPaymentBlob? ppBlob = null;
|
||||
string? ppCurrency = null;
|
||||
if (request?.PullPaymentId is not null)
|
||||
{
|
||||
|
||||
@ -463,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)
|
||||
@ -471,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)
|
||||
|
@ -28,7 +28,7 @@ public class GreenfieldServerRolesController : ControllerBase
|
||||
[HttpGet("~/api/v1/server/roles")]
|
||||
public async Task<IActionResult> GetServerRoles()
|
||||
{
|
||||
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false, false)));
|
||||
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false)));
|
||||
}
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
{
|
||||
|
@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
PayoutMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
});
|
||||
|
||||
return Ok(configured.Select(ToModel).ToArray());
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
@ -88,7 +86,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = new[] { pmi }
|
||||
PayoutMethods = new[] { pmi }
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
activeProcessor ??= new PayoutProcessorData();
|
||||
|
@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
PayoutMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
});
|
||||
|
||||
return Ok(configured.Select(ToModel).ToArray());
|
||||
@ -94,7 +94,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = new[] { payoutMethodId }
|
||||
PayoutMethods = new[] { payoutMethodId }
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
activeProcessor ??= new PayoutProcessorData();
|
||||
|
@ -334,7 +334,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
Labels = info?.LegacyLabels ?? new Dictionary<string, LabelData>(),
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
Link = _transactionLinkProviders.GetTransactionLink(network.CryptoCode, coin.OutPoint.ToString()),
|
||||
Link = _transactionLinkProviders.GetTransactionLink(pmi, coin.OutPoint.ToString()),
|
||||
Timestamp = coin.Timestamp,
|
||||
KeyPath = coin.KeyPath,
|
||||
Confirmations = coin.Confirmations,
|
||||
@ -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);
|
||||
|
@ -145,9 +145,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (includeConfig is true)
|
||||
{
|
||||
var canModifyStore = (await _authorizationService.AuthorizeAsync(User, null,
|
||||
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;
|
||||
if (!canModifyStore)
|
||||
if (!await _authorizationService.CanModifyStore(User))
|
||||
return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings);
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Name = datas.Key,
|
||||
FriendlyName = _factories.FirstOrDefault(factory => factory.Processor == datas.Key)?.FriendlyName,
|
||||
PaymentMethods = datas.Select(data => data.PayoutMethodId).ToArray()
|
||||
PayoutMethods = datas.Select(data => data.PayoutMethodId).ToArray()
|
||||
});
|
||||
return Ok(configured);
|
||||
|
||||
@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { processor },
|
||||
PayoutMethodIds = new[] { PayoutMethodId.Parse(paymentMethod) }
|
||||
PayoutMethods = new[] { PayoutMethodId.Parse(paymentMethod) }
|
||||
})).FirstOrDefault();
|
||||
if (matched is null)
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var store = HttpContext.GetStoreData();
|
||||
return store == null
|
||||
? StoreNotFound()
|
||||
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false, false)));
|
||||
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false)));
|
||||
}
|
||||
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
|
@ -27,17 +27,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public class GreenfieldStoresController : ControllerBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
|
||||
public GreenfieldStoresController(
|
||||
StoreRepository storeRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IFileService fileService,
|
||||
UriResolver uriResolver)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_userManager = userManager;
|
||||
_fileService = fileService;
|
||||
_uriResolver = uriResolver;
|
||||
@ -335,7 +338,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[index].CurrencyCode, "CurrencyCode is required", this);
|
||||
}
|
||||
else if (CurrencyNameTable.Instance.GetCurrencyData(pmc.CurrencyCode, false) is null)
|
||||
else if (_currencyNameTable.GetCurrencyData(pmc.CurrencyCode, false) is null)
|
||||
{
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[index].CurrencyCode, "CurrencyCode is invalid", this);
|
||||
}
|
||||
|
@ -831,10 +831,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
public override async Task<InvoicePaymentMethodDataModel[]> GetInvoicePaymentMethods(string storeId,
|
||||
string invoiceId, CancellationToken token = default)
|
||||
string invoiceId,
|
||||
bool onlyAccountedPayments = true, bool includeSensitive = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<InvoicePaymentMethodDataModel[]>(
|
||||
await GetController<GreenfieldInvoiceController>().GetInvoicePaymentMethods(storeId, invoiceId));
|
||||
await GetController<GreenfieldInvoiceController>().GetInvoicePaymentMethods(storeId, invoiceId, onlyAccountedPayments, includeSensitive));
|
||||
}
|
||||
|
||||
public override async Task ArchiveInvoice(string storeId, string invoiceId, CancellationToken token = default)
|
||||
|
@ -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]
|
||||
@ -123,18 +127,33 @@ namespace BTCPayServer.Controllers
|
||||
return View(nameof(Login), new LoginViewModel { Email = email });
|
||||
}
|
||||
|
||||
// GET is for signin via the POS backend
|
||||
[HttpGet("/login/code")]
|
||||
[AllowAnonymous]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> LoginUsingCode(string loginCode, string returnUrl = null)
|
||||
{
|
||||
return await LoginCodeResult(loginCode, returnUrl);
|
||||
}
|
||||
|
||||
[HttpPost("/login/code")]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> LoginWithCode(string loginCode, string returnUrl = null)
|
||||
{
|
||||
return await LoginCodeResult(loginCode, returnUrl);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> LoginCodeResult(string loginCode, string returnUrl)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(loginCode))
|
||||
{
|
||||
var userId = _userLoginCodeService.Verify(loginCode);
|
||||
var code = loginCode.Split(';').First();
|
||||
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);
|
||||
}
|
||||
|
||||
@ -172,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
|
||||
@ -296,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))
|
||||
{
|
||||
@ -614,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)
|
||||
@ -689,15 +708,16 @@ 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 });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@ -795,8 +815,11 @@ 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));
|
||||
}
|
||||
|
||||
@ -822,16 +845,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
|
||||
var requiresSetPassword = !await _userManager.HasPasswordAsync(user);
|
||||
|
||||
_eventAggregator.Publish(new UserInviteAcceptedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
|
||||
// unset used token
|
||||
await _userManager.UnsetInvitationTokenAsync<ApplicationUser>(user.Id);
|
||||
|
||||
if (requiresEmailConfirmation)
|
||||
{
|
||||
return await RedirectToConfirmEmail(user);
|
||||
@ -841,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);
|
||||
}
|
||||
@ -850,12 +863,25 @@ 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);
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
}
|
||||
|
||||
|
||||
private async Task FinalizeInvitationIfApplicable(ApplicationUser user)
|
||||
{
|
||||
if (!_userManager.HasInvitationToken<ApplicationUser>(user)) return;
|
||||
_eventAggregator.Publish(new UserInviteAcceptedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
// unset used token
|
||||
await _userManager.UnsetInvitationTokenAsync<ApplicationUser>(user.Id);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToConfirmEmail(ApplicationUser user)
|
||||
{
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
@ -910,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;
|
||||
|
@ -9,12 +9,14 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -24,25 +26,34 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public UIAppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepository,
|
||||
IFileService fileService,
|
||||
AppService appService,
|
||||
IStringLocalizer stringLocalizer,
|
||||
IHtmlHelper html)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_handlers = handlers;
|
||||
_networkProvider = networkProvider;
|
||||
_storeRepository = storeRepository;
|
||||
_fileService = fileService;
|
||||
_appService = appService;
|
||||
Html = html;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly AppService _appService;
|
||||
|
||||
public string CreatedAppId { get; set; }
|
||||
public IHtmlHelper Html { get; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public class AppUpdated
|
||||
{
|
||||
@ -133,11 +144,25 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (!store.AnyPaymentMethodAvailable(_handlers))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Html = $"To create a {vm.AppType} app, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, storeId })}' class='alert-link'>set up a wallet</a> first",
|
||||
AllowDismiss = false
|
||||
});
|
||||
return View(vm);
|
||||
}
|
||||
vm.StoreId = store.Id;
|
||||
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)
|
||||
@ -156,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);
|
||||
@ -171,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)]
|
||||
@ -183,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 });
|
||||
}
|
||||
@ -206,12 +231,14 @@ namespace BTCPayServer.Controllers
|
||||
if (await _appService.SetArchived(app, archived))
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = archived
|
||||
? "The app has been archived and will no longer appear in the apps list by default."
|
||||
: "The app has been unarchived and will appear in the apps list by default again.";
|
||||
? StringLocalizer["The app has been archived and will no longer appear in the apps list by default."].Value
|
||||
: StringLocalizer["The app has been unarchived and will appear in the apps list by default again."].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Failed to {(archived ? "archive" : "unarchive")} the app.";
|
||||
TempData[WellKnownTempData.ErrorMessage] = archived
|
||||
? StringLocalizer["Failed to archive the app."].Value
|
||||
: StringLocalizer["Failed to unarchive the app."].Value;
|
||||
}
|
||||
|
||||
var url = await type.ConfigureLink(app);
|
||||
|
@ -65,6 +65,6 @@ public class UIBoltcardController : Controller
|
||||
if (!cardKey.CheckSunMac(c, piccData))
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", cancellationToken);
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", true, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ namespace BTCPayServer.Controllers
|
||||
var newDeliveryId = await WebhookNotificationManager.Redeliver(deliveryId);
|
||||
if (newDeliveryId is null)
|
||||
return NotFound();
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery";
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Successfully planned a redelivery"].Value;
|
||||
return RedirectToAction(nameof(Invoice),
|
||||
new
|
||||
{
|
||||
@ -128,6 +128,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreLink = Url.Action(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId = store.Id }),
|
||||
PaymentRequestLink = Url.Action(nameof(UIPaymentRequestController.ViewPaymentRequest), "UIPaymentRequest", new { payReqId = invoice.Metadata.PaymentRequestId }),
|
||||
Id = invoice.Id,
|
||||
Entity = invoice,
|
||||
State = invoiceState,
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
@ -293,9 +294,9 @@ namespace BTCPayServer.Controllers
|
||||
var payoutMethodIds = _payoutHandlers.GetSupportedPayoutMethods(this.GetCurrentStore());
|
||||
if (!payoutMethodIds.Any())
|
||||
{
|
||||
var vm = new RefundModel { Title = "No matching payment method" };
|
||||
var vm = new RefundModel { Title = StringLocalizer["No matching payment method"] };
|
||||
ModelState.AddModelError(nameof(vm.AvailablePaymentMethods),
|
||||
"There are no payment methods available to provide refunds with for this invoice.");
|
||||
StringLocalizer["There are no payment methods available to provide refunds with for this invoice."]);
|
||||
return View("_RefundModal", vm);
|
||||
}
|
||||
|
||||
@ -305,7 +306,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var refund = new RefundModel
|
||||
{
|
||||
Title = "Payment method",
|
||||
Title = StringLocalizer["Payment method"],
|
||||
AvailablePaymentMethods =
|
||||
new SelectList(payoutMethodIds.Select(id => new SelectListItem(id.ToString(), id.ToString())),
|
||||
"Value", "Text"),
|
||||
@ -343,7 +344,7 @@ namespace BTCPayServer.Controllers
|
||||
var pmis = _payoutHandlers.GetSupportedPayoutMethods(store);
|
||||
if (!pmis.Contains(pmi))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), $"Invalid payout method");
|
||||
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), StringLocalizer["Invalid payout method"]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
@ -352,7 +353,7 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethod = paymentMethodId is null ? null : invoice.GetPaymentPrompt(paymentMethodId);
|
||||
if (paymentMethod?.Currency is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), $"Invalid payout method");
|
||||
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), StringLocalizer["Invalid payout method"]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
@ -376,7 +377,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
case RefundSteps.SelectPaymentMethod:
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
model.Title = "How much to refund?";
|
||||
model.Title = StringLocalizer["How much to refund?"];
|
||||
|
||||
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
|
||||
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethod.Divisibility);
|
||||
@ -389,7 +390,7 @@ namespace BTCPayServer.Controllers
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
StringLocalizer["Impossible to fetch rate: {0}", rateResult.EvaluatedRule]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
@ -412,8 +413,8 @@ namespace BTCPayServer.Controllers
|
||||
case RefundSteps.SelectRate:
|
||||
createPullPayment = new CreatePullPayment
|
||||
{
|
||||
Name = $"Refund {invoice.Id}",
|
||||
PayoutMethodIds = new[] { pmi },
|
||||
Name = StringLocalizer["Refund {0}", invoice.Id],
|
||||
PayoutMethods = new[] { pmi },
|
||||
StoreId = invoice.StoreId,
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
|
||||
};
|
||||
@ -422,7 +423,7 @@ namespace BTCPayServer.Controllers
|
||||
.Succeeded;
|
||||
if (model.SubtractPercentage is < 0 or > 100)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
|
||||
ModelState.AddModelError(nameof(model.SubtractPercentage), StringLocalizer["Percentage must be a numeric value between 0 and 100"]);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -456,11 +457,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!isPaidOver)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invoice is not overpaid");
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), StringLocalizer["Invoice is not overpaid"]);
|
||||
}
|
||||
if (overpaidAmount == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Overpaid amount cannot be calculated");
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), StringLocalizer["Overpaid amount cannot be calculated"]);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -473,17 +474,17 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
|
||||
case "Custom":
|
||||
model.Title = "How much to refund?";
|
||||
model.Title = StringLocalizer["How much to refund?"];
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
|
||||
if (model.CustomAmount <= 0)
|
||||
{
|
||||
model.AddModelError(refundModel => refundModel.CustomAmount, "Amount must be greater than 0", this);
|
||||
model.AddModelError(refundModel => refundModel.CustomAmount, StringLocalizer["Amount must be greater than 0"], this);
|
||||
}
|
||||
if (string.IsNullOrEmpty(model.CustomCurrency) ||
|
||||
_CurrencyNameTable.GetCurrencyData(model.CustomCurrency, false) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CustomCurrency), "Invalid currency");
|
||||
ModelState.AddModelError(nameof(model.CustomCurrency), StringLocalizer["Invalid currency"]);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -499,7 +500,7 @@ namespace BTCPayServer.Controllers
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
StringLocalizer["Impossible to fetch rate: {0}", rateResult.EvaluatedRule]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
@ -509,7 +510,7 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
|
||||
default:
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Please select an option before proceeding");
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), StringLocalizer["Please select an option before proceeding"]);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
break;
|
||||
@ -554,6 +555,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(false),
|
||||
Entity = invoice,
|
||||
CryptoPayments = invoice.GetPaymentPrompts().Select(
|
||||
data =>
|
||||
{
|
||||
@ -606,10 +608,12 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
await _InvoiceRepository.ToggleInvoiceArchival(invoiceId, !invoice.Archived);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = invoice.Archived ? "The invoice has been unarchived and will appear in the invoice list by default again." : "The invoice has been archived and will no longer appear in the invoice list by default."
|
||||
Message = invoice.Archived
|
||||
? StringLocalizer["The invoice has been unarchived and will appear in the invoice list by default again."].Value
|
||||
: StringLocalizer["The invoice has been archived and will no longer appear in the invoice list by default."].Value
|
||||
});
|
||||
return RedirectToAction(nameof(invoice), new { invoiceId });
|
||||
}
|
||||
@ -624,28 +628,32 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListInvoices), new { storeId });
|
||||
}
|
||||
if (selectedItems.Length == 0)
|
||||
return NotSupported("No invoice has been selected");
|
||||
return NotSupported(StringLocalizer["No invoice has been selected"]);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "archive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
|
||||
TempData[WellKnownTempData.SuccessMessage] = selectedItems.Length == 1
|
||||
? StringLocalizer["{0} invoice archived.", selectedItems.Length].Value
|
||||
: StringLocalizer["{0} invoices archived.", selectedItems.Length].Value;
|
||||
break;
|
||||
|
||||
case "unarchive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems, false);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
|
||||
TempData[WellKnownTempData.SuccessMessage] = selectedItems.Length == 1
|
||||
? StringLocalizer["{0} invoice unarchived.", selectedItems.Length].Value
|
||||
: StringLocalizer["{0} invoices unarchived.", selectedItems.Length].Value;
|
||||
break;
|
||||
case "cpfp" when storeId is not null:
|
||||
var network = _NetworkProvider.DefaultNetwork;
|
||||
var explorer = _ExplorerClients.GetExplorerClient(network);
|
||||
if (explorer is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
return NotSupported(StringLocalizer["This feature is only available to BTC wallets"]);
|
||||
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
|
||||
return Forbid();
|
||||
|
||||
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_handlers, network.CryptoCode))?.AccountDerivation;
|
||||
var derivationScheme = GetCurrentStore().GetDerivationSchemeSettings(_handlers, network.CryptoCode)?.AccountDerivation;
|
||||
if (derivationScheme is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
|
||||
@ -655,7 +663,7 @@ namespace BTCPayServer.Controllers
|
||||
var parameters = new MultiValueDictionary<string, string>();
|
||||
foreach (var utxo in bumpableUTXOs)
|
||||
{
|
||||
parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
|
||||
parameters.Add("outpoints[]", utxo.Outpoint.ToString());
|
||||
}
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
@ -692,10 +700,11 @@ namespace BTCPayServer.Controllers
|
||||
if (invoiceId is null)
|
||||
return NotFound();
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
var model = await GetCheckoutModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
if (model == null)
|
||||
{
|
||||
// see if the invoice actually exists and is in a state for which we do not display the checkout
|
||||
// TODO: Can happen if the invoice has lazy activation which failed for all payment methods. We should display error instead...
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
var store = invoice != null ? await _StoreRepository.GetStoreByInvoiceId(invoice.Id) : null;
|
||||
var receipt = invoice != null && store != null ? InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, invoice.ReceiptOptions) : null;
|
||||
@ -711,23 +720,9 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("invoice-noscript")]
|
||||
public async Task<IActionResult> CheckoutNoScript(string? invoiceId, string? id = null, string? paymentMethodId = null, [FromQuery] string? lang = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
//
|
||||
if (invoiceId is null)
|
||||
return NotFound();
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId is null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<PaymentModel?> GetInvoiceModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang)
|
||||
private async Task<CheckoutModel?> GetCheckoutModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang, HashSet<PaymentMethodId>? excludedPaymentMethodIds = null)
|
||||
{
|
||||
var originalPaymentMethodId = paymentMethodId;
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
@ -735,11 +730,13 @@ namespace BTCPayServer.Controllers
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
if (store == null)
|
||||
return null;
|
||||
|
||||
excludedPaymentMethodIds ??= new HashSet<PaymentMethodId>();
|
||||
bool isDefaultPaymentId = false;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
var displayedPaymentMethods = invoice.GetPaymentPrompts().Select(p => p.PaymentMethodId).ToList();
|
||||
var displayedPaymentMethods = invoice.GetPaymentPrompts()
|
||||
.Where(p => !excludedPaymentMethodIds.Contains(p.PaymentMethodId))
|
||||
.Select(p => p.PaymentMethodId).ToHashSet();
|
||||
|
||||
|
||||
var btcId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
|
||||
@ -767,34 +764,7 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethodId = null;
|
||||
if (paymentMethodId is null)
|
||||
{
|
||||
PaymentMethodId? invoicePaymentId = invoice.DefaultPaymentMethod;
|
||||
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
|
||||
if (invoicePaymentId is not null)
|
||||
{
|
||||
if (displayedPaymentMethods.Contains(invoicePaymentId))
|
||||
paymentMethodId = invoicePaymentId;
|
||||
}
|
||||
if (paymentMethodId is null && storePaymentId is not null)
|
||||
{
|
||||
if (displayedPaymentMethods.Contains(storePaymentId))
|
||||
paymentMethodId = storePaymentId;
|
||||
}
|
||||
if (paymentMethodId is null && invoicePaymentId is not null)
|
||||
{
|
||||
paymentMethodId = invoicePaymentId.FindNearest(displayedPaymentMethods);
|
||||
}
|
||||
if (paymentMethodId is null && storePaymentId is not null)
|
||||
{
|
||||
paymentMethodId = storePaymentId.FindNearest(displayedPaymentMethods);
|
||||
}
|
||||
if (paymentMethodId is null)
|
||||
{
|
||||
var defaultBTC = PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode);
|
||||
var defaultLNURLPay = PaymentTypes.LNURL.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode);
|
||||
paymentMethodId = displayedPaymentMethods.FirstOrDefault(e => e == defaultBTC) ??
|
||||
displayedPaymentMethods.FirstOrDefault(e => e == defaultLNURLPay) ??
|
||||
displayedPaymentMethods.FirstOrDefault();
|
||||
}
|
||||
paymentMethodId = invoice.GetDefaultPaymentMethodId(store, _NetworkProvider, displayedPaymentMethods);
|
||||
isDefaultPaymentId = true;
|
||||
}
|
||||
if (paymentMethodId is null)
|
||||
@ -835,7 +805,14 @@ namespace BTCPayServer.Controllers
|
||||
if (prompt is null)
|
||||
return null;
|
||||
if (activated)
|
||||
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
|
||||
return await GetCheckoutModel(invoiceId, paymentMethodId, lang, excludedPaymentMethodIds);
|
||||
|
||||
if (!prompt.Activated)
|
||||
{
|
||||
// It failed to activate. Let's try to exclude it and retry
|
||||
excludedPaymentMethodIds.Add(prompt.PaymentMethodId);
|
||||
return await GetCheckoutModel(invoiceId, originalPaymentMethodId, lang, excludedPaymentMethodIds);
|
||||
}
|
||||
|
||||
var accounting = prompt.Calculate();
|
||||
|
||||
@ -875,17 +852,12 @@ namespace BTCPayServer.Controllers
|
||||
return extension?.Image ?? "";
|
||||
}
|
||||
|
||||
// Show the "Common divisibility" rather than the payment method disibility.
|
||||
// For example, BTC has commonly 8 digits, but on lightning it has 11. In this case, pick 8.
|
||||
if (this._CurrencyNameTable.GetCurrencyData(prompt.Currency, false)?.Divisibility is not int divisibility)
|
||||
divisibility = prompt.Divisibility;
|
||||
|
||||
string ShowMoney(decimal value) => MoneyExtensions.ShowMoney(value, divisibility);
|
||||
var model = new PaymentModel
|
||||
string ShowMoney(decimal value) => MoneyExtensions.ShowMoney(value, prompt.RateDivisibility ?? prompt.Divisibility);
|
||||
var model = new CheckoutModel
|
||||
{
|
||||
Activated = prompt.Activated,
|
||||
PaymentMethodName = _prettyName.PrettyName(paymentMethodId),
|
||||
CryptoCode = prompt.Currency,
|
||||
PaymentMethodName = _prettyName.PrettyName(paymentMethodId, true),
|
||||
PaymentMethodCurrency = prompt.Currency,
|
||||
RootPath = Request.PathBase.Value.WithTrailingSlash(),
|
||||
OrderId = orderId,
|
||||
InvoiceId = invoiceId,
|
||||
@ -897,9 +869,9 @@ namespace BTCPayServer.Controllers
|
||||
CelebratePayment = storeBlob.CelebratePayment,
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(paymentMethodId)),
|
||||
BtcAddress = prompt.Destination,
|
||||
BtcDue = ShowMoney(accounting.Due),
|
||||
BtcPaid = ShowMoney(accounting.Paid),
|
||||
Address = prompt.Destination,
|
||||
Due = ShowMoney(accounting.Due),
|
||||
Paid = ShowMoney(accounting.Paid),
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
// The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible.
|
||||
OrderAmount = ShowMoney(accounting.TotalDue - (prompt.PaymentMethodFee - prompt.TweakFee)),
|
||||
@ -927,45 +899,31 @@ namespace BTCPayServer.Controllers
|
||||
Status = invoice.Status.ToString(),
|
||||
// The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible.
|
||||
NetworkFee = prompt.PaymentMethodFee - prompt.TweakFee,
|
||||
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.PaymentMethodId).Concat(new[] { prompt.PaymentMethodId }).Distinct().Count() > 1,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentPrompts()
|
||||
AvailablePaymentMethods = invoice.GetPaymentPrompts()
|
||||
.Select(kv =>
|
||||
{
|
||||
var handler = _handlers[kv.PaymentMethodId];
|
||||
return new PaymentModel.AvailableCrypto
|
||||
return new CheckoutModel.AvailablePaymentMethod
|
||||
{
|
||||
Displayed = displayedPaymentMethods.Contains(kv.PaymentMethodId),
|
||||
PaymentMethodId = kv.PaymentMethodId.ToString(),
|
||||
CryptoCode = kv.Currency,
|
||||
PaymentMethodName = _prettyName.PrettyName(kv.PaymentMethodId),
|
||||
IsLightning = handler is ILightningPaymentHandler,
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(kv.PaymentMethodId)),
|
||||
Link = Url.Action(nameof(Checkout),
|
||||
new
|
||||
{
|
||||
invoiceId,
|
||||
paymentMethodId = kv.PaymentMethodId.ToString()
|
||||
})
|
||||
PaymentMethodId = kv.PaymentMethodId,
|
||||
PaymentMethodName = _prettyName.PrettyName(kv.PaymentMethodId, true),
|
||||
Order = kv.PaymentMethodId switch
|
||||
{
|
||||
_ when PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 0,
|
||||
_ when PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 1,
|
||||
_ when handler is ILightningPaymentHandler => 2,
|
||||
_ => 3
|
||||
}
|
||||
};
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.OrderByDescending(a => a.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode).ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
|
||||
})
|
||||
.OrderBy(a => a.Order)
|
||||
.ToList()
|
||||
};
|
||||
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension))
|
||||
extension.ModifyPaymentModel(new PaymentModelContext(model, store, storeBlob, invoice, Url, prompt, handler));
|
||||
model.UISettings = _viewProvider.TryGetViewViewModel(prompt, "CheckoutUI")?.View as CheckoutUIPaymentMethodSettings;
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
model.OrderAmountFiat = OrderAmountFromInvoice(model.CryptoCode, invoice, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
|
||||
foreach (var paymentPrompt in invoice.GetPaymentPrompts())
|
||||
{
|
||||
var vvm = _viewProvider.TryGetViewViewModel(paymentPrompt, "CheckoutUI");
|
||||
if (vvm?.View is CheckoutUIPaymentMethodSettings { ExtensionPartial: { } partial })
|
||||
{
|
||||
model.ExtensionPartials.Add(partial);
|
||||
}
|
||||
}
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
model.OrderAmountFiat = OrderAmountFromInvoice(model.PaymentMethodCurrency, invoice, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
|
||||
if (storeBlob.PlaySoundOnPayment)
|
||||
{
|
||||
@ -978,6 +936,12 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = expiration.PrettyPrint();
|
||||
|
||||
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension) &&
|
||||
_handlers.TryGetValue(paymentMethodId, out var h))
|
||||
{
|
||||
extension.ModifyCheckoutModel(new CheckoutModelContext(model, store, storeBlob, invoice, Url, prompt, h));
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
@ -1013,7 +977,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (string.IsNullOrEmpty(paymentMethodId))
|
||||
paymentMethodId = implicitPaymentMethodId;
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
var model = await GetCheckoutModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
@ -1169,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");
|
||||
}
|
||||
|
||||
@ -1177,7 +1141,7 @@ namespace BTCPayServer.Controllers
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
if (!store.AnyPaymentMethodAvailable())
|
||||
if (!store.AnyPaymentMethodAvailable(_handlers))
|
||||
{
|
||||
return NoPaymentMethodResult(store.Id);
|
||||
}
|
||||
@ -1200,7 +1164,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (!store.AnyPaymentMethodAvailable())
|
||||
if (!store.AnyPaymentMethodAvailable(_handlers))
|
||||
{
|
||||
return NoPaymentMethodResult(store.Id);
|
||||
}
|
||||
@ -1217,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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1243,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,
|
||||
@ -1264,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
|
||||
@ -1370,9 +1334,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private SelectList GetPaymentMethodsSelectList(StoreData store)
|
||||
{
|
||||
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
return new SelectList(store.GetPaymentMethodConfigs()
|
||||
.Where(s => !excludeFilter.Match(s.Key))
|
||||
return new SelectList(store.GetPaymentMethodConfigs(_handlers, true)
|
||||
.Select(method => new SelectListItem(method.Key.ToString(), method.Key.ToString())),
|
||||
nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text));
|
||||
|
@ -35,6 +35,7 @@ using StoreData = BTCPayServer.Data.StoreData;
|
||||
using Serilog.Filters;
|
||||
using PeterO.Numbers;
|
||||
using BTCPayServer.Payouts;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -62,14 +63,14 @@ namespace BTCPayServer.Controllers
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
|
||||
private readonly PaymentMethodViewProvider _viewProvider;
|
||||
private readonly Dictionary<PaymentMethodId, ICheckoutModelExtension> _paymentModelExtensions;
|
||||
private readonly PrettyNameProvider _prettyName;
|
||||
private readonly AppService _appService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UIInvoiceController(
|
||||
InvoiceRepository invoiceRepository,
|
||||
@ -98,8 +99,8 @@ namespace BTCPayServer.Controllers
|
||||
DefaultRulesCollection defaultRules,
|
||||
IAuthorizationService authorizationService,
|
||||
TransactionLinkProviders transactionLinkProviders,
|
||||
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
|
||||
PaymentMethodViewProvider viewProvider,
|
||||
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
|
||||
IStringLocalizer stringLocalizer,
|
||||
PrettyNameProvider prettyName)
|
||||
{
|
||||
_displayFormatter = displayFormatter;
|
||||
@ -124,12 +125,12 @@ namespace BTCPayServer.Controllers
|
||||
_authorizationService = authorizationService;
|
||||
_transactionLinkProviders = transactionLinkProviders;
|
||||
_paymentModelExtensions = paymentModelExtensions;
|
||||
_viewProvider = viewProvider;
|
||||
_prettyName = prettyName;
|
||||
_fileService = fileService;
|
||||
_uriResolver = uriResolver;
|
||||
_defaultRules = defaultRules;
|
||||
_appService = appService;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(Data.PaymentRequestData prData, decimal? amount, decimal amountDue, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
|
||||
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -23,22 +24,24 @@ namespace BTCPayServer
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly LnurlAuthService _lnurlAuthService;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UILNURLAuthController(UserManager<ApplicationUser> userManager, LnurlAuthService lnurlAuthService,
|
||||
LinkGenerator linkGenerator)
|
||||
IStringLocalizer stringLocalizer, LinkGenerator linkGenerator)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_lnurlAuthService = lnurlAuthService;
|
||||
_linkGenerator = linkGenerator;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/delete")]
|
||||
public IActionResult Remove(string id)
|
||||
{
|
||||
return View("Confirm",
|
||||
new ConfirmModel("Remove LNURL Auth link",
|
||||
"Your account will no longer have this Lightning wallet as an option for two-factor authentication.",
|
||||
"Remove"));
|
||||
new ConfirmModel(StringLocalizer["Remove LNURL Auth link"],
|
||||
StringLocalizer["Your account will no longer have this Lightning wallet as an option for two-factor authentication."],
|
||||
StringLocalizer["Remove"]));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
@ -49,7 +52,7 @@ namespace BTCPayServer
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = "LNURL Auth was removed successfully."
|
||||
Message = StringLocalizer["LNURL Auth was removed successfully."].Value
|
||||
});
|
||||
|
||||
return RedirectToList();
|
||||
@ -65,7 +68,7 @@ namespace BTCPayServer
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Html = "The Lightning node could not be registered."
|
||||
Html = StringLocalizer["The Lightning node could not be registered."].Value
|
||||
});
|
||||
|
||||
return RedirectToList();
|
||||
|
@ -19,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");
|
||||
}
|
||||
|
@ -1,21 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> LoginCodes()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
return View(nameof(LoginCodes), _userLoginCodeService.GetOrGenerate(user.Id));
|
||||
}
|
||||
public partial class UIManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public ActionResult LoginCodes()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -40,12 +36,12 @@ namespace BTCPayServer.Controllers
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly Fido2Service _fido2Service;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly UserLoginCodeService _userLoginCodeService;
|
||||
private readonly IHtmlHelper Html;
|
||||
private readonly UserService _userService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly IFileService _fileService;
|
||||
readonly StoreRepository _StoreRepository;
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UIManageController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
@ -62,7 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
UserService userService,
|
||||
UriResolver uriResolver,
|
||||
IFileService fileService,
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
IStringLocalizer stringLocalizer,
|
||||
IHtmlHelper htmlHelper
|
||||
)
|
||||
{
|
||||
@ -76,12 +72,12 @@ namespace BTCPayServer.Controllers
|
||||
_authorizationService = authorizationService;
|
||||
_fido2Service = fido2Service;
|
||||
_linkGenerator = linkGenerator;
|
||||
_userLoginCodeService = userLoginCodeService;
|
||||
Html = htmlHelper;
|
||||
_userService = userService;
|
||||
_uriResolver = uriResolver;
|
||||
_fileService = fileService;
|
||||
_StoreRepository = storeRepository;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -138,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);
|
||||
@ -210,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));
|
||||
@ -238,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));
|
||||
}
|
||||
|
||||
@ -284,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));
|
||||
}
|
||||
@ -333,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));
|
||||
}
|
||||
@ -348,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;
|
||||
|
||||
@ -33,6 +34,7 @@ namespace BTCPayServer.Controllers
|
||||
public class UIPaymentRequestController : Controller
|
||||
{
|
||||
private readonly UIInvoiceController _InvoiceController;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
@ -46,9 +48,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private FormComponentProviders FormProviders { get; }
|
||||
public FormDataService FormDataService { get; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UIPaymentRequestController(
|
||||
UIInvoiceController invoiceController,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService,
|
||||
@ -60,9 +64,11 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceRepository invoiceRepository,
|
||||
FormComponentProviders formProviders,
|
||||
FormDataService formDataService,
|
||||
IStringLocalizer stringLocalizer,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_handlers = handlers;
|
||||
_UserManager = userManager;
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
@ -75,6 +81,7 @@ namespace BTCPayServer.Controllers
|
||||
FormProviders = formProviders;
|
||||
FormDataService = formDataService;
|
||||
_networkProvider = networkProvider;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/payment-requests")]
|
||||
@ -124,7 +131,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (!store.AnyPaymentMethodAvailable())
|
||||
if (!store.AnyPaymentMethodAvailable(_handlers))
|
||||
{
|
||||
return NoPaymentMethodResult(storeId);
|
||||
}
|
||||
@ -159,14 +166,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (!store.AnyPaymentMethodAvailable())
|
||||
if (!store.AnyPaymentMethodAvailable(_handlers))
|
||||
{
|
||||
return NoPaymentMethodResult(store.Id);
|
||||
}
|
||||
|
||||
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;
|
||||
@ -177,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)
|
||||
@ -207,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 });
|
||||
}
|
||||
|
||||
@ -299,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());
|
||||
@ -315,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))
|
||||
{
|
||||
@ -334,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)
|
||||
@ -344,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);
|
||||
@ -388,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 =>
|
||||
@ -396,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)
|
||||
@ -406,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")]
|
||||
@ -443,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();
|
||||
|
@ -21,13 +21,13 @@ namespace BTCPayServer.Controllers
|
||||
public class UIPublicLightningNodeInfoController : Controller
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
|
||||
private readonly Dictionary<PaymentMethodId, ICheckoutModelExtension> _paymentModelExtensions;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
|
||||
public UIPublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
|
||||
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
|
||||
UriResolver uriResolver,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
StoreRepository storeRepository)
|
||||
|
@ -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 { }
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user