Compare commits
103 Commits
v1.8.0-rc2
...
qwfoin
Author | SHA1 | Date | |
---|---|---|---|
a7195fd9dd | |||
29d3a0197f | |||
60d6e98c67 | |||
11f05285a1 | |||
2bd1842da1 | |||
57544068e9 | |||
60cfea9f94 | |||
47f73a46fa | |||
fbeaa3aeea | |||
a7a6a4723d | |||
eece001376 | |||
98d8ef8e1a | |||
427500cf3c | |||
0e43042217 | |||
c8e6714207 | |||
824e779eb2 | |||
83ea898780 | |||
5af3233fd6 | |||
08ff2f3173 | |||
22657b66d7 | |||
8b6c7a6061 | |||
1f197f6688 | |||
1055e61bb4 | |||
7ad0aa82fc | |||
d3f5576570 | |||
45141d1391 | |||
de9ac9fd43 | |||
c53d5272d6 | |||
18c78192ec | |||
632d67eef4 | |||
c23aa48688 | |||
95f3e429b4 | |||
8635fcfe84 | |||
d861537d9a | |||
631ee99f60 | |||
ffa1441ccd | |||
2f3e947027 | |||
a62aecfdfe | |||
5f829c68f2 | |||
0290d74aeb | |||
f6bc16007d | |||
ad5752f09b | |||
55565f1718 | |||
5f96d17b8c | |||
fd22406e0a | |||
64fe542c1e | |||
fae1dc8dbb | |||
6f2b673021 | |||
b26679ca14 | |||
04ba1430ca | |||
53f3758abc | |||
c6742f5533 | |||
cb44591a47 | |||
e02abb509f | |||
eff6be9643 | |||
348dbd7107 | |||
f74ea14d8b | |||
a671632fde | |||
e344622c9e | |||
06d7483ca3 | |||
7fe041fc2c | |||
3f18e5476a | |||
2a31613fe8 | |||
ded0c8a3bc | |||
f3d9e07c5e | |||
eb3ba95114 | |||
7951dcada6 | |||
06951a39c6 | |||
abe29f21f0 | |||
f57eab3008 | |||
6d4b2348ac | |||
397ca6ef0c | |||
d6e5ee2851 | |||
98d62e826b | |||
7b5ce8f70c | |||
2010a9a458 | |||
f787058c17 | |||
87ccae0d90 | |||
07d95c6ed7 | |||
514823f7d2 | |||
fb4feb24f3 | |||
5caa0e0722 | |||
0406b420c8 | |||
9d72b9779e | |||
fdc47e4a38 | |||
0566e964c0 | |||
896fbf9a5c | |||
126c8c101e | |||
3cb7cc01e4 | |||
2b3d15bf45 | |||
4049bdadcb | |||
2042ba37d8 | |||
41a4ba62b0 | |||
21558d25b1 | |||
06622bfbfd | |||
16fd2e3938 | |||
040d7670ec | |||
23761eacc1 | |||
5790bed766 | |||
2f88da67e8 | |||
21091cbf1a | |||
808949a884 | |||
06334273dc |
12
.github/ISSUE_TEMPLATE/config.yml
vendored
12
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,14 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 🚀 Discussions
|
||||
url: https://github.com/btcpayserver/btcpayserver/discussions
|
||||
about: Technical discussions, questions and feature requests
|
||||
- name: 💡 Request a feature
|
||||
url: https://github.com/btcpayserver/btcpayserver/discussions/categories/ideas-feature-requests
|
||||
about: Submit a feature request or vote on ideas posted by others. Features with most upvotes become roadmap candidates
|
||||
- name: 🧑💻 Ask a technical question
|
||||
url: https://github.com/btcpayserver/btcpayserver/discussions/new?category=technical-support
|
||||
about: If you're experiencing a technical problem post it to our community support forum
|
||||
- name: 🔌 Report a problem with a plugin
|
||||
url: https://github.com/btcpayserver/btcpayserver/discussions/new?category=plugins-integrations
|
||||
about: Experiencing a problem with a third-party plugin? Post it here and we will tag their developers to assist
|
||||
- name: 📝 Official Documentation
|
||||
url: https://docs.btcpayserver.org
|
||||
about: Check our documentation for answers to common questions
|
||||
|
@ -5,4 +5,8 @@ public class AssetBalancesUnavailableException : CustodianApiException
|
||||
public AssetBalancesUnavailableException(System.Exception e) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {e.Message}", e)
|
||||
{
|
||||
}
|
||||
|
||||
public AssetBalancesUnavailableException(string errorMsg) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {errorMsg}")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.JsonConverters;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians.Client;
|
||||
|
||||
public class SimulateWithdrawalResult
|
||||
{
|
||||
public string PaymentMethod { get; }
|
||||
public string Asset { get; }
|
||||
public decimal MinQty { get; }
|
||||
public decimal MaxQty { get; }
|
||||
|
||||
public List<LedgerEntryData> LedgerEntries { get; }
|
||||
|
||||
// Fee can be NULL if unknown.
|
||||
public decimal? Fee { get; }
|
||||
|
||||
public SimulateWithdrawalResult(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries,
|
||||
decimal minQty, decimal maxQty)
|
||||
{
|
||||
PaymentMethod = paymentMethod;
|
||||
Asset = asset;
|
||||
LedgerEntries = ledgerEntries;
|
||||
MinQty = minQty;
|
||||
MaxQty = maxQty;
|
||||
}
|
||||
}
|
@ -5,9 +5,14 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for custodians that can move funds to the store wallet.
|
||||
/// </summary>
|
||||
public interface ICanWithdraw
|
||||
{
|
||||
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
|
||||
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken);
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -20,7 +21,6 @@ public interface ICustodian
|
||||
*/
|
||||
Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public Task<Form.Form> GetConfigForm(JObject config, string locale,
|
||||
CancellationToken cancellationToken = default);
|
||||
public Task<Form.Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ namespace BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
public static class GreenfieldExtensions
|
||||
{
|
||||
public static IActionResult UserNotFound(this ControllerBase ctrl)
|
||||
{
|
||||
return ctrl.CreateAPIError(404, "user-not-found", "The user was not found");
|
||||
}
|
||||
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
|
||||
{
|
||||
return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError());
|
||||
|
@ -28,7 +28,7 @@ public class Field
|
||||
|
||||
public bool Constant;
|
||||
|
||||
// HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options).
|
||||
// HTML5 compatible type string like "text", "textarea", "email", "password", etc.
|
||||
public string Type;
|
||||
|
||||
public static Field CreateFieldset()
|
||||
|
@ -32,6 +32,8 @@ public class Form
|
||||
// Are all the fields valid in the form?
|
||||
public bool IsValid()
|
||||
{
|
||||
if (TopMessages?.Any(t => t.Type == AlertMessage.AlertMessageType.Danger) is true)
|
||||
return false;
|
||||
return Fields.Select(f => f.IsValid()).All(o => o);
|
||||
}
|
||||
|
||||
@ -50,7 +52,7 @@ public class Form
|
||||
HashSet<string> nameReturned = new();
|
||||
foreach (var f in GetAllFieldsCore(new List<string>(), Fields))
|
||||
{
|
||||
var fullName = string.Join('_', f.Path);
|
||||
var fullName = string.Join('_', f.Path.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (!nameReturned.Add(fullName))
|
||||
continue;
|
||||
yield return (fullName, f.Path, f.Field);
|
||||
@ -63,7 +65,7 @@ public class Form
|
||||
HashSet<string> nameReturned = new();
|
||||
foreach (var f in GetAllFieldsCore(new List<string>(), Fields))
|
||||
{
|
||||
var fullName = string.Join('_', f.Path);
|
||||
var fullName = string.Join('_', f.Path.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (!nameReturned.Add(fullName))
|
||||
{
|
||||
errors.Add($"Form contains duplicate field names '{fullName}'");
|
||||
@ -128,32 +130,12 @@ public class Form
|
||||
}
|
||||
else if (prop.Value.Type == JTokenType.String)
|
||||
{
|
||||
var fullname = String.Join('_', propPath);
|
||||
if (fields.TryGetValue(fullname, out var f) && !f.Constant)
|
||||
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
|
||||
f.Value = prop.Value.Value<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JObject GetValues()
|
||||
{
|
||||
var r = new JObject();
|
||||
foreach (var f in GetAllFields())
|
||||
{
|
||||
var node = r;
|
||||
for (int i = 0; i < f.Path.Count - 1; i++)
|
||||
{
|
||||
var p = f.Path[i];
|
||||
var child = node[p] as JObject;
|
||||
if (child is null)
|
||||
{
|
||||
child = new JObject();
|
||||
node[p] = child;
|
||||
}
|
||||
node = child;
|
||||
}
|
||||
node[f.Field.Name] = f.Field.Value;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -114,6 +114,11 @@ namespace BTCPayServer.Security
|
||||
_Policies.Add(policy);
|
||||
}
|
||||
|
||||
public void UnsafeEval()
|
||||
{
|
||||
Add("script-src", "'unsafe-eval'");
|
||||
}
|
||||
|
||||
public IEnumerable<ConsentSecurityPolicy> Rules => _Policies;
|
||||
public bool HasRules => _Policies.Count != 0;
|
||||
|
||||
|
@ -50,7 +50,7 @@ namespace BTCPayServer.Client
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<DepositAddressData> GetDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
|
||||
public virtual async Task<DepositAddressData> GetCustodianAccountDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/addresses/{paymentMethod}"), token);
|
||||
return await HandleResponse<DepositAddressData>(response);
|
||||
@ -58,7 +58,6 @@ namespace BTCPayServer.Client
|
||||
|
||||
public virtual async Task<MarketTradeResponseData> MarketTradeCustodianAccountAsset(string storeId, string accountId, TradeRequestData request, CancellationToken token = default)
|
||||
{
|
||||
|
||||
//var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token);
|
||||
//return await HandleResponse<ApplicationUserData>(response);
|
||||
var internalRequest = CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", null,
|
||||
@ -67,13 +66,13 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<MarketTradeResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<MarketTradeResponseData> GetTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
|
||||
public virtual async Task<MarketTradeResponseData> GetCustodianAccountTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/{tradeId}", method: HttpMethod.Get), token);
|
||||
return await HandleResponse<MarketTradeResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<TradeQuoteResponseData> GetTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
|
||||
public virtual async Task<TradeQuoteResponseData> GetCustodianAccountTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = new Dictionary<string, object>();
|
||||
queryPayload.Add("fromAsset", fromAsset);
|
||||
@ -81,14 +80,20 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/quote", queryPayload), token);
|
||||
return await HandleResponse<TradeQuoteResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<WithdrawalResponseData> CreateWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
|
||||
|
||||
public virtual async Task<WithdrawalResponseData> CreateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals", bodyPayload: request, method: HttpMethod.Post), token);
|
||||
return await HandleResponse<WithdrawalResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<WithdrawalSimulationResponseData> SimulateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation", bodyPayload: request, method: HttpMethod.Post), token);
|
||||
return await HandleResponse<WithdrawalSimulationResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<WithdrawalResponseData> GetWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
|
||||
public virtual async Task<WithdrawalResponseData> GetCustodianAccountWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{paymentMethod}/{withdrawalId}", method: HttpMethod.Get), token);
|
||||
return await HandleResponse<WithdrawalResponseData>(response);
|
||||
|
@ -30,9 +30,9 @@ namespace BTCPayServer.JsonConverters
|
||||
case JTokenType.Integer:
|
||||
case JTokenType.String:
|
||||
if (objectType == typeof(decimal) || objectType == typeof(decimal?))
|
||||
return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture);
|
||||
return decimal.Parse(token.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture);
|
||||
if (objectType == typeof(double) || objectType == typeof(double?))
|
||||
return double.Parse(token.ToString(), CultureInfo.InvariantCulture);
|
||||
return double.Parse(token.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture);
|
||||
throw new JsonSerializationException("Unexpected object type: " + objectType);
|
||||
case JTokenType.Null when objectType == typeof(decimal?) || objectType == typeof(double?):
|
||||
return null;
|
||||
|
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class TradeQuantityJsonConverter : JsonConverter<TradeQuantity>
|
||||
{
|
||||
public override TradeQuantity ReadJson(JsonReader reader, Type objectType, TradeQuantity existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JToken token = JToken.Load(reader);
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Float:
|
||||
case JTokenType.Integer:
|
||||
case JTokenType.String:
|
||||
if (TradeQuantity.TryParse(token.ToString(), out var q))
|
||||
return q;
|
||||
break;
|
||||
case JTokenType.Null:
|
||||
return null;
|
||||
}
|
||||
throw new JsonObjectException("Invalid TradeQuantity, expected string. Expected: \"1.50\" or \"50%\"", reader);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, TradeQuantity value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not null)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
@ -6,6 +7,7 @@ namespace BTCPayServer.Client.Models;
|
||||
public class LedgerEntryData
|
||||
{
|
||||
public string Asset { get; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Qty { get; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
|
@ -1,8 +1,13 @@
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class TradeQuoteResponseData
|
||||
{
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Bid { get; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Ask { get; }
|
||||
public string ToAsset { get; }
|
||||
public string FromAsset { get; }
|
||||
|
@ -1,8 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class TradeRequestData
|
||||
{
|
||||
public string FromAsset { set; get; }
|
||||
public string ToAsset { set; get; }
|
||||
public string Qty { set; get; }
|
||||
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
|
||||
public TradeQuantity Qty { set; get; }
|
||||
}
|
||||
|
@ -1,13 +1,85 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class WithdrawRequestData
|
||||
{
|
||||
public string PaymentMethod { set; get; }
|
||||
public decimal Qty { set; get; }
|
||||
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
|
||||
public TradeQuantity Qty { set; get; }
|
||||
|
||||
public WithdrawRequestData(string paymentMethod, decimal qty)
|
||||
public WithdrawRequestData()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public WithdrawRequestData(string paymentMethod, TradeQuantity qty)
|
||||
{
|
||||
PaymentMethod = paymentMethod;
|
||||
Qty = qty;
|
||||
}
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public record TradeQuantity
|
||||
{
|
||||
public TradeQuantity(decimal value, ValueType type)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public enum ValueType
|
||||
{
|
||||
Exact,
|
||||
Percent
|
||||
}
|
||||
|
||||
public ValueType Type { get; }
|
||||
public decimal Value { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Type == ValueType.Exact)
|
||||
return Value.ToString(CultureInfo.InvariantCulture);
|
||||
else
|
||||
return Value.ToString(CultureInfo.InvariantCulture) + "%";
|
||||
}
|
||||
public static TradeQuantity Parse(string str)
|
||||
{
|
||||
if (!TryParse(str, out var r))
|
||||
throw new FormatException("Invalid TradeQuantity");
|
||||
return r;
|
||||
}
|
||||
public static bool TryParse(string str, [MaybeNullWhen(false)] out TradeQuantity quantity)
|
||||
{
|
||||
if (str is null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
quantity = null;
|
||||
str = str.Trim();
|
||||
str = str.Replace(" ", "");
|
||||
if (str.Length == 0)
|
||||
return false;
|
||||
if (str[^1] == '%')
|
||||
{
|
||||
if (!decimal.TryParse(str[..^1], NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
|
||||
return false;
|
||||
if (r < 0.0m)
|
||||
return false;
|
||||
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Percent);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!decimal.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
|
||||
return false;
|
||||
if (r < 0.0m)
|
||||
return false;
|
||||
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Exact);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
22
BTCPayServer.Client/Models/WithdrawalBaseResponseData.cs
Normal file
22
BTCPayServer.Client/Models/WithdrawalBaseResponseData.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public abstract class WithdrawalBaseResponseData
|
||||
{
|
||||
public string Asset { get; }
|
||||
public string PaymentMethod { get; }
|
||||
public List<LedgerEntryData> LedgerEntries { get; }
|
||||
public string AccountId { get; }
|
||||
public string CustodianCode { get; }
|
||||
|
||||
public WithdrawalBaseResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string accountId,
|
||||
string custodianCode)
|
||||
{
|
||||
PaymentMethod = paymentMethod;
|
||||
Asset = asset;
|
||||
LedgerEntries = ledgerEntries;
|
||||
AccountId = accountId;
|
||||
CustodianCode = custodianCode;
|
||||
}
|
||||
}
|
@ -5,18 +5,13 @@ using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class WithdrawalResponseData
|
||||
public class WithdrawalResponseData : WithdrawalBaseResponseData
|
||||
{
|
||||
public string Asset { get; }
|
||||
public string PaymentMethod { get; }
|
||||
public List<LedgerEntryData> LedgerEntries { get; }
|
||||
public string WithdrawalId { get; }
|
||||
public string AccountId { get; }
|
||||
public string CustodianCode { get; }
|
||||
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public WithdrawalStatus Status { get; }
|
||||
|
||||
public string WithdrawalId { get; }
|
||||
public DateTimeOffset CreatedTime { get; }
|
||||
|
||||
public string TransactionId { get; }
|
||||
@ -24,14 +19,10 @@ public class WithdrawalResponseData
|
||||
public string TargetAddress { get; }
|
||||
|
||||
public WithdrawalResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string withdrawalId, string accountId,
|
||||
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId)
|
||||
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId) : base(paymentMethod, asset, ledgerEntries, accountId,
|
||||
custodianCode)
|
||||
{
|
||||
PaymentMethod = paymentMethod;
|
||||
Asset = asset;
|
||||
LedgerEntries = ledgerEntries;
|
||||
WithdrawalId = withdrawalId;
|
||||
AccountId = accountId;
|
||||
CustodianCode = custodianCode;
|
||||
TargetAddress = targetAddress;
|
||||
TransactionId = transactionId;
|
||||
Status = status;
|
||||
|
@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class WithdrawalSimulationResponseData : WithdrawalBaseResponseData
|
||||
{
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? MinQty { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? MaxQty { get; set; }
|
||||
|
||||
public WithdrawalSimulationResponseData(string paymentMethod, string asset, string accountId,
|
||||
string custodianCode, List<LedgerEntryData> ledgerEntries, decimal? minQty, decimal? maxQty) : base(paymentMethod,
|
||||
asset, ledgerEntries, accountId, custodianCode)
|
||||
{
|
||||
MinQty = minQty;
|
||||
MaxQty = maxQty;
|
||||
}
|
||||
}
|
@ -98,6 +98,37 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
return policy.StartsWith("btcpay.plugin", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public static bool IsUserPolicy(string policy)
|
||||
{
|
||||
return policy.StartsWith("btcpay.user", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class PermissionSet
|
||||
{
|
||||
public PermissionSet() : this(Array.Empty<Permission>())
|
||||
{
|
||||
|
||||
}
|
||||
public PermissionSet(Permission[] permissions)
|
||||
{
|
||||
Permissions = permissions;
|
||||
}
|
||||
|
||||
public Permission[] Permissions { get; }
|
||||
|
||||
public bool Contains(Permission requestedPermission)
|
||||
{
|
||||
return Permissions.Any(p => p.Contains(requestedPermission));
|
||||
}
|
||||
public bool Contains(string permission, string store)
|
||||
{
|
||||
if (permission is null)
|
||||
throw new ArgumentNullException(nameof(permission));
|
||||
if (store is null)
|
||||
throw new ArgumentNullException(nameof(store));
|
||||
return Contains(Permission.Create(permission, store));
|
||||
}
|
||||
}
|
||||
public class Permission
|
||||
{
|
||||
@ -105,7 +136,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
Init();
|
||||
}
|
||||
|
||||
|
||||
public static Permission Create(string policy, string scope = null)
|
||||
{
|
||||
if (TryCreatePermission(policy, scope, out var r))
|
||||
@ -121,7 +152,7 @@ namespace BTCPayServer.Client
|
||||
policy = policy.Trim().ToLowerInvariant();
|
||||
if (!Policies.IsValidPolicy(policy))
|
||||
return false;
|
||||
if (scope != null && !Policies.IsStorePolicy(policy))
|
||||
if (!string.IsNullOrEmpty(scope) && !Policies.IsStorePolicy(policy))
|
||||
return false;
|
||||
permission = new Permission(policy, scope);
|
||||
return true;
|
||||
@ -174,7 +205,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
if (!Policies.IsStorePolicy(subpermission.Policy))
|
||||
return true;
|
||||
return Scope == null || subpermission.Scope == this.Scope;
|
||||
return Scope == null || subpermission.Scope == Scope;
|
||||
}
|
||||
|
||||
public static IEnumerable<Permission> ToPermissions(string[] permissions)
|
||||
@ -199,7 +230,8 @@ namespace BTCPayServer.Client
|
||||
return true;
|
||||
if (policy == subpolicy)
|
||||
return true;
|
||||
if (!PolicyMap.TryGetValue(policy, out var subPolicies)) return false;
|
||||
if (!PolicyMap.TryGetValue(policy, out var subPolicies))
|
||||
return false;
|
||||
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
|
||||
}
|
||||
|
||||
@ -213,23 +245,23 @@ namespace BTCPayServer.Client
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreWebhooks,
|
||||
Policies.CanModifyPaymentRequests);
|
||||
Policies.CanModifyPaymentRequests,
|
||||
Policies.CanUseLightningNodeInStore);
|
||||
|
||||
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
|
||||
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments );
|
||||
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments );
|
||||
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests );
|
||||
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile );
|
||||
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore );
|
||||
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser );
|
||||
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
||||
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
||||
PolicyHasChild(Policies.CanModifyServerSettings,
|
||||
Policies.CanUseInternalLightningNode,
|
||||
Policies.CanManageUsers);
|
||||
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode,Policies.CanViewLightningInvoiceInternalNode );
|
||||
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts );
|
||||
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice );
|
||||
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests );
|
||||
|
||||
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
||||
}
|
||||
|
||||
private static void PolicyHasChild(string policy, params string[] subPolicies)
|
||||
@ -243,33 +275,26 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
else
|
||||
{
|
||||
PolicyMap.Add(policy,subPolicies.ToHashSet());
|
||||
PolicyMap.Add(policy, subPolicies.ToHashSet());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string Scope { get; }
|
||||
public string Policy { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Scope != null)
|
||||
{
|
||||
return $"{Policy}:{Scope}";
|
||||
}
|
||||
return Policy;
|
||||
return Scope != null ? $"{Policy}:{Scope}" : Policy;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
Permission item = obj as Permission;
|
||||
if (item == null)
|
||||
return false;
|
||||
return ToString().Equals(item.ToString());
|
||||
return item != null && ToString().Equals(item.ToString());
|
||||
}
|
||||
public static bool operator ==(Permission a, Permission b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
if (ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
31
BTCPayServer.Data/Migrations/20230315062447_fixmaxlength.cs
Normal file
31
BTCPayServer.Data/Migrations/20230315062447_fixmaxlength.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20230315062447_fixmaxlength")]
|
||||
public partial class fixmaxlength : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("ALTER TABLE \"InvoiceSearches\" ALTER COLUMN \"Value\" TYPE TEXT USING \"Value\"::TEXT;");
|
||||
migrationBuilder.Sql("ALTER TABLE \"Invoices\" ALTER COLUMN \"OrderId\" TYPE TEXT USING \"OrderId\"::TEXT;");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
}
|
@ -1305,7 +1305,7 @@
|
||||
"name":"Satoshis",
|
||||
"code":"SATS",
|
||||
"divisibility":0,
|
||||
"symbol":"Sats",
|
||||
"symbol":"sats",
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
|
@ -5,7 +5,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -28,14 +27,6 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
||||
public string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
|
||||
}
|
||||
public string FormatCurrency(decimal price, string currency)
|
||||
{
|
||||
return price.ToString("C", GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
|
||||
{
|
||||
@ -56,6 +47,7 @@ namespace BTCPayServer.Services.Rates
|
||||
currencyInfo.CurrencySymbol = currency;
|
||||
return currencyInfo;
|
||||
}
|
||||
|
||||
public NumberFormatInfo GetNumberFormatInfo(string currency)
|
||||
{
|
||||
var curr = GetCurrencyProvider(currency);
|
||||
@ -65,6 +57,7 @@ namespace BTCPayServer.Services.Rates
|
||||
return ni;
|
||||
return null;
|
||||
}
|
||||
|
||||
public IFormatProvider GetCurrencyProvider(string currency)
|
||||
{
|
||||
lock (_CurrencyProviders)
|
||||
@ -104,30 +97,6 @@ namespace BTCPayServer.Services.Rates
|
||||
currencyProviders.TryAdd(code, number);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a currency like "0.004 $ (USD)", round to significant divisibility
|
||||
/// </summary>
|
||||
/// <param name="value">The value</param>
|
||||
/// <param name="currency">Currency code</param>
|
||||
/// <returns></returns>
|
||||
public string DisplayFormatCurrency(decimal value, string currency)
|
||||
{
|
||||
var provider = GetNumberFormatInfo(currency, true);
|
||||
var currencyData = GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
value = value.RoundToSignificant(ref divisibility);
|
||||
if (divisibility != provider.CurrencyDecimalDigits)
|
||||
{
|
||||
provider = (NumberFormatInfo)provider.Clone();
|
||||
provider.CurrencyDecimalDigits = divisibility;
|
||||
}
|
||||
|
||||
if (currencyData.Crypto)
|
||||
return value.ToString("C", provider);
|
||||
else
|
||||
return value.ToString("C", provider) + $" ({currency})";
|
||||
}
|
||||
|
||||
readonly Dictionary<string, CurrencyData> _Currencies;
|
||||
|
||||
static CurrencyData[] LoadCurrency()
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
@ -386,7 +387,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("BOLT11Expiration")).SendKeys("5" + Keys.Enter);
|
||||
s.GoToInvoice(invoice.Id);
|
||||
s.Driver.FindElement(By.Id("IssueRefund")).Click();
|
||||
|
||||
|
||||
if (multiCurrency)
|
||||
{
|
||||
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
|
||||
@ -396,21 +397,21 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
|
||||
Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat
|
||||
Assert.Contains("1.10000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
|
||||
Assert.Contains("2.20000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
|
||||
Assert.Contains("5,500.00 USD", s.Driver.PageSource); // Should propose reimburse in fiat
|
||||
Assert.Contains("1.10000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
|
||||
Assert.Contains("2.20000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
|
||||
s.Driver.WaitForAndClick(By.Id(rateSelection));
|
||||
s.Driver.FindElement(By.Id("ok")).Click();
|
||||
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
|
||||
Assert.Contains("pull-payments", s.Driver.Url);
|
||||
if (rateSelection == "FiatOption")
|
||||
Assert.Contains("$5,500.00", s.Driver.PageSource);
|
||||
Assert.Contains("5,500.00 USD", s.Driver.PageSource);
|
||||
if (rateSelection == "CurrentOption")
|
||||
Assert.Contains("2.20000000 ₿", s.Driver.PageSource);
|
||||
Assert.Contains("2.20000000 BTC", s.Driver.PageSource);
|
||||
if (rateSelection == "RateThenOption")
|
||||
Assert.Contains("1.10000000 ₿", s.Driver.PageSource);
|
||||
|
||||
Assert.Contains("1.10000000 BTC", s.Driver.PageSource);
|
||||
|
||||
s.GoToInvoice(invoice.Id);
|
||||
s.Driver.FindElement(By.Id("IssueRefund")).Click();
|
||||
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
|
||||
@ -584,7 +585,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
|
||||
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
|
||||
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
|
||||
|
||||
|
||||
// Check if we can disable LTC
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
@ -622,10 +623,11 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
var appType = PointOfSaleAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
@ -680,7 +682,7 @@ donation:
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
Assert.NotNull(appleInvoice);
|
||||
Assert.Equal("good apple", appleInvoice.ItemDesc);
|
||||
|
||||
|
||||
// testing custom amount
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
|
||||
@ -735,7 +737,7 @@ donation:
|
||||
Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility);
|
||||
Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace);
|
||||
}
|
||||
|
||||
|
||||
//test inventory related features
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "hello";
|
||||
@ -756,7 +758,7 @@ noninventoryitem:
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
|
||||
//we already bought all available stock so this should fail
|
||||
await Task.Delay(100);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
@ -819,13 +821,13 @@ normal:
|
||||
normalInvoice.CryptoInfo,
|
||||
s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
|
||||
s.CryptoCode));
|
||||
|
||||
|
||||
//test topup option
|
||||
vmpos.Template = @"
|
||||
a:
|
||||
price: 1000.0
|
||||
title: good apple
|
||||
|
||||
|
||||
b:
|
||||
price: 10.0
|
||||
custom: false
|
||||
@ -843,7 +845,7 @@ f:
|
||||
g:
|
||||
custom: topup
|
||||
";
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.DoesNotContain("custom", vmpos.Template);
|
||||
@ -855,7 +857,7 @@ g:
|
||||
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
|
@ -286,7 +286,7 @@ namespace BTCPayServer.Tests
|
||||
if (permissions.Contains(canModifyAllStores) || storePermissions.Any())
|
||||
{
|
||||
var resultStores =
|
||||
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
|
||||
await TestApiAgainstAccessToken<Client.Models.StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
|
||||
tester.PayTester.HttpClient);
|
||||
|
||||
foreach (var selectiveStorePermission in storePermissions)
|
||||
|
@ -182,7 +182,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var invoiceId = s.CreateInvoice(10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
|
@ -6,6 +6,7 @@ using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using NBitcoin;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@ -50,7 +51,14 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), true);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.WaitForAndClick(By.Id("Presets"));
|
||||
s.Driver.WaitForAndClick(By.Id("Presets_InStore"));
|
||||
Assert.True(s.Driver.SetCheckbox(By.Id("ShowPayInWalletButton"), true));
|
||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
// Top up/zero amount invoices
|
||||
var invoiceId = s.CreateInvoice(amount: null);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
@ -70,7 +78,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(address, copyAddress);
|
||||
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
|
||||
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PayByLNURL"));
|
||||
|
||||
// Details should show exchange rate
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue"));
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("sat/byte", s.Driver.FindElement(By.Id("PaymentDetails-RecommendedFee")).Text);
|
||||
|
||||
// Switch to LNURL
|
||||
s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click();
|
||||
@ -80,12 +95,11 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith("lightning:lnurl", payUrl);
|
||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.Id("Lightning_BTC")).GetAttribute("value"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
});
|
||||
|
||||
// Default payment method
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||
@ -99,9 +113,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(address, copyAddress);
|
||||
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
|
||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
// Lightning amount in Sats
|
||||
// Lightning amount in sats
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
@ -110,7 +123,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Contains("Sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
||||
// Details should not show exchange rate
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-ExchangeRate"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-RecommendedFee"));
|
||||
Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// Expire
|
||||
var expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
@ -123,7 +144,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var expiredSection = s.Driver.FindElement(By.Id("expired"));
|
||||
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||
Assert.True(expiredSection.Displayed);
|
||||
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||
});
|
||||
@ -144,6 +165,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Exchange Rate", details.Text);
|
||||
Assert.Contains("Amount Due", details.Text);
|
||||
Assert.Contains("Recommended Fee", details.Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// Pay partial amount
|
||||
await Task.Delay(200);
|
||||
@ -160,12 +185,27 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Assert.Contains("Created transaction",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
s.Server.ExplorerNode.Generate(1);
|
||||
s.Server.ExplorerNode.Generate(2);
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text);
|
||||
Assert.Contains("Please send", paymentInfo.Text);
|
||||
});
|
||||
|
||||
// Pay full amount
|
||||
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
|
||||
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
|
||||
s.Driver.FindElement(By.Id("FakePay")).Click();
|
||||
|
||||
// Processing
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
|
||||
Assert.True(processingSection.Displayed);
|
||||
Assert.Contains("Payment Received", processingSection.Text);
|
||||
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("confetti")));
|
||||
});
|
||||
|
||||
// Mine
|
||||
s.Driver.FindElement(By.Id("Mine")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
@ -173,18 +213,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Mined 1 block",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
});
|
||||
|
||||
// Pay full amount
|
||||
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
|
||||
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
|
||||
s.Driver.FindElement(By.Id("FakePay")).Click();
|
||||
|
||||
// Settled
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Server.ExplorerNode.Generate(1);
|
||||
var paidSection = s.Driver.WaitForElement(By.Id("paid"));
|
||||
Assert.True(paidSection.Displayed);
|
||||
Assert.Contains("Invoice Paid", paidSection.Text);
|
||||
var settledSection = s.Driver.WaitForElement(By.Id("settled"));
|
||||
Assert.True(settledSection.Displayed);
|
||||
Assert.Contains("Invoice Paid", settledSection.Text);
|
||||
});
|
||||
s.Driver.FindElement(By.Id("confetti"));
|
||||
s.Driver.FindElement(By.Id("ReceiptLink"));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
@ -192,6 +229,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), true);
|
||||
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), false);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
@ -199,6 +237,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
@ -212,8 +251,33 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith("lnbcrt", copyAddressLightning);
|
||||
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue);
|
||||
Assert.Contains("&lightning=LNBCRT", qrValue);
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
|
||||
// Check details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
Assert.Contains("1 BTC = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// Switch to amount displayed in sats
|
||||
s.GoToHome();
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
||||
// Check details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// BIP21 with LN as default payment method
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
@ -223,7 +287,14 @@ namespace BTCPayServer.Tests
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&lightning=lnbcrt", payUrl);
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
// Check details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// Ensure LNURL is enabled
|
||||
s.GoToHome();
|
||||
@ -248,7 +319,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(address, copyAddressOnchain);
|
||||
Assert.StartsWith("lnurl", copyAddressLightning);
|
||||
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue);
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
// Check details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice"));
|
||||
|
||||
// Expiry message should not show amount for top-up invoice
|
||||
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
@ -307,7 +385,23 @@ namespace BTCPayServer.Tests
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&lightning=lnbcrt", payUrl);
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
// Language Switch
|
||||
var languageSelect = new SelectElement(s.Driver.FindElement(By.Id("DefaultLang")));
|
||||
Assert.Equal("English", languageSelect.SelectedOption.Text);
|
||||
Assert.Equal("View Details", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
|
||||
Assert.DoesNotContain("lang=", s.Driver.Url);
|
||||
languageSelect.SelectByText("Deutsch");
|
||||
Assert.Equal("Details anzeigen", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
|
||||
Assert.Contains("lang=de", s.Driver.Url);
|
||||
|
||||
s.Driver.Navigate().Refresh();
|
||||
languageSelect = new SelectElement(s.Driver.FindElement(By.Id("DefaultLang")));
|
||||
Assert.Equal("Deutsch", languageSelect.SelectedOption.Text);
|
||||
Assert.Equal("Details anzeigen", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
|
||||
languageSelect.SelectByText("English");
|
||||
Assert.Equal("View Details", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
|
||||
Assert.Contains("lang=en", s.Driver.Url);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
|
@ -4,11 +4,11 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
@ -34,18 +34,16 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync();
|
||||
var stores = user.GetController<UIStoresController>();
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
var appType = CrowdfundAppType.AppType;
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
|
||||
Assert.Equal(appType, vm.SelectedAppType);
|
||||
Assert.Null(vm.AppName);
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.Equal(nameof(crowdfund.UpdateCrowdfund), redirectToAction.ActionName);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
@ -61,8 +59,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||
Assert.Empty(appList.Apps);
|
||||
}
|
||||
@ -79,10 +77,11 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
var appType = CrowdfundAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
@ -105,7 +104,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
|
||||
|
||||
//Scenario 2: Not Enabled But Admin - Allowed
|
||||
Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
@ -113,8 +112,8 @@ namespace BTCPayServer.Tests
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
|
||||
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
|
||||
|
||||
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
||||
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
|
||||
@ -170,10 +169,10 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
var appType = CrowdfundAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
@ -193,7 +192,7 @@ namespace BTCPayServer.Tests
|
||||
var publicApps = user.GetController<UICrowdfundController>();
|
||||
|
||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||
|
||||
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount);
|
||||
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate);
|
||||
@ -217,7 +216,7 @@ namespace BTCPayServer.Tests
|
||||
}, Facade.Merchant);
|
||||
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||
|
||||
Assert.Equal(0m, model.Info.CurrentAmount);
|
||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||
@ -226,12 +225,12 @@ namespace BTCPayServer.Tests
|
||||
|
||||
TestLogs.LogInformation("Let's check current amount change once payment is confirmed");
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
|
||||
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
|
||||
await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, invoice.BtcDue);
|
||||
await tester.ExplorerNode.GenerateAsync(1); // By default invoice confirmed at 1 block
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||
Assert.Equal(1m, model.Info.CurrentAmount);
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
@ -279,7 +278,7 @@ namespace BTCPayServer.Tests
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
}
|
||||
|
@ -199,11 +199,15 @@ retry:
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SetCheckbox(this IWebDriver driver, By selector, bool value)
|
||||
public static bool SetCheckbox(this IWebDriver driver, By selector, bool value)
|
||||
{
|
||||
var element = driver.FindElement(selector);
|
||||
if (value != element.Selected)
|
||||
{
|
||||
driver.WaitForAndClick(selector);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,6 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public FastTests(ITestOutputHelper helper) : base(helper)
|
||||
{
|
||||
|
||||
}
|
||||
class DockerImage
|
||||
{
|
||||
@ -135,6 +134,33 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CanParseDecimals()
|
||||
{
|
||||
CanParseDecimalsCore("{\"qty\": 1}", 1.0m);
|
||||
CanParseDecimalsCore("{\"qty\": \"1\"}", 1.0m);
|
||||
CanParseDecimalsCore("{\"qty\": 1.0}", 1.0m);
|
||||
CanParseDecimalsCore("{\"qty\": \"1.0\"}", 1.0m);
|
||||
CanParseDecimalsCore("{\"qty\": 6.1e-7}", 6.1e-7m);
|
||||
CanParseDecimalsCore("{\"qty\": \"6.1e-7\"}", 6.1e-7m);
|
||||
|
||||
var data = JsonConvert.DeserializeObject<TradeRequestData>("{\"qty\": \"6.1e-7\", \"fromAsset\":\"Test\"}");
|
||||
Assert.Equal(6.1e-7m, data.Qty.Value);
|
||||
Assert.Equal("Test", data.FromAsset);
|
||||
data = JsonConvert.DeserializeObject<TradeRequestData>("{\"fromAsset\":\"Test\", \"qty\": \"6.1e-7\"}");
|
||||
Assert.Equal(6.1e-7m, data.Qty.Value);
|
||||
Assert.Equal("Test", data.FromAsset);
|
||||
}
|
||||
|
||||
private void CanParseDecimalsCore(string str, decimal expected)
|
||||
{
|
||||
var d = JsonConvert.DeserializeObject<LedgerEntryData>(str);
|
||||
Assert.Equal(expected, d.Qty);
|
||||
var d2 = JsonConvert.DeserializeObject<TradeRequestData>(str);
|
||||
Assert.Equal(new TradeQuantity(expected, TradeQuantity.ValueType.Exact), d2.Qty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMergeReceiptOptions()
|
||||
{
|
||||
@ -326,7 +352,7 @@ namespace BTCPayServer.Tests
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var entity = new InvoiceEntity();
|
||||
@ -512,7 +538,7 @@ namespace BTCPayServer.Tests
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var entity = new InvoiceEntity();
|
||||
@ -600,15 +626,16 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void RoundupCurrenciesCorrectly()
|
||||
{
|
||||
DisplayFormatter displayFormatter = new (CurrencyNameTable.Instance);
|
||||
foreach (var test in new[]
|
||||
{
|
||||
(0.0005m, "$0.0005 (USD)", "USD"), (0.001m, "$0.001 (USD)", "USD"), (0.01m, "$0.01 (USD)", "USD"),
|
||||
(0.1m, "$0.10 (USD)", "USD"), (0.1m, "0,10 € (EUR)", "EUR"), (1000m, "¥1,000 (JPY)", "JPY"),
|
||||
(1000.0001m, "₹ 1,000.00 (INR)", "INR"),
|
||||
(0.0m, "$0.00 (USD)", "USD")
|
||||
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
|
||||
(0.1m, "0.10 USD", "USD"), (0.1m, "0,10 EUR", "EUR"), (1000m, "1,000 JPY", "JPY"),
|
||||
(1000.0001m, "1,000.00 INR", "INR"),
|
||||
(0.0m, "0.00 USD", "USD")
|
||||
})
|
||||
{
|
||||
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
|
||||
var actual = displayFormatter.Currency(test.Item1, test.Item3);
|
||||
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
@ -706,22 +733,69 @@ namespace BTCPayServer.Tests
|
||||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTradeQuantity()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.2345o"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("o"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse(""));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.353%%"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.353 %%"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("-1.353%"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("-1.353"));
|
||||
|
||||
var qty = TradeQuantity.Parse("1.3%");
|
||||
Assert.Equal(1.3m, qty.Value);
|
||||
Assert.Equal(TradeQuantity.ValueType.Percent, qty.Type);
|
||||
var qty2 = TradeQuantity.Parse("1.3");
|
||||
Assert.Equal(1.3m, qty2.Value);
|
||||
Assert.Equal(TradeQuantity.ValueType.Exact, qty2.Type);
|
||||
Assert.NotEqual(qty, qty2);
|
||||
Assert.Equal(qty, TradeQuantity.Parse("1.3%"));
|
||||
Assert.Equal(qty2, TradeQuantity.Parse("1.3"));
|
||||
Assert.Equal(TradeQuantity.Parse(qty.ToString()), TradeQuantity.Parse("1.3%"));
|
||||
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse("1.3"));
|
||||
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDerivationSchemeSettings()
|
||||
{
|
||||
var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork<BTCPayNetwork>("BTC");
|
||||
var mainnet = new BTCPayNetworkProvider(ChainName.Mainnet).GetNetwork<BTCPayNetwork>("BTC");
|
||||
var root = new Mnemonic(
|
||||
"usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage")
|
||||
.DeriveExtKey();
|
||||
|
||||
// xpub
|
||||
var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS";
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(tpub, testnet, out var settings, out var error));
|
||||
Assert.Null(error);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
|
||||
Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
|
||||
|
||||
// xpub with fingerprint and account
|
||||
tpub = "tpubDCXK98mNrPWuoWweaoUkqwxQF5NMWpQLy7n7XJgDCpwYfoZRXGafPaVM7mYqD7UKhsbMxkN864JY2PniMkt1Uk4dNuAMnWFVqdquyvZNyca";
|
||||
var vpub = "vpub5YVA1ZbrqkUVq8NZTtvRDrS2a1yoeBvHbG9NbxqJ6uRtpKGFwjQT11WEqKYsgoDF6gpqrDf8ddmPZe4yXWCjzqF8ad2Cw9xHiE8DSi3X3ik";
|
||||
var fingerprint = "e5746fd9";
|
||||
var account = "84'/1'/0'";
|
||||
var str = $"[{fingerprint}/{account}]{vpub}";
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(str, testnet, out settings, out error));
|
||||
Assert.Null(error);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
Assert.Equal(vpub, settings.AccountOriginal);
|
||||
Assert.Equal(tpub, ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
|
||||
Assert.Equal(HDFingerprint.TryParse(fingerprint, out var hd) ? hd : default, settings.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
|
||||
// ColdCard
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
mainnet, out var settings, out var error));
|
||||
mainnet, out settings, out error));
|
||||
Assert.Null(error);
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint,
|
||||
HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
|
||||
HDFingerprint.TryParse("8bafd160", out hd) ? hd : default);
|
||||
Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label);
|
||||
Assert.Equal("49'/0'/0'", settings.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
Assert.Equal(
|
||||
@ -729,28 +803,26 @@ namespace BTCPayServer.Tests
|
||||
settings.AccountOriginal);
|
||||
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey,
|
||||
settings.AccountDerivation.GetDerivation().ScriptPubKey);
|
||||
var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork<BTCPayNetwork>("BTC");
|
||||
|
||||
// Should be legacy
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit p2sh
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
|
||||
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } });
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
Assert.Null(error);
|
||||
|
||||
// Specter
|
||||
@ -1466,14 +1538,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(1m / 0.000061m, rule2.BidAsk.Bid);
|
||||
|
||||
// testing rounding
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("Sats_EUR"));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("SATS_EUR"));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("0.00000001 * (1.23, 2.34)", rule2.ToString(true));
|
||||
Assert.Equal(0.0000000234m, rule2.BidAsk.Ask);
|
||||
Assert.Equal(0.0000000123m, rule2.BidAsk.Bid);
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_Sats"));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_SATS"));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / (0.00000001 * (1.23, 2.34))", rule2.ToString(true));
|
||||
@ -1715,7 +1787,7 @@ namespace BTCPayServer.Tests
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var networkBTC = networkProvider.GetNetwork("BTC");
|
||||
@ -1860,7 +1932,7 @@ namespace BTCPayServer.Tests
|
||||
})
|
||||
.ToHashSet();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Assert.Equal(1, formats.Count);
|
||||
Assert.Single(formats);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Forms;
|
||||
@ -42,9 +40,10 @@ public class FormTests : UnitTestBase
|
||||
}
|
||||
}
|
||||
};
|
||||
var service = new FormDataService(null, null);
|
||||
var providers = new FormComponentProviders(new List<IFormComponentProvider>());
|
||||
var service = new FormDataService(null, providers);
|
||||
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
|
||||
form = new Form()
|
||||
form = new Form
|
||||
{
|
||||
Fields = new List<Field>
|
||||
{
|
||||
@ -161,12 +160,12 @@ public class FormTests : UnitTestBase
|
||||
}
|
||||
}
|
||||
|
||||
var obj = form.GetValues();
|
||||
var obj = service.GetValues(form);
|
||||
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
||||
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
||||
Clear(form);
|
||||
form.SetValues(obj);
|
||||
obj = form.GetValues();
|
||||
obj = service.GetValues(form);
|
||||
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
||||
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
||||
|
||||
@ -184,10 +183,10 @@ public class FormTests : UnitTestBase
|
||||
}
|
||||
};
|
||||
form.SetValues(obj);
|
||||
obj = form.GetValues();
|
||||
obj = service.GetValues(form);
|
||||
Assert.Null(obj["test"].Value<string>());
|
||||
form.SetValues(new JObject{ ["test"] = "hello" });
|
||||
obj = form.GetValues();
|
||||
obj = service.GetValues(form);
|
||||
Assert.Equal("hello", obj["test"].Value<string>());
|
||||
}
|
||||
|
@ -212,12 +212,14 @@ namespace BTCPayServer.Tests
|
||||
var store = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "Pouet lol" });
|
||||
|
||||
// Grant right to another user
|
||||
newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Id, new CreateApiKeyRequest()
|
||||
newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Email, new CreateApiKeyRequest()
|
||||
{
|
||||
Label = "Hello world",
|
||||
Permissions = new Permission[] { Permission.Create(Policies.CanViewInvoices, store.Id) },
|
||||
});
|
||||
|
||||
await AssertAPIError("user-not-found", () => unrestricted.CreateAPIKey("fewiofwuefo", new CreateApiKeyRequest()));
|
||||
|
||||
// Despite the grant, the user shouldn't be able to get the invoices!
|
||||
newUserClient = acc.CreateClientFromAPIKey(newUserAPIKey.ApiKey);
|
||||
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
|
||||
@ -1303,15 +1305,21 @@ namespace BTCPayServer.Tests
|
||||
await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString());
|
||||
Assert.Single(await scopedClient.GetStores());
|
||||
|
||||
var noauth = await user.CreateClient(Array.Empty<string>());
|
||||
await AssertAPIError("missing-permission", () => noauth.GetStores());
|
||||
|
||||
// We strip the user's Owner right, so the key should not work
|
||||
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
|
||||
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
|
||||
storeEntity.Role = "Guest";
|
||||
await ctx.SaveChangesAsync();
|
||||
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
|
||||
|
||||
client = await user.CreateClient(Policies.Unrestricted);
|
||||
stores = await client.GetStores();
|
||||
foreach (var s2 in stores)
|
||||
{
|
||||
await tester.PayTester.StoreRepository.DeleteStore(s2.Id);
|
||||
}
|
||||
tester.DeleteStore = false;
|
||||
Assert.Empty(await client.GetStores());
|
||||
}
|
||||
|
||||
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
|
||||
@ -3932,8 +3940,7 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
var withdrawalClient = await admin.CreateClient(Policies.CanWithdrawFromCustodianAccounts);
|
||||
var depositClient = await admin.CreateClient(Policies.CanDepositToCustodianAccounts);
|
||||
var tradeClient = await admin.CreateClient(Policies.CanTradeCustodianAccount);
|
||||
|
||||
|
||||
|
||||
var store = await adminClient.GetStore(admin.StoreId);
|
||||
var storeId = store.Id;
|
||||
|
||||
@ -3973,28 +3980,28 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
|
||||
|
||||
// Test: GetDepositAddress, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, wrong payment method
|
||||
await AssertHttpError(400, async () => await depositClient.GetDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
|
||||
|
||||
await AssertApiError( 400, "unsupported-payment-method", async () => await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
|
||||
|
||||
// Test: GetDepositAddress, wrong store ID
|
||||
await AssertHttpError(403, async () => await depositClient.GetDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
await AssertHttpError(403, async () => await depositClient.GetCustodianAccountDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, wrong account ID
|
||||
await AssertHttpError(404, async () => await depositClient.GetDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
|
||||
|
||||
await AssertHttpError(404, async () => await depositClient.GetCustodianAccountDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, correct payment method
|
||||
var depositAddress = await depositClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
|
||||
var depositAddress = await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
|
||||
Assert.NotNull(depositAddress);
|
||||
Assert.Equal(MockCustodian.DepositAddress, depositAddress.Address);
|
||||
|
||||
|
||||
// Test: Trade, unauth
|
||||
var tradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture) };
|
||||
var tradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact)};
|
||||
await AssertHttpError(401, async () => await unauthClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest));
|
||||
|
||||
// Test: Trade, auth, but wrong permission
|
||||
@ -4021,17 +4028,13 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, newTradeResult.LedgerEntries[2].Type);
|
||||
|
||||
// Test: GetTradeQuote, SATS
|
||||
var satsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = "SATS", Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture) };
|
||||
var satsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = "SATS", Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) };
|
||||
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, satsTradeRequest));
|
||||
|
||||
// TODO Test: Trade with percentage qty
|
||||
|
||||
// Test: Trade with wrong decimal format (example: JavaScript scientific format)
|
||||
var wrongQtyTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = "6.1e-7" };
|
||||
await AssertApiError(400, "bad-qty-format", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, wrongQtyTradeRequest));
|
||||
|
||||
// Test: Trade, wrong assets method
|
||||
var wrongAssetsTradeRequest = new TradeRequestData { FromAsset = "WRONG", ToAsset = MockCustodian.TradeToAsset, Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture) };
|
||||
var wrongAssetsTradeRequest = new TradeRequestData { FromAsset = "WRONG", ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) };
|
||||
await AssertHttpError(WrongTradingPairException.HttpCode, async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, wrongAssetsTradeRequest));
|
||||
|
||||
// Test: wrong account ID
|
||||
@ -4041,18 +4044,18 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
await AssertHttpError(403, async () => await tradeClient.MarketTradeCustodianAccountAsset("WRONG-STORE-ID", accountId, tradeRequest));
|
||||
|
||||
// Test: Trade, correct assets, wrong amount
|
||||
var insufficientFundsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = "0.01" };
|
||||
var insufficientFundsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(0.01m, TradeQuantity.ValueType.Exact) };
|
||||
await AssertApiError(400, "insufficient-funds", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, insufficientFundsTradeRequest));
|
||||
|
||||
|
||||
// Test: GetTradeQuote, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: GetTradeQuote, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: GetTradeQuote, auth, correct permission
|
||||
var tradeQuote = await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
|
||||
var tradeQuote = await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
|
||||
Assert.NotNull(tradeQuote);
|
||||
Assert.Equal(MockCustodian.TradeFromAsset, tradeQuote.FromAsset);
|
||||
Assert.Equal(MockCustodian.TradeToAsset, tradeQuote.ToAsset);
|
||||
@ -4060,30 +4063,30 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
Assert.Equal(MockCustodian.BtcPriceInEuro, tradeQuote.Ask);
|
||||
|
||||
// Test: GetTradeQuote, SATS
|
||||
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
|
||||
|
||||
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
|
||||
|
||||
// Test: GetTradeQuote, wrong asset
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset));
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "WRONG-ASSET"));
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset));
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset , "WRONG-ASSET"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: wrong store ID
|
||||
await AssertHttpError(403, async () => await tradeClient.GetTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Test: GetTradeInfo, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
// Test: GetTradeInfo, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
// Test: GetTradeInfo, auth, correct permission
|
||||
var tradeResult = await tradeClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId);
|
||||
var tradeResult = await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId);
|
||||
Assert.NotNull(tradeResult);
|
||||
Assert.Equal(accountId, tradeResult.AccountId);
|
||||
Assert.Equal(mockCustodian.Code, tradeResult.CustodianCode);
|
||||
@ -4103,66 +4106,93 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, tradeResult.LedgerEntries[2].Type);
|
||||
|
||||
// Test: GetTradeInfo, wrong trade ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
|
||||
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
|
||||
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
|
||||
|
||||
// Test: wrong store ID
|
||||
await AssertHttpError(403, async () => await tradeClient.GetTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
|
||||
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
|
||||
|
||||
var qty = new TradeQuantity(MockCustodian.WithdrawalAmount, TradeQuantity.ValueType.Exact);
|
||||
// Test: SimulateWithdrawal, unauth
|
||||
var simulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
await AssertHttpError(401, async () => await unauthClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, correct payment method, correct amount
|
||||
var simulateWithdrawResponse = await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest);
|
||||
AssertMockWithdrawal(simulateWithdrawResponse, custodianAccountData);
|
||||
|
||||
// Test: SimulateWithdrawal, wrong payment method
|
||||
var wrongPaymentMethodSimulateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty);
|
||||
await AssertApiError( 400, "unsupported-payment-method", async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodSimulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, wrong store ID
|
||||
// TODO it is wierd that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
|
||||
await AssertHttpError(403, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal( "WRONG-STORE-ID",accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, correct payment method, wrong amount
|
||||
var wrongAmountSimulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
|
||||
await AssertHttpError(400, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongAmountSimulateWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, unauth
|
||||
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalAmount);
|
||||
await AssertHttpError(401, async () => await unauthClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
var createWithdrawalRequestPercentage = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
await AssertHttpError(401, async () => await unauthClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, correct amount
|
||||
var withdrawResponse = await withdrawalClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest);
|
||||
var withdrawResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest);
|
||||
AssertMockWithdrawal(withdrawResponse, custodianAccountData);
|
||||
|
||||
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, correct amount, but as a percentage
|
||||
var withdrawWithPercentageResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequestPercentage);
|
||||
AssertMockWithdrawal(withdrawWithPercentageResponse, custodianAccountData);
|
||||
|
||||
// Test: CreateWithdrawal, wrong payment method
|
||||
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", MockCustodian.WithdrawalAmount);
|
||||
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
|
||||
|
||||
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty);
|
||||
await AssertApiError( 400, "unsupported-payment-method", async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.CreateWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
|
||||
|
||||
await AssertHttpError(404, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, wrong store ID
|
||||
// TODO it is wierd that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
|
||||
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal("WRONG-STORE-ID", accountId, createWithdrawalRequest));
|
||||
|
||||
await AssertHttpError(403, async () => await withdrawalClient.CreateCustodianAccountWithdrawal( "WRONG-STORE-ID",accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, wrong amount
|
||||
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, new decimal(0.666));
|
||||
await AssertHttpError(400, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
|
||||
|
||||
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
|
||||
await AssertHttpError(400, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
|
||||
|
||||
// Test: GetWithdrawalInfo, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: GetWithdrawalInfo, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: GetWithdrawalInfo, auth, correct permission
|
||||
var withdrawalInfo = await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
|
||||
var withdrawalInfo = await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
|
||||
AssertMockWithdrawal(withdrawalInfo, custodianAccountData);
|
||||
|
||||
// Test: GetWithdrawalInfo, wrong withdrawal ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
|
||||
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: wrong store ID
|
||||
// TODO shouldn't this be 404? I cannot change this without bigger impact, as it would affect all API endpoints that are store centered
|
||||
await AssertHttpError(403, async () => await withdrawalClient.GetWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
|
||||
await AssertHttpError(403, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// TODO assert API error codes, not just status codes by using AssertCustodianApiError()
|
||||
|
||||
// TODO also test withdrawals for the various "Status" (Queued, Complete, Failed)
|
||||
// TODO create a mock custodian with only ICustodian
|
||||
// TODO create a mock custodian with only ICustodian + ICanWithdraw
|
||||
@ -4170,12 +4200,11 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
// TODO create a mock custodian with only ICustodian + ICanDeposit
|
||||
}
|
||||
|
||||
private void AssertMockWithdrawal(WithdrawalResponseData withdrawResponse, CustodianAccountData account)
|
||||
private void AssertMockWithdrawal(WithdrawalBaseResponseData withdrawResponse, CustodianAccountData account)
|
||||
{
|
||||
Assert.NotNull(withdrawResponse);
|
||||
Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.Asset);
|
||||
Assert.Equal(MockCustodian.WithdrawalPaymentMethod, withdrawResponse.PaymentMethod);
|
||||
Assert.Equal(MockCustodian.WithdrawalStatus, withdrawResponse.Status);
|
||||
Assert.Equal(account.Id, withdrawResponse.AccountId);
|
||||
Assert.Equal(account.CustodianCode, withdrawResponse.CustodianCode);
|
||||
|
||||
@ -4189,10 +4218,20 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
Assert.Equal(MockCustodian.WithdrawalFee, withdrawResponse.LedgerEntries[1].Qty);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, withdrawResponse.LedgerEntries[1].Type);
|
||||
|
||||
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawResponse.TargetAddress);
|
||||
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawResponse.TransactionId);
|
||||
Assert.Equal(MockCustodian.WithdrawalId, withdrawResponse.WithdrawalId);
|
||||
Assert.NotEqual(default, withdrawResponse.CreatedTime);
|
||||
if (withdrawResponse is WithdrawalResponseData withdrawalResponseData)
|
||||
{
|
||||
Assert.Equal(MockCustodian.WithdrawalStatus, withdrawalResponseData.Status);
|
||||
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawalResponseData.TargetAddress);
|
||||
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawalResponseData.TransactionId);
|
||||
Assert.Equal(MockCustodian.WithdrawalId, withdrawalResponseData.WithdrawalId);
|
||||
Assert.NotEqual(default, withdrawalResponseData.CreatedTime);
|
||||
}
|
||||
|
||||
if (withdrawResponse is WithdrawalSimulationResponseData withdrawalSimulationResponseData)
|
||||
{
|
||||
Assert.Equal(MockCustodian.WithdrawalMinAmount, withdrawalSimulationResponseData.MinQty);
|
||||
Assert.Equal(MockCustodian.WithdrawalMaxAmount, withdrawalSimulationResponseData.MaxQty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
@ -24,6 +25,9 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
|
||||
public const string WithdrawalAsset = "BTC";
|
||||
public const string WithdrawalId = "WITHDRAWAL-ID-001";
|
||||
public static readonly decimal WithdrawalAmount = new decimal(0.5);
|
||||
public static readonly string WithdrawalAmountPercentage = "12.5%";
|
||||
public static readonly decimal WithdrawalMinAmount = new decimal(0.001);
|
||||
public static readonly decimal WithdrawalMaxAmount = new decimal(0.6);
|
||||
public static readonly decimal WithdrawalFee = new decimal(0.0005);
|
||||
public const string WithdrawalTransactionId = "yyy";
|
||||
public const string WithdrawalTargetAddress = "bc1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";
|
||||
@ -52,7 +56,7 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
|
||||
return Task.FromResult(r);
|
||||
}
|
||||
|
||||
public Task<Form> GetConfigForm(JObject config, string locale, CancellationToken cancellationToken = default)
|
||||
public Task<Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -135,14 +139,38 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
|
||||
var r = new WithdrawResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalId, WithdrawalStatus, createdTime, WithdrawalTargetAddress, WithdrawalTransactionId);
|
||||
return r;
|
||||
}
|
||||
|
||||
private SimulateWithdrawalResult CreateWithdrawSimulationResult()
|
||||
{
|
||||
var ledgerEntries = new List<LedgerEntryData>();
|
||||
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalAmount - WithdrawalFee, LedgerEntryData.LedgerEntryType.Withdrawal));
|
||||
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalFee, LedgerEntryData.LedgerEntryType.Fee));
|
||||
var r = new SimulateWithdrawalResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalMinAmount, WithdrawalMaxAmount);
|
||||
return r;
|
||||
}
|
||||
|
||||
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
|
||||
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
if (paymentMethod == WithdrawalPaymentMethod)
|
||||
{
|
||||
if (amount.ToString(CultureInfo.InvariantCulture).Equals(""+WithdrawalAmount, StringComparison.InvariantCulture) || WithdrawalAmountPercentage.Equals(amount))
|
||||
{
|
||||
return Task.FromResult(CreateWithdrawResult());
|
||||
}
|
||||
|
||||
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount} or {WithdrawalAmountPercentage}");
|
||||
}
|
||||
|
||||
throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}");
|
||||
}
|
||||
|
||||
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
if (paymentMethod == WithdrawalPaymentMethod)
|
||||
{
|
||||
if (amount == WithdrawalAmount)
|
||||
{
|
||||
return Task.FromResult(CreateWithdrawResult());
|
||||
return Task.FromResult(CreateWithdrawSimulationResult());
|
||||
}
|
||||
|
||||
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}");
|
||||
|
@ -2,10 +2,9 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -32,10 +31,11 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
var appType = PointOfSaleAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
|
@ -221,7 +221,7 @@ namespace BTCPayServer.Tests
|
||||
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
|
||||
|
||||
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true });
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "SATS", FullNotifications = true });
|
||||
if (unsupportedFormats.Contains(receiverAddressType))
|
||||
{
|
||||
Assert.Null(TestAccount.GetPayjoinBitcoinUrl(invoice, cashCow.Network));
|
||||
|
@ -4,11 +4,13 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -75,8 +77,7 @@ namespace BTCPayServer.Tests
|
||||
s.GenerateWallet(isHotWallet: true);
|
||||
|
||||
// Point Of Sale
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
|
||||
new SelectElement(s.Driver.FindElement(By.Id("SelectedAppType"))).SelectByValue("PointOfSale");
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
@ -466,7 +467,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Rules_0__To")).SendKeys("test@gmail.com");
|
||||
s.Driver.FindElement(By.Id("Rules_0__CustomerEmail")).Click();
|
||||
s.Driver.FindElement(By.Id("Rules_0__Subject")).SendKeys("Thanks!");
|
||||
s.Driver.FindElement(By.Id("Rules_0__Body")).SendKeys("Your invoice is settled");
|
||||
s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your invoice is settled");
|
||||
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
|
||||
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
|
||||
}
|
||||
@ -578,8 +579,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
|
||||
});
|
||||
|
||||
Assert.Contains(s.Server.PayTester.GetService<CurrencyNameTable>().DisplayFormatCurrency(100, "USD"),
|
||||
s.Driver.PageSource);
|
||||
Assert.Contains("100.00 USD", s.Driver.PageSource);
|
||||
Assert.Contains(i, s.Driver.PageSource);
|
||||
|
||||
s.GoToInvoices(s.StoreId);
|
||||
@ -832,6 +832,105 @@ namespace BTCPayServer.Tests
|
||||
AssertUrlHasPairingCode(s);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CookieReflectProperPermissions()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
var alice = s.Server.NewAccount();
|
||||
alice.Register(false);
|
||||
await alice.CreateStoreAsync();
|
||||
var bob = s.Server.NewAccount();
|
||||
await bob.CreateStoreAsync();
|
||||
await bob.AddGuest(alice.UserId);
|
||||
|
||||
s.GoToLogin();
|
||||
s.LogIn(alice.Email, alice.Password);
|
||||
s.GoToUrl($"/cheat/permissions/stores/{bob.StoreId}");
|
||||
var pageSource = s.Driver.PageSource;
|
||||
AssertPermissions(pageSource, true,
|
||||
new[]
|
||||
{
|
||||
Policies.CanViewInvoices,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewPaymentRequests,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreSettingsUnscoped,
|
||||
Policies.CanDeleteUser
|
||||
});
|
||||
AssertPermissions(pageSource, false,
|
||||
new[]
|
||||
{
|
||||
Policies.CanModifyStoreSettings,
|
||||
Policies.CanCreateNonApprovedPullPayments,
|
||||
Policies.CanCreatePullPayments,
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyServerSettings
|
||||
});
|
||||
|
||||
s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
|
||||
pageSource = s.Driver.PageSource;
|
||||
|
||||
AssertPermissions(pageSource, true,
|
||||
new[]
|
||||
{
|
||||
Policies.CanViewInvoices,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewPaymentRequests,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreSettingsUnscoped,
|
||||
Policies.CanDeleteUser,
|
||||
Policies.CanModifyStoreSettings,
|
||||
Policies.CanCreateNonApprovedPullPayments,
|
||||
Policies.CanCreatePullPayments,
|
||||
Policies.CanManagePullPayments
|
||||
});
|
||||
AssertPermissions(pageSource, false,
|
||||
new[]
|
||||
{
|
||||
Policies.CanModifyServerSettings
|
||||
});
|
||||
|
||||
await alice.MakeAdmin();
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.LogIn(alice.Email, alice.Password);
|
||||
s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
|
||||
pageSource = s.Driver.PageSource;
|
||||
|
||||
AssertPermissions(pageSource, true,
|
||||
new[]
|
||||
{
|
||||
Policies.CanViewInvoices,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewPaymentRequests,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreSettingsUnscoped,
|
||||
Policies.CanDeleteUser,
|
||||
Policies.CanModifyStoreSettings,
|
||||
Policies.CanCreateNonApprovedPullPayments,
|
||||
Policies.CanCreatePullPayments,
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyServerSettings,
|
||||
Policies.CanCreateUser,
|
||||
Policies.CanManageUsers
|
||||
});
|
||||
}
|
||||
|
||||
void AssertPermissions(string source, bool expected, string[] permissions)
|
||||
{
|
||||
if (expected)
|
||||
{
|
||||
foreach (var p in permissions)
|
||||
Assert.Contains(p + "<", source);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var p in permissions)
|
||||
Assert.DoesNotContain(p + "<", source);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanCreateAppPoS()
|
||||
{
|
||||
@ -839,9 +938,8 @@ namespace BTCPayServer.Tests
|
||||
await s.StartAsync();
|
||||
var userId = s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid());
|
||||
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
@ -927,9 +1025,8 @@ namespace BTCPayServer.Tests
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme();
|
||||
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateCrowdfund")).Click();
|
||||
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
|
||||
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
@ -1274,11 +1371,11 @@ namespace BTCPayServer.Tests
|
||||
// Can add a label?
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).Click();
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).Click();
|
||||
await Task.Delay(500);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("test-label" + Keys.Enter);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("test-label" + Keys.Enter);
|
||||
await Task.Delay(500);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("label2" + Keys.Enter);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("label2" + Keys.Enter);
|
||||
});
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
@ -1470,9 +1567,20 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// BIP-329 export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
|
||||
Thread.Sleep(1000);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
|
||||
Assert.EndsWith("export?format=bip329", s.Driver.Url);
|
||||
Assert.Contains("{\"type\":\"tx\",\"ref\":\"", s.Driver.PageSource);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// CSV export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportCSV")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1619,7 +1727,10 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains("transaction-label", s.Driver.PageSource);
|
||||
});
|
||||
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transaction-label")).Text);
|
||||
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
|
||||
Assert.Equal(2, labels.Count);
|
||||
Assert.Contains(labels, element => element.Text == "payout");
|
||||
Assert.Contains(labels, element => element.Text == "pull-payment");
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
||||
@ -1892,9 +2003,7 @@ namespace BTCPayServer.Tests
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
|
||||
s.Driver.FindElement(By.Id("SelectedAppType")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("option[value='PointOfSale']")).Click();
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text));
|
||||
|
@ -246,15 +246,18 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
public List<string> Stores { get; internal set; } = new List<string>();
|
||||
|
||||
public bool DeleteStore { get; set; } = true;
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var r in this.Resources)
|
||||
r.Dispose();
|
||||
TestLogs.LogInformation("Disposing the BTCPayTester...");
|
||||
foreach (var store in Stores)
|
||||
if (DeleteStore)
|
||||
{
|
||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||
foreach (var store in Stores)
|
||||
{
|
||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||
}
|
||||
}
|
||||
if (PayTester != null)
|
||||
PayTester.Dispose();
|
||||
|
@ -220,8 +220,8 @@ namespace BTCPayServer.Tests
|
||||
RegisterDetails = new RegisterViewModel()
|
||||
{
|
||||
Email = Utils.GenerateEmail(),
|
||||
ConfirmPassword = "Kitten0@",
|
||||
Password = "Kitten0@",
|
||||
ConfirmPassword = Password,
|
||||
Password = Password,
|
||||
IsAdmin = isAdmin
|
||||
};
|
||||
await account.Register(RegisterDetails);
|
||||
@ -240,6 +240,7 @@ namespace BTCPayServer.Tests
|
||||
Email = RegisterDetails.Email;
|
||||
IsAdmin = account.RegisteredAdmin;
|
||||
}
|
||||
public string Password { get; set; } = "Kitten0@";
|
||||
|
||||
public RegisterViewModel RegisterDetails { get; set; }
|
||||
|
||||
|
@ -353,6 +353,11 @@ retry:
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(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}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
string GetFileContent(params string[] path)
|
||||
|
@ -35,6 +35,8 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Plugins.PayButton;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services;
|
||||
@ -1953,14 +1955,13 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
var appType = PointOfSaleAppType.AppType;
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
|
||||
Assert.Equal(appType, vm.SelectedAppType);
|
||||
Assert.Null(vm.AppName);
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.Equal(nameof(pos.UpdatePointOfSale), redirectToAction.ActionName);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var appList2 =
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
||||
@ -1976,7 +1977,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
|
@ -1,13 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.Auth.AccessControlPolicy;
|
||||
using Amazon.Runtime.Internal;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using ExchangeSharp;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.DevTools.V100.Network;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static BTCPayServer.Tests.TransifexClient;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -16,6 +31,12 @@ namespace BTCPayServer.Tests
|
||||
/// </summary>
|
||||
public class UtilitiesTests
|
||||
{
|
||||
public ITestOutputHelper Logs { get; }
|
||||
|
||||
public UtilitiesTests(ITestOutputHelper logs)
|
||||
{
|
||||
Logs = logs;
|
||||
}
|
||||
internal static string GetSecuritySchemeDescription()
|
||||
{
|
||||
var description =
|
||||
@ -39,6 +60,178 @@ namespace BTCPayServer.Tests
|
||||
string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")));
|
||||
return description;
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// This will take the translations from v1 or v2
|
||||
// /// and upload them to transifex if not found
|
||||
// /// </summary>
|
||||
// [FactWithSecret("TransifexAPIToken")]
|
||||
// [Trait("Utilities", "Utilities")]
|
||||
//#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
// public async Task UpdateTransifex()
|
||||
// {
|
||||
// // DO NOT RUN IT, THIS WILL ERASE THE CURRENT TRANSIFEX TRANSLATIONS
|
||||
|
||||
// var client = GetTransifexClient();
|
||||
// var translations = JsonTranslation.GetTranslations(TranslationFolder.CheckoutV2);
|
||||
// var enTranslations = translations["en"];
|
||||
// translations.Remove("en");
|
||||
|
||||
// foreach (var t in translations)
|
||||
// {
|
||||
// foreach (var w in t.Value.Words.ToArray())
|
||||
// {
|
||||
// if (t.Value.Words[w.Key] == null)
|
||||
// t.Value.Words[w.Key] = enTranslations.Words[w.Key];
|
||||
// }
|
||||
// t.Value.Words.Remove("code");
|
||||
// t.Value.Words.Remove("NOTICE_WARN");
|
||||
// }
|
||||
// await client.UpdateTranslations(translations);
|
||||
// }
|
||||
|
||||
//#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
///// <summary>
|
||||
///// This utility will copy translations made on checkout v1 to checkout v2
|
||||
///// </summary>
|
||||
//[Fact]
|
||||
//[Trait("Utilities", "Utilities")]
|
||||
//public void SetTranslationV1ToV2()
|
||||
//{
|
||||
// var mappings = new Dictionary<string, string>();
|
||||
// foreach (var kv in JsonTranslation.GetTranslations(TranslationFolder.CheckoutV1))
|
||||
// {
|
||||
// var v1File = kv.Value;
|
||||
// var v2File = JsonTranslation.GetTranslation(TranslationFolder.CheckoutV2, v1File.Lang);
|
||||
// if (mappings.Count == 0)
|
||||
// {
|
||||
// foreach (var prop1 in v1File.Words)
|
||||
// foreach (var prop2 in v2File.Words)
|
||||
// {
|
||||
// if (Normalize(prop1.Key) == Normalize(prop2.Key))
|
||||
// mappings.Add(prop1.Key, prop2.Key);
|
||||
// }
|
||||
// mappings.Add("Copied", "copy_confirm");
|
||||
// mappings.Add("ConversionTab_BodyDesc", "conversion_body");
|
||||
// mappings.Add("Return to StoreName", "return_to_store");
|
||||
// }
|
||||
// foreach (var m in mappings)
|
||||
// {
|
||||
// var orig = v1File.Words[m.Key];
|
||||
// v2File.Words[m.Value] = orig;
|
||||
// }
|
||||
// v2File.Words["currentLanguage"] = v1File.Words["currentLanguage"];
|
||||
// v2File.Save();
|
||||
// }
|
||||
//}
|
||||
|
||||
//private string Normalize(string name)
|
||||
//{
|
||||
// return name.Replace("_", "").ToLowerInvariant();
|
||||
//}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This utility will use selenium to pilot your browser to
|
||||
/// automatically translate a language.
|
||||
///
|
||||
/// Step 1: Close all Chrome instances
|
||||
/// Step2: Edit "v1" variable if want to translate checkout v1 or v2
|
||||
/// - Windows: "chrome.exe --remote-debugging-port=9222 https://chat.openai.com/"
|
||||
/// - Linux: "google-chrome --remote-debugging-port=9222 https://chat.openai.com/"
|
||||
/// Step 3: Run this.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Trait("Utilities", "Utilities")]
|
||||
[FactWithSecret("TransifexAPIToken")]
|
||||
public async Task AutoTranslateChatGPT()
|
||||
{
|
||||
var file = TranslationFolder.CheckoutV1;
|
||||
|
||||
using var driver = new ChromeDriver(new ChromeOptions()
|
||||
{
|
||||
DebuggerAddress = "127.0.0.1:9222"
|
||||
});
|
||||
|
||||
var englishTranslations = JsonTranslation.GetTranslation(file, "en");
|
||||
|
||||
TransifexClient client = GetTransifexClient();
|
||||
var langs = await client.GetLangs(englishTranslations.TransifexProject, englishTranslations.TransifexResource);
|
||||
foreach (var lang in langs)
|
||||
{
|
||||
if (lang == "en")
|
||||
continue;
|
||||
var jsonLangCode = GetLangCodeTransifexToJson(lang);
|
||||
var v1LangFile = JsonTranslation.GetTranslation(TranslationFolder.CheckoutV1, jsonLangCode);
|
||||
|
||||
if (!v1LangFile.Exists())
|
||||
continue;
|
||||
var languageCurrent = v1LangFile.Words["currentLanguage"];
|
||||
if (v1LangFile.ShouldSkip())
|
||||
{
|
||||
Logs.WriteLine("Skipped " + jsonLangCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
var langFile = JsonTranslation.GetTranslation(file, jsonLangCode);
|
||||
bool askedPrompt = false;
|
||||
foreach (var translation in langFile.Words)
|
||||
{
|
||||
if (translation.Key == "NOTICE_WARN" ||
|
||||
translation.Key == "currentLanguage" ||
|
||||
translation.Key == "code")
|
||||
continue;
|
||||
|
||||
var english = englishTranslations.Words[translation.Key];
|
||||
if (translation.Value != null)
|
||||
continue; // Already translated
|
||||
if (!askedPrompt)
|
||||
{
|
||||
driver.FindElement(By.XPath("//a[contains(text(), \"New chat\")]")).Click();
|
||||
Thread.Sleep(200);
|
||||
var input = driver.FindElement(By.XPath("//textarea[@data-id]"));
|
||||
input.SendKeys($"I am translating a checkout crypto payment page, and I want you to translate it from English (en-US) to {languageCurrent} ({jsonLangCode}).");
|
||||
input.SendKeys(Keys.LeftShift + Keys.Enter);
|
||||
input.SendKeys("Reply only with the translation of the sentences I will give you and nothing more." + Keys.Enter);
|
||||
WaitCanWritePrompt(driver);
|
||||
askedPrompt = true;
|
||||
}
|
||||
english = english.Replace('\n', ' ');
|
||||
driver.FindElement(By.XPath("//textarea[@data-id]")).SendKeys(english + Keys.Enter);
|
||||
WaitCanWritePrompt(driver);
|
||||
var elements = driver.FindElements(By.XPath("//div[contains(@class,'markdown') and contains(@class,'prose')]//p"));
|
||||
var result = elements.Last().Text;
|
||||
langFile.Words[translation.Key] = result;
|
||||
}
|
||||
langFile.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static TransifexClient GetTransifexClient()
|
||||
{
|
||||
return new TransifexClient(FactWithSecretAttribute.GetFromSecrets("TransifexAPIToken"));
|
||||
}
|
||||
|
||||
private void WaitCanWritePrompt(IWebDriver driver)
|
||||
{
|
||||
|
||||
retry:
|
||||
Thread.Sleep(200);
|
||||
try
|
||||
{
|
||||
driver.FindElement(By.XPath("//*[contains(text(), \"Regenerate response\")]"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
Thread.Sleep(200);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This utility will make sure that permission documentation is properly written in swagger.template.json
|
||||
/// </summary>
|
||||
[Trait("Utilities", "Utilities")]
|
||||
[Fact]
|
||||
public void UpdateSwagger()
|
||||
@ -50,7 +243,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download transifex transactions and put them in BTCPayServer\wwwroot\locales
|
||||
/// Download transifex transactions and put them in BTCPayServer\wwwroot\locales and BTCPayServer\wwwroot\locales\checkout
|
||||
/// </summary>
|
||||
[FactWithSecret("TransifexAPIToken")]
|
||||
[Trait("Utilities", "Utilities")]
|
||||
@ -58,77 +251,74 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
|
||||
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
|
||||
var client = new TransifexClient(FactWithSecretAttribute.GetFromSecrets("TransifexAPIToken"));
|
||||
await PullTransifexTranslationsCore(TranslationFolder.CheckoutV1);
|
||||
await PullTransifexTranslationsCore(TranslationFolder.CheckoutV2);
|
||||
|
||||
var proj = "o:btcpayserver:p:btcpayserver";
|
||||
var resource = $"{proj}:r:enjson";
|
||||
var json = await client.GetTransifexAsync($"https://rest.api.transifex.com/resource_language_stats?filter[project]={proj}&filter[resource]={resource}");
|
||||
var langs = json["data"].Select(o => o["id"].Value<string>().Split(':').Last()).ToArray();
|
||||
json = await client.GetTransifexAsync($"https://rest.api.transifex.com/resource_strings?filter[resource]={resource}");
|
||||
var hashToKeys = json["data"]
|
||||
.ToDictionary(
|
||||
o => o["id"].Value<string>().Split(':').Last(),
|
||||
o => o["attributes"]["key"].Value<string>().Replace("\\.", "."));
|
||||
}
|
||||
|
||||
var translations = new ConcurrentDictionary<string, JObject>();
|
||||
translations.TryAdd("en",
|
||||
new JObject(
|
||||
json["data"]
|
||||
.Select(o => new JProperty(
|
||||
o["attributes"]["key"].Value<string>().Replace("\\.", "."),
|
||||
o["attributes"]["strings"]["other"].Value<string>())))
|
||||
);
|
||||
private async Task PullTransifexTranslationsCore(TranslationFolder folder)
|
||||
{
|
||||
var enTranslation = JsonTranslation.GetTranslation(folder, "en");
|
||||
var client = GetTransifexClient();
|
||||
var langs = await client.GetLangs(enTranslation.TransifexProject, enTranslation.TransifexResource);
|
||||
var resourceStrings = await client.GetResourceStrings(enTranslation.TransifexResource);
|
||||
|
||||
var langsDir = Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales");
|
||||
enTranslation.Words.Clear();
|
||||
enTranslation.Translate(resourceStrings.SourceTranslations);
|
||||
enTranslation.Save();
|
||||
|
||||
Task.WaitAll(langs.Select(async l =>
|
||||
{
|
||||
if (l == "en")
|
||||
return;
|
||||
retry:
|
||||
var j = await client.GetTransifexAsync($"https://rest.api.transifex.com/resource_translations?filter[resource]={resource}&filter[language]=l:{l}");
|
||||
retry:
|
||||
try
|
||||
{
|
||||
var jobj = new JObject(
|
||||
j["data"].Select(o => (Key: hashToKeys[o["id"].Value<string>().Split(':')[^3]], Strings: o["attributes"]["strings"]))
|
||||
.Select(o =>
|
||||
new JProperty(
|
||||
o.Key,
|
||||
o.Strings.Type == JTokenType.Null ? translations["en"][o.Key].Value<string>() : o.Strings["other"].Value<string>()
|
||||
)));
|
||||
if (l == "ne_NP")
|
||||
l = "np_NP";
|
||||
if (l == "zh_CN")
|
||||
l = "zh-SP";
|
||||
if (l == "kk")
|
||||
l = "kk-KZ";
|
||||
|
||||
var langCode = l.Replace("_", "-");
|
||||
jobj["code"] = langCode;
|
||||
|
||||
if ((string)jobj["currentLanguage"] == "English")
|
||||
return; // Not translated
|
||||
if ((string)jobj["currentLanguage"] == "disable")
|
||||
return; // Not translated
|
||||
|
||||
if (jobj["InvoiceExpired_Body_3"].Value<string>() == translations["en"]["InvoiceExpired_Body_3"].Value<string>())
|
||||
var langCode = GetLangCodeTransifexToJson(l);
|
||||
var langTranslations = await client.GetTranslations(resourceStrings, l);
|
||||
var translation = JsonTranslation.GetTranslation(folder, langCode);
|
||||
if (translation.ShouldSkip())
|
||||
{
|
||||
jobj["InvoiceExpired_Body_3"] = string.Empty;
|
||||
Logs.WriteLine("Skipping " + langCode);
|
||||
return;
|
||||
}
|
||||
translations.TryAdd(langCode, jobj);
|
||||
|
||||
if (translation.Words.ContainsKey("InvoiceExpired_Body_3") && translation.Words["InvoiceExpired_Body_3"] == enTranslation.Words["InvoiceExpired_Body_3"])
|
||||
{
|
||||
translation.Words["InvoiceExpired_Body_3"] = string.Empty;
|
||||
}
|
||||
translation.Translate(langTranslations);
|
||||
translation.Save();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
goto retry;
|
||||
}
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
foreach (var t in translations)
|
||||
{
|
||||
t.Value.AddFirst(new JProperty("NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/"));
|
||||
var content = t.Value.ToString(Newtonsoft.Json.Formatting.Indented);
|
||||
File.WriteAllText(Path.Combine(langsDir, $"{t.Key}.json"), content);
|
||||
}
|
||||
internal static string GetLangCodeTransifexToJson(string l)
|
||||
{
|
||||
if (l == "ne_NP")
|
||||
l = "np-NP";
|
||||
if (l == "zh_CN")
|
||||
l = "zh-SP";
|
||||
if (l == "kk")
|
||||
l = "kk-KZ";
|
||||
|
||||
return l.Replace("_", "-");
|
||||
}
|
||||
internal static string GetLangCodeJsonToTransifex(string l)
|
||||
{
|
||||
if (l == "np-NP")
|
||||
l = "ne_NP";
|
||||
if (l == "zh-SP")
|
||||
l = "zh_CN";
|
||||
if (l == "kk-KZ")
|
||||
l = "kk";
|
||||
|
||||
return l.Replace("-", "_");
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,9 +338,254 @@ namespace BTCPayServer.Tests
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", APIToken);
|
||||
message.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/vnd.api+json"));
|
||||
var response = await Client.SendAsync(message);
|
||||
using var response = await Client.SendAsync(message);
|
||||
var str = await response.Content.ReadAsStringAsync();
|
||||
return JObject.Parse(str);
|
||||
}
|
||||
|
||||
public async Task UpdateTranslations(Dictionary<string, JsonTranslation> translations)
|
||||
{
|
||||
var resourceStrings = await GetResourceStrings(translations.First().Value.TransifexResource);
|
||||
List<JObject> patches = new List<JObject>();
|
||||
List<JObject[]> batches = new List<JObject[]>();
|
||||
foreach (var translation in translations.Values)
|
||||
{
|
||||
foreach (var word in translation.Words)
|
||||
{
|
||||
if (word.Key == "NOTICE_WARN")
|
||||
continue;
|
||||
patches.Add(new JObject()
|
||||
{
|
||||
["id"] = $"{translation.TransifexResource}:s:{resourceStrings.KeyToHashMapping[word.Key]}:l:{UtilitiesTests.GetLangCodeJsonToTransifex(translation.Lang)}",
|
||||
["type"] = "resource_translations",
|
||||
["attributes"] = new JObject()
|
||||
{
|
||||
["strings"] = word.Value is null ? null : new JObject()
|
||||
{
|
||||
["other"] = word.Value
|
||||
}
|
||||
}
|
||||
});
|
||||
if (patches.Count >= 150)
|
||||
{
|
||||
batches.Add(patches.ToArray());
|
||||
patches = new List<JObject>();
|
||||
}
|
||||
}
|
||||
if (patches.Count > 0)
|
||||
{
|
||||
batches.Add(patches.ToArray());
|
||||
patches = new List<JObject>();
|
||||
}
|
||||
}
|
||||
|
||||
if (patches.Count > 0)
|
||||
{
|
||||
batches.Add(patches.ToArray());
|
||||
patches = new List<JObject>();
|
||||
}
|
||||
await Task.WhenAll(batches.Select(async batch =>
|
||||
{
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, "https://rest.api.transifex.com/resource_translations");
|
||||
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", APIToken);
|
||||
message.Method = HttpMethod.Patch;
|
||||
var content = new StringContent(new JObject()
|
||||
{
|
||||
["data"] = new JArray(batch.OfType<object>().ToArray())
|
||||
}.ToString(), Encoding.UTF8);
|
||||
content.Headers.Remove("Content-Type");
|
||||
content.Headers.TryAddWithoutValidation("Content-Type", "application/vnd.api+json;profile=\"bulk\"");
|
||||
message.Content = content;
|
||||
using var response = await Client.SendAsync(message);
|
||||
var str = await response.Content.ReadAsStringAsync();
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string>> GetTranslations(ResourceStrings resourceStrings, string lang)
|
||||
{
|
||||
var j = await GetTransifexAsync($"https://rest.api.transifex.com/resource_translations?filter[resource]={resourceStrings.ResourceId}&filter[language]=l:{lang}");
|
||||
if (j["data"] is null)
|
||||
{
|
||||
return resourceStrings.SourceTranslations.ToDictionary(kv => kv.Key, kv => null as string);
|
||||
}
|
||||
return
|
||||
j["data"].Select(o => (Key: resourceStrings.GetKey(o["id"].Value<string>()), Strings: o["attributes"]["strings"]))
|
||||
.ToDictionary(
|
||||
o => o.Key,
|
||||
o => o.Strings.Type == JTokenType.Null ? null : o.Strings["other"].Value<string>());
|
||||
}
|
||||
|
||||
public async Task<string[]> GetLangs(string projectId, string resourceId)
|
||||
{
|
||||
var json = await GetTransifexAsync($"https://rest.api.transifex.com/resource_language_stats?filter[project]={projectId}&filter[resource]={resourceId}");
|
||||
return json["data"].Select(o => o["id"].Value<string>().Split(':').Last()).ToArray();
|
||||
}
|
||||
|
||||
|
||||
public async Task<ResourceStrings> GetResourceStrings(string resourceId)
|
||||
{
|
||||
var res = new ResourceStrings();
|
||||
res.ResourceId = resourceId;
|
||||
var json = await GetTransifexAsync($"https://rest.api.transifex.com/resource_strings?filter[resource]={resourceId}");
|
||||
res.HashToKeyMapping =
|
||||
json["data"]
|
||||
.ToDictionary(
|
||||
o => o["id"].Value<string>().Split(':').Last(),
|
||||
o => o["attributes"]["key"].Value<string>().Replace("\\.", "."));
|
||||
res.KeyToHashMapping = res.HashToKeyMapping.ToDictionary(o => o.Value, o => o.Key);
|
||||
res.SourceTranslations =
|
||||
json["data"]
|
||||
.ToDictionary(
|
||||
o => o["attributes"]["key"].Value<string>().Replace("\\.", "."),
|
||||
o => o["attributes"]["strings"]["other"].Value<string>());
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
public class ResourceStrings
|
||||
{
|
||||
public string ResourceId { get; set; }
|
||||
public Dictionary<string, string> HashToKeyMapping { get; set; }
|
||||
public Dictionary<string, string> SourceTranslations { get; set; }
|
||||
public Dictionary<string, string> KeyToHashMapping { get; internal set; }
|
||||
|
||||
public string GetKey(string hash)
|
||||
{
|
||||
if (HashToKeyMapping.TryGetValue(hash, out var v))
|
||||
return v;
|
||||
hash = hash.Split(':')[^3];
|
||||
if (HashToKeyMapping.TryGetValue(hash, out v))
|
||||
return v;
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public enum TranslationFolder
|
||||
{
|
||||
CheckoutV1,
|
||||
CheckoutV2
|
||||
}
|
||||
public class JsonTranslation
|
||||
{
|
||||
|
||||
public static Dictionary<string, JsonTranslation> GetTranslations(TranslationFolder folder)
|
||||
{
|
||||
var res = new Dictionary<string, JsonTranslation>();
|
||||
var source = GetTranslation(null, folder, "en");
|
||||
foreach (var f in Directory.GetFiles(GetFolder(folder)))
|
||||
{
|
||||
var lang = Path.GetFileNameWithoutExtension(f);
|
||||
res.Add(lang, GetTranslation(source, folder, lang));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
public static JsonTranslation GetTranslation(TranslationFolder folder, string lang)
|
||||
{
|
||||
var source = GetTranslation(null, folder, "en");
|
||||
return GetTranslation(source, folder, lang);
|
||||
}
|
||||
private static JsonTranslation GetTranslation(JsonTranslation sourceTranslation, TranslationFolder folder, string lang)
|
||||
{
|
||||
var fullPath = Path.Combine(GetFolder(folder), $"{lang}.json");
|
||||
var proj = "o:btcpayserver:p:btcpayserver";
|
||||
string resource;
|
||||
if (folder == TranslationFolder.CheckoutV1)
|
||||
{
|
||||
resource = $"{proj}:r:enjson";
|
||||
}
|
||||
else // file == v2
|
||||
{
|
||||
resource = $"{proj}:r:checkout-v2";
|
||||
}
|
||||
var words = new Dictionary<string, string>();
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
var obj = JObject.Parse(File.ReadAllText(fullPath));
|
||||
foreach (var prop in obj.Properties())
|
||||
words.Add(prop.Name, prop.Value.Value<string>());
|
||||
}
|
||||
if (sourceTranslation != null)
|
||||
{
|
||||
foreach (var w in sourceTranslation.Words)
|
||||
{
|
||||
if (!words.ContainsKey(w.Key))
|
||||
words.Add(w.Key, null);
|
||||
}
|
||||
}
|
||||
return new JsonTranslation()
|
||||
{
|
||||
FullPath = fullPath,
|
||||
Lang = lang,
|
||||
Words = words,
|
||||
TransifexProject = proj,
|
||||
TransifexResource = resource
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetFolder(TranslationFolder file)
|
||||
{
|
||||
if (file == TranslationFolder.CheckoutV1)
|
||||
return Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales");
|
||||
else
|
||||
return Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales", "checkout");
|
||||
}
|
||||
|
||||
public string Lang { get; set; }
|
||||
public Dictionary<string, string> Words { get; set; }
|
||||
|
||||
|
||||
public string FullPath { get; set; }
|
||||
public string TransifexProject { get; set; }
|
||||
public string TransifexResource { get; private set; }
|
||||
|
||||
public void Save()
|
||||
{
|
||||
JObject obj = new JObject
|
||||
{
|
||||
{ "NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK https://chat.btcpayserver.org/ TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/" },
|
||||
{ "code", Lang },
|
||||
{ "currentLanguage", Words["currentLanguage"] }
|
||||
};
|
||||
foreach (var kv in Words)
|
||||
{
|
||||
if (obj[kv.Key] is not null)
|
||||
continue;
|
||||
if (kv.Value is null)
|
||||
continue;
|
||||
obj.Add(kv.Key, kv.Value);
|
||||
}
|
||||
try
|
||||
{
|
||||
File.WriteAllText(FullPath, obj.ToString(Newtonsoft.Json.Formatting.Indented));
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
File.Create(FullPath).Close();
|
||||
File.WriteAllText(FullPath, obj.ToString(Newtonsoft.Json.Formatting.Indented));
|
||||
}
|
||||
}
|
||||
|
||||
public void Translate(Dictionary<string,string> sourceTranslations)
|
||||
{
|
||||
foreach (var o in sourceTranslations)
|
||||
if (o.Value != null)
|
||||
Words.AddOrReplace(o.Key, o.Value);
|
||||
}
|
||||
|
||||
public bool ShouldSkip()
|
||||
{
|
||||
if (!Words.ContainsKey("currentLanguage"))
|
||||
return true;
|
||||
if (Words["currentLanguage"] == "English")
|
||||
return true;
|
||||
if (Words["currentLanguage"] == "disable")
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Exists()
|
||||
{
|
||||
return File.Exists(FullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v22.11-dev
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -203,7 +203,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v22.11-dev
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
|
@ -141,7 +141,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v22.11-dev
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -190,7 +190,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v22.11-dev
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
|
@ -53,7 +53,7 @@
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="LNURL" Version="0.0.28" />
|
||||
<PackageReference Include="LNURL" Version="0.0.29" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
@ -134,11 +134,11 @@
|
||||
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" />
|
||||
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
|
||||
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BTCPayServer.Plugins.Custodians.FakeCustodian\BTCPayServer.Plugins.Custodians.FakeCustodian.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Watch Include="Views\**\*.*"></Watch>
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer
|
||||
var bg = ColorTranslator.FromHtml(bgColor);
|
||||
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
|
||||
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
|
||||
return ColorTranslator.ToHtml(color);
|
||||
return ColorTranslator.ToHtml(color).ToLowerInvariant();
|
||||
}
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
public static readonly ColorPalette Default = new ColorPalette(new string[] {
|
||||
@ -59,5 +59,44 @@ namespace BTCPayServer
|
||||
return Labels[num % Labels.Length];
|
||||
}
|
||||
}
|
||||
|
||||
/// https://gist.github.com/zihotki/09fc41d52981fb6f93a81ebf20b35cd5
|
||||
/// <summary>
|
||||
/// Creates color with corrected brightness.
|
||||
/// </summary>
|
||||
/// <param name="color">Color to correct.</param>
|
||||
/// <param name="correctionFactor">The brightness correction factor. Must be between -1 and 1.
|
||||
/// Negative values produce darker colors.</param>
|
||||
/// <returns>
|
||||
/// Corrected <see cref="Color"/> structure.
|
||||
/// </returns>
|
||||
public Color AdjustBrightness(Color color, float correctionFactor)
|
||||
{
|
||||
float red = color.R;
|
||||
float green = color.G;
|
||||
float blue = color.B;
|
||||
|
||||
if (correctionFactor < 0)
|
||||
{
|
||||
correctionFactor = 1 + correctionFactor;
|
||||
red *= correctionFactor;
|
||||
green *= correctionFactor;
|
||||
blue *= correctionFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
red = (255 - red) * correctionFactor + red;
|
||||
green = (255 - green) * correctionFactor + green;
|
||||
blue = (255 - blue) * correctionFactor + blue;
|
||||
}
|
||||
|
||||
return Color.FromArgb(color.A, (int)red, (int)green, (int)blue);
|
||||
}
|
||||
|
||||
public string AdjustBrightness(string html, float correctionFactor)
|
||||
{
|
||||
var color = AdjustBrightness(ColorTranslator.FromHtml(html), correctionFactor);
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Security.AccessControl;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
@ -6,6 +7,8 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
@ -24,17 +27,28 @@ public class AppSales : ViewComponent
|
||||
_appService = appService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppSalesViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
||||
{
|
||||
if (vm.App == null)
|
||||
throw new ArgumentNullException(nameof(vm.App));
|
||||
var type = _appService.GetAppType(appType);
|
||||
if (type is not IHasSaleStatsAppType salesAppType || type is not AppBaseType appBaseType)
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
var vm = new AppSalesViewModel
|
||||
{
|
||||
Id = appId,
|
||||
AppType = appType,
|
||||
DataUrl = Url.Action("AppSales", "UIApps", new { appId }),
|
||||
InitialRendering = HttpContext.GetAppData()?.Id != appId
|
||||
};
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
var stats = await _appService.GetSalesStats(vm.App);
|
||||
|
||||
|
||||
var app = HttpContext.GetAppData();
|
||||
var stats = await _appService.GetSalesStats(app);
|
||||
vm.SalesCount = stats.SalesCount;
|
||||
vm.Series = stats.Series;
|
||||
vm.AppType = app.AppType;
|
||||
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
||||
vm.Name = app.Name;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
public class AppSalesViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public AppSalesPeriod Period { get; set; } = AppSalesPeriod.Week;
|
||||
public int SalesCount { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public AppSalesPeriod Period { get; set; }
|
||||
public string AppUrl { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
public long SalesCount { get; set; }
|
||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
@using BTCPayServer.Services.Apps
|
||||
@using BTCPayServer.Components.AppSales
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Plugins.Crowdfund
|
||||
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
||||
|
||||
@{
|
||||
var controller = $"UI{Model.App.AppType}";
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "Contributions" : "Sales";
|
||||
var label = Model.AppType == CrowdfundAppType.AppType ? "Contributions" : "Sales";
|
||||
}
|
||||
|
||||
<div id="AppSales-@Model.App.Id" class="widget app-sales">
|
||||
<div id="AppSales-@Model.Id" class="widget app-sales">
|
||||
<header class="mb-3">
|
||||
<h3>@Model.App.Name @label</h3>
|
||||
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
|
||||
<h3>@Model.Name @label</h3>
|
||||
@if (!string.IsNullOrEmpty(Model.AppUrl))
|
||||
{
|
||||
<a href="@Model.AppUrl">Manage</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
@ -20,15 +21,16 @@
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/Components/AppSales/Default.cshtml.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("AppSales", "UIApps", new { appId = Model.App.Id }));
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const url = @Safe.Json(Model.DataUrl);
|
||||
const appId = @Safe.Json(Model.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppSales-${appId}`).outerHTML = await response.text();
|
||||
const initScript = document.querySelector(`#AppSales-${appId} script`);
|
||||
if (initScript) eval(initScript.innerHTML);
|
||||
const data = document.querySelector(`#AppSales-${appId} template`);
|
||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@ -40,54 +42,15 @@
|
||||
<span class="sales-count">@Model.SalesCount</span> Total @label
|
||||
</span>
|
||||
<div class="btn-group only-for-js" role="group" aria-label="Filter">
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.App.Id" id="AppSalesPeriodWeek-@Model.App.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodWeek-@Model.App.Id">1W</label>
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.App.Id" id="AppSalesPeriodMonth-@Model.App.Id" value="@AppSalesPeriod.Month" @(Model.Period == AppSalesPeriod.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodMonth-@Model.App.Id">1M</label>
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.Id" id="AppSalesPeriodWeek-@Model.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodWeek-@Model.Id">1W</label>
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.Id" id="AppSalesPeriodMonth-@Model.Id" value="@AppSalesPeriod.Month" @(Model.Period == AppSalesPeriod.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodMonth-@Model.Id">1M</label>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ct-chart"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = @Safe.Json($"AppSales-{Model.App.Id}");
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const period = @Safe.Json(Model.Period.ToString());
|
||||
const baseUrl = @Safe.Json(Url.Action("AppSales", "UIApps", new { appId = Model.App.Id }));
|
||||
const data = { series: @Safe.Json(Model.Series), salesCount: @Safe.Json(Model.SalesCount) };
|
||||
|
||||
const render = (data, period) => {
|
||||
const series = data.series.map(s => s.salesCount);
|
||||
const labels = data.series.map((s, i) => period === @Safe.Json(Model.Period.ToString()) ? s.label : (i % 5 === 0 ? s.label : ''));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
|
||||
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
||||
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
});
|
||||
};
|
||||
|
||||
render(data, period);
|
||||
|
||||
const update = async period => {
|
||||
const url = `${baseUrl}/${period}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
render(data, period);
|
||||
}
|
||||
};
|
||||
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<template>
|
||||
@Safe.Json(Model)
|
||||
</template>
|
||||
}
|
||||
</div>
|
||||
|
45
BTCPayServer/Components/AppSales/Default.cshtml.js
Normal file
45
BTCPayServer/Components/AppSales/Default.cshtml.js
Normal file
@ -0,0 +1,45 @@
|
||||
if (!window.appSales) {
|
||||
window.appSales =
|
||||
{
|
||||
dataLoaded: function (model) {
|
||||
const id = "AppSales-" + model.id;
|
||||
const appId = model.id;
|
||||
const period = model.period;
|
||||
const baseUrl = model.url;
|
||||
const data = model;
|
||||
|
||||
const render = (data, period) => {
|
||||
const series = data.series.map(s => s.salesCount);
|
||||
const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : ''));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
|
||||
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
||||
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
});
|
||||
};
|
||||
|
||||
render(data, period);
|
||||
|
||||
const update = async period => {
|
||||
const url = `${baseUrl}/${period}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
render(data, period);
|
||||
}
|
||||
};
|
||||
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Components.AppSales;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
||||
@ -18,18 +21,29 @@ public class AppTopItems : ViewComponent
|
||||
_appService = appService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppTopItemsViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
||||
{
|
||||
if (vm.App == null)
|
||||
throw new ArgumentNullException(nameof(vm.App));
|
||||
var type = _appService.GetAppType(appType);
|
||||
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType)
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
|
||||
var vm = new AppTopItemsViewModel
|
||||
{
|
||||
Id = appId,
|
||||
AppType = appType,
|
||||
DataUrl = Url.Action("AppTopItems", "UIApps", new { appId }),
|
||||
InitialRendering = HttpContext.GetAppData()?.Id != appId
|
||||
};
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
var entries = Enum.Parse<AppType>(vm.App.AppType) == AppType.Crowdfund
|
||||
? await _appService.GetPerkStats(vm.App)
|
||||
: await _appService.GetItemStats(vm.App);
|
||||
|
||||
var app = HttpContext.GetAppData();
|
||||
var entries = await _appService.GetItemStats(app);
|
||||
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
||||
vm.Entries = entries.ToList();
|
||||
vm.AppType = app.AppType;
|
||||
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
||||
vm.Name = app.Name;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
||||
public class AppTopItemsViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public string AppUrl { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
public List<ItemStats> Entries { get; set; }
|
||||
public List<int> SalesCount { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
@using BTCPayServer.Services.Apps
|
||||
@using BTCPayServer.Plugins.Crowdfund
|
||||
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
|
||||
|
||||
@{
|
||||
var controller = $"UI{Model.App.AppType}";
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "contribution" : "sale";
|
||||
var label = Model.AppType == CrowdfundAppType.AppType ? "contribution" : "sale";
|
||||
}
|
||||
|
||||
<div id="AppTopItems-@Model.App.Id" class="widget app-top-items">
|
||||
<div id="AppTopItems-@Model.Id" class="widget app-top-items">
|
||||
<header class="mb-3">
|
||||
<h3>Top @(Model.App.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")</h3>
|
||||
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
|
||||
<h3>Top @(Model.AppType == CrowdfundAppType.AppType ? "Perks" : "Items")</h3>
|
||||
@if (!string.IsNullOrEmpty(Model.AppUrl))
|
||||
{
|
||||
<a href="@Model.AppUrl">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
@ -19,37 +19,26 @@
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/Components/AppTopItems/Default.cshtml.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("AppTopItems", "UIApps", new { appId = Model.App.Id }));
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
||||
const initScript = document.querySelector(`#AppTopItems-${appId} script`);
|
||||
if (initScript) eval(initScript.innerHTML);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Model.DataUrl);
|
||||
const appId = @Safe.Json(Model.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
||||
const data = document.querySelector(`#AppSales-${appId} template`);
|
||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
else if (Model.Entries.Any())
|
||||
{
|
||||
<div class="ct-chart mb-3"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = @Safe.Json($"AppTopItems-{Model.App.Id}");
|
||||
const series = @Safe.Json(Model.Entries.Select(i => i.SalesCount));
|
||||
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
|
||||
distributeSeries: true,
|
||||
horizontalBars: true,
|
||||
showLabel: false,
|
||||
stackBars: true,
|
||||
axisY: {
|
||||
offset: 0
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<template>
|
||||
@Safe.Json(Model)
|
||||
</template>
|
||||
<div class="app-items">
|
||||
@for (var i = 0; i < Model.Entries.Count; i++)
|
||||
{
|
||||
|
18
BTCPayServer/Components/AppTopItems/Default.cshtml.js
Normal file
18
BTCPayServer/Components/AppTopItems/Default.cshtml.js
Normal file
@ -0,0 +1,18 @@
|
||||
if (!window.appTopItems) {
|
||||
window.appTopItems =
|
||||
{
|
||||
dataLoaded: function (model) {
|
||||
const id = "AppTopItems-" + model.id;
|
||||
const series = model.salesCount;
|
||||
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
|
||||
distributeSeries: true,
|
||||
horizontalBars: true,
|
||||
showLabel: false,
|
||||
stackBars: true,
|
||||
axisY: {
|
||||
offset: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Components.Icon
|
||||
{
|
||||
public class IconViewModel
|
||||
|
@ -1,104 +1,22 @@
|
||||
@using NBitcoin.DataEncoders
|
||||
@using NBitcoin
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@model BTCPayServer.Components.LabelManager.LabelViewModel
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
|
||||
@{
|
||||
var commonCall = Model.ObjectId.Type + Model.ObjectId.Id;
|
||||
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
var fetchUrl = Url.Action("GetLabels", "UIWallets", new {
|
||||
walletId = Model.WalletObjectId.WalletId,
|
||||
excludeTypes = Safe.Json(Model.ExcludeTypes)
|
||||
});
|
||||
var updateUrl = Model.AutoUpdate? Url.Action("UpdateLabels", "UIWallets", new {
|
||||
walletId = Model.WalletObjectId.WalletId
|
||||
}): string.Empty;
|
||||
}
|
||||
|
||||
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="~/vendor/tom-select/tom-select.complete.min.js"></script>
|
||||
<script>
|
||||
const updateUrl = @Safe.Json(Url.Action("UpdateLabels", "UIWallets", new {
|
||||
Model.ObjectId.WalletId
|
||||
}));
|
||||
const getUrl = @Safe.Json(@Url.Action("GetLabels", "UIWallets", new {
|
||||
walletId = Model.ObjectId.WalletId,
|
||||
excludeTypes = true
|
||||
}));
|
||||
const commonCall = @Safe.Json(commonCall);
|
||||
const elementId = @Safe.Json(elementId);
|
||||
if (!window[commonCall]) {
|
||||
window[commonCall] = fetch(getUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(response => {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const element = document.querySelector(`#${elementId}`);
|
||||
|
||||
if (element) {
|
||||
const labelsFetchTask = await window[commonCall];
|
||||
const config = {
|
||||
create: true,
|
||||
items: @Safe.Json(Model.SelectedLabels),
|
||||
options: labelsFetchTask,
|
||||
valueField: "label",
|
||||
labelField: "label",
|
||||
searchField: "label",
|
||||
allowEmptyOption: false,
|
||||
closeAfterSelect: false,
|
||||
persist: true,
|
||||
render: {
|
||||
option: function(data, escape) {
|
||||
return `<div ${data.color? `style='background-color:${data.color}; color:${data.textColor}'`: ""}>${escape(data.label)}</div>`;
|
||||
},
|
||||
item: function(data, escape) {
|
||||
return `<div ${data.color? `style='background-color:${data.color}; color:${data.textColor}'`: ""}>${escape(data.label)}</div>`;
|
||||
}
|
||||
},
|
||||
onItemAdd: (val) => {
|
||||
window[commonCall] = window[commonCall].then(labels => {
|
||||
return [...labels, { label: val }]
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CustomEvent(`${commonCall}-option-added`, {
|
||||
detail: val
|
||||
}));
|
||||
},
|
||||
onChange: async (values) => {
|
||||
select.lock();
|
||||
try {
|
||||
const response = await fetch(updateUrl, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
address: @Safe.Json(Model.ObjectId.Id),
|
||||
labels: select.items
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not OK');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There has been a problem with your fetch operation:', error);
|
||||
} finally {
|
||||
select.unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
const select = new TomSelect(element, config);
|
||||
|
||||
document.addEventListener(`${commonCall}-option-added`, evt => {
|
||||
if (!(evt.detail in select.options)) {
|
||||
select.addOption({
|
||||
label: evt.detail
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<input id="@elementId" placeholder="Select labels to associate with this object" autocomplete="off" class="form-control label-manager"/>
|
||||
<input id="@elementId" placeholder="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"
|
||||
data-wallet-id="@Model.WalletObjectId.WalletId"
|
||||
data-wallet-object-id="@Model.WalletObjectId.Id"
|
||||
data-wallet-object-type="@Model.WalletObjectId.Type"
|
||||
data-select-element="@Model.SelectElement"
|
||||
data-labels='@Safe.Json(Model.RichLabelInfo)' />
|
||||
|
@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -5,14 +7,25 @@ namespace BTCPayServer.Components.LabelManager
|
||||
{
|
||||
public class LabelManager : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels)
|
||||
public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels, bool excludeTypes = true, bool displayInline = false, Dictionary<string, RichLabelInfo> richLabelInfo = null, bool autoUpdate = true, string selectElement = null)
|
||||
{
|
||||
var vm = new LabelViewModel
|
||||
{
|
||||
ObjectId = walletObjectId,
|
||||
SelectedLabels = selectedLabels
|
||||
ExcludeTypes = excludeTypes,
|
||||
WalletObjectId = walletObjectId,
|
||||
SelectedLabels = selectedLabels?? Array.Empty<string>(),
|
||||
DisplayInline = displayInline,
|
||||
RichLabelInfo = richLabelInfo,
|
||||
AutoUpdate = autoUpdate,
|
||||
SelectElement = selectElement
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
public class RichLabelInfo
|
||||
{
|
||||
public string Link { get; set; }
|
||||
public string Tooltip { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Components.LabelManager
|
||||
@ -5,6 +6,11 @@ namespace BTCPayServer.Components.LabelManager
|
||||
public class LabelViewModel
|
||||
{
|
||||
public string[] SelectedLabels { get; set; }
|
||||
public WalletObjectId ObjectId { get; set; }
|
||||
public WalletObjectId WalletObjectId { get; set; }
|
||||
public bool ExcludeTypes { get; set; }
|
||||
public bool DisplayInline { get; set; }
|
||||
public Dictionary<string, RichLabelInfo> RichLabelInfo { get; set; }
|
||||
public bool AutoUpdate { get; set; }
|
||||
public string SelectElement { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -155,30 +155,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<header class="accordion-header" id="Nav-Apps-Header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#Nav-Apps" aria-expanded="true" aria-controls="Nav-Apps">
|
||||
Apps
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
</header>
|
||||
<div id="Nav-Apps" class="accordion-collapse collapse show" aria-labelledby="Nav-Apps-Header">
|
||||
<div class="accordion-body">
|
||||
<ul class="navbar-nav">
|
||||
@foreach (var app in Model.Apps)
|
||||
{
|
||||
<vc:ui-extension-point location="apps-nav" model="@app"/>
|
||||
}
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Create)" id="StoreNav-CreateApp">
|
||||
<vc:icon symbol="new"/>
|
||||
<span>New App</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="accordion-item">
|
||||
<header class="accordion-header" id="Nav-Plugins-Header">
|
||||
@ -195,9 +171,11 @@
|
||||
{
|
||||
<vc:ui-extension-point location="store-integrations-nav" model="@Model"/>
|
||||
}
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
|
||||
<vc:icon symbol="new"/>
|
||||
<vc:icon symbol="plugin"/>
|
||||
<span>Manage Plugins</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -74,7 +74,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
Id = a.Id,
|
||||
IsOwner = a.IsOwner,
|
||||
AppName = a.AppName,
|
||||
AppType = Enum.Parse<AppType>(a.AppType)
|
||||
AppType = a.AppType
|
||||
}).ToList();
|
||||
|
||||
if (PoliciesSettings.Experimental)
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public AppType AppType { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
||||
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
||||
"invoice_confirmed" => "notifications-invoice-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
||||
"external-payout-transaction" => "notifications-payout",
|
||||
"payout_awaitingapproval" => "notifications-payout",
|
||||
"payout_awaitingpayment" => "notifications-payout-approved",
|
||||
|
@ -6,51 +6,53 @@
|
||||
|
||||
@if (Model.Total is null || Model.Total.Value > pageSizeOptions.Min())
|
||||
{
|
||||
<nav aria-label="..." class="w-100 clearfix">
|
||||
<nav aria-label="..." class="d-flex flex-wrap gap-3 justify-content-between">
|
||||
@if (Model.Total is null || Model.Total.Value > Model.Count)
|
||||
{
|
||||
<ul class="pagination float-start">
|
||||
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
|
||||
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">«</a>
|
||||
</li>
|
||||
<ul class="pagination me-auto">
|
||||
@if (Model.Skip > 0)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">Prev</a>
|
||||
</li>
|
||||
}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link @(Model.Total is null ? "px-0" : (Model.Skip == 0 ? "ps-0" : null))">
|
||||
@if (Model.Total is null)
|
||||
{
|
||||
<span class="page-link">
|
||||
@(Model.Skip + 1)–@(Model.Skip + Model.Count)
|
||||
</span>
|
||||
}
|
||||
else if (Model.Total.Value <= Model.Count)
|
||||
{
|
||||
<span class="page-link">
|
||||
1–@Model.Total.Value
|
||||
</span>
|
||||
@($"{Model.Skip + 1} – {Model.Skip + Model.Count}")
|
||||
}
|
||||
else if (Model.Total.Value <= Model.Count)
|
||||
{
|
||||
@($"1 – {Model.Total.Value}")
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="page-link">
|
||||
@(Model.Skip + 1)–@(Model.Skip + Model.Count), Total: @Model.Total.Value
|
||||
</span>
|
||||
@($"{Model.Skip + 1} – {Model.Skip + Model.Count}, Total: {Model.Total.Value}")
|
||||
}
|
||||
</span>
|
||||
</li>
|
||||
<li class="page-item @(((Model.Total is null && Model.CurrentPageCount >= Model.Count) || (Model.Total is not null && Model.Total.Value > (Model.Skip + Model.Count))) ? null : "disabled")">
|
||||
<a class="page-link" href="@NavigatePages(1, Model.Count)">»</a>
|
||||
</li>
|
||||
@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>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (Model.Total is null || Model.Total.Value >= pageSizeOptions.Min())
|
||||
{
|
||||
<ul class="pagination float-end">
|
||||
<ul class="pagination ms-auto">
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page Size:</span>
|
||||
<span class="page-link">Page Size</span>
|
||||
</li>
|
||||
@foreach (int pageSize in pageSizeOptions)
|
||||
@foreach (var pageSize in pageSizeOptions)
|
||||
{
|
||||
if (Model.Total is null || Model.Total.Value >= pageSize)
|
||||
{
|
||||
<li class="page-item @(Model.Count == pageSize ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, pageSize)">@pageSize</a>
|
||||
<a class="page-link @(Model.Count != pageSize && pageSize == pageSizeOptions.Max() ? "pe-0" : null)" href="@NavigatePages(0, pageSize)">@pageSize</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
|
||||
|
||||
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
|
||||
@ -63,7 +65,7 @@
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">@invoice.AmountCurrency</td>
|
||||
<td class="text-end">@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
@ -7,7 +7,8 @@ public class StoreRecentInvoiceViewModel
|
||||
{
|
||||
public string InvoiceId { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public string AmountCurrency { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public InvoiceState Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
|
@ -61,7 +61,8 @@ public class StoreRecentInvoices : ViewComponent
|
||||
HasRefund = invoice.Refunds.Any(),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
AmountCurrency = _currencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
Amount = invoice.Price,
|
||||
Currency = invoice.Currency
|
||||
}).ToList();
|
||||
|
||||
return View(vm);
|
||||
|
@ -1,4 +1,6 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Services
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
|
||||
|
||||
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
|
||||
@ -34,6 +36,7 @@
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -43,17 +46,37 @@
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a href="@tx.Link" target="_blank" rel="noreferrer noopener" class="text-break">
|
||||
@tx.Id
|
||||
</a>
|
||||
<vc:truncate-center text="@tx.Id" link="tx.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td>
|
||||
@if (tx.Labels.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in tx.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||
<span>@label.Text</span>
|
||||
@if (!string.IsNullOrEmpty(label.Link))
|
||||
{
|
||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">@tx.Balance</td>
|
||||
<td class="text-end text-success">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">@tx.Balance</td>
|
||||
<td class="text-end text-danger">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
public class StoreRecentTransactionViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public bool IsConfirmed { get; set; }
|
||||
public string Link { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public IEnumerable<TransactionTagModel> Labels { get; set; }
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
@ -23,14 +24,20 @@ namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
public class StoreRecentTransactions : ViewComponent
|
||||
{
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly WalletRepository _walletRepository;
|
||||
private readonly LabelService _labelService;
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
|
||||
public StoreRecentTransactions(
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayWalletProvider walletProvider)
|
||||
BTCPayWalletProvider walletProvider,
|
||||
WalletRepository walletRepository,
|
||||
LabelService labelService)
|
||||
{
|
||||
NetworkProvider = networkProvider;
|
||||
_walletProvider = walletProvider;
|
||||
_walletRepository = walletRepository;
|
||||
_labelService = labelService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreRecentTransactionsViewModel vm)
|
||||
@ -52,15 +59,25 @@ public class StoreRecentTransactions : ViewComponent
|
||||
var network = derivationSettings.Network;
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0));
|
||||
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo( vm.WalletId , allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
|
||||
transactions = allTransactions
|
||||
.Select(tx => new StoreRecentTransactionViewModel
|
||||
.Select(tx =>
|
||||
{
|
||||
Id = tx.TransactionId.ToString(),
|
||||
Positive = tx.BalanceChange.GetValue(network) >= 0,
|
||||
Balance = tx.BalanceChange.ShowMoney(network),
|
||||
IsConfirmed = tx.Confirmations != 0,
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, tx.TransactionId.ToString()),
|
||||
Timestamp = tx.SeenAt
|
||||
walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo);
|
||||
var labels = _labelService.CreateTransactionTagModels(transactionInfo, Request);
|
||||
return new StoreRecentTransactionViewModel
|
||||
{
|
||||
Id = tx.TransactionId.ToString(),
|
||||
Positive = tx.BalanceChange.GetValue(network) >= 0,
|
||||
Balance = tx.BalanceChange.ShowMoney(network),
|
||||
Currency = vm.CryptoCode,
|
||||
IsConfirmed = tx.Confirmations != 0,
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
|
||||
tx.TransactionId.ToString()),
|
||||
Timestamp = tx.SeenAt,
|
||||
Labels = labels
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
17
BTCPayServer/Components/TruncateCenter/Default.cshtml
Normal file
17
BTCPayServer/Components/TruncateCenter/Default.cshtml
Normal file
@ -0,0 +1,17 @@
|
||||
@model BTCPayServer.Components.TruncateCenter.TruncateCenterViewModel
|
||||
<span class="truncate-center @Model.Classes">
|
||||
<span class="truncate-center-truncated" @(Model.Truncated != Model.Text ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>@Model.Truncated</span>
|
||||
<span class="truncate-center-text">@Model.Text</span>
|
||||
@if (Model.Copy)
|
||||
{
|
||||
<button type="button" class="btn btn-link p-0" data-clipboard="@Model.Text">
|
||||
<vc:icon symbol="copy" />
|
||||
</button>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Link))
|
||||
{
|
||||
<a href="@Model.Link" rel="noreferrer noopener" target="_blank">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</span>
|
29
BTCPayServer/Components/TruncateCenter/TruncateCenter.cs
Normal file
29
BTCPayServer/Components/TruncateCenter/TruncateCenter.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.TruncateCenter;
|
||||
|
||||
/// <summary>
|
||||
/// Truncates long strings in the center with ellipsis: Turns e.g. a BOLT11 into "lnbcrt7…q2ns60y"
|
||||
/// </summary>
|
||||
/// <param name="text">The full text, e.g. a Bitcoin address or BOLT11</param>
|
||||
/// <param name="link">Optional link, e.g. a block explorer URL</param>
|
||||
/// <param name="classes">Optional additional CSS classes</param>
|
||||
/// <param name="padding">The number of characters to show on each side</param>
|
||||
/// <param name="copy">Display a copy button</param>
|
||||
/// <returns>HTML with truncated string</returns>
|
||||
public class TruncateCenter : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true)
|
||||
{
|
||||
var vm = new TruncateCenterViewModel
|
||||
{
|
||||
Classes = classes,
|
||||
Padding = padding,
|
||||
Copy = copy,
|
||||
Text = text,
|
||||
Link = link,
|
||||
Truncated = text.Length > 2 * padding ? $"{text[..padding]}…{text[^padding..]}" : text
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
namespace BTCPayServer.Components.TruncateCenter
|
||||
{
|
||||
public class TruncateCenterViewModel
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public string Truncated { get; set; }
|
||||
public string Classes { get; set; }
|
||||
public string Link { get; set; }
|
||||
public int Padding { get; set; }
|
||||
public bool Copy { get; set; }
|
||||
}
|
||||
}
|
@ -67,11 +67,21 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
if (conf.GetOrDefault<string>("POSTGRES", null) == null)
|
||||
{
|
||||
|
||||
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
|
||||
Logs.Configuration.LogWarning("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md");
|
||||
if (conf.GetOrDefault<string>("MYSQL", null) != null)
|
||||
Logs.Configuration.LogWarning("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md)");
|
||||
var allowDeprecated = conf.GetOrDefault<bool>("DEPRECATED", false);
|
||||
if (allowDeprecated)
|
||||
{
|
||||
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
|
||||
Logs.Configuration.LogWarning("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md");
|
||||
if (conf.GetOrDefault<string>("MYSQL", null) != null)
|
||||
Logs.Configuration.LogWarning("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md)");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
|
||||
throw new ConfigException("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md). If you don't want to update, you can try to start this instance by using the command line argument --deprecated");
|
||||
if (conf.GetOrDefault<string>("MYSQL", null) != null)
|
||||
throw new ConfigException("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md). If you don't want to update, you can try to start this instance by using the command line argument --deprecated");
|
||||
}
|
||||
}
|
||||
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
@ -148,9 +158,6 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
|
||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||
var pluginRemote = conf.GetOrDefault<string>("plugin-remote", null);
|
||||
if (pluginRemote != null)
|
||||
Logs.Configuration.LogWarning("plugin-remote is an obsolete configuration setting, please remove it from configuration");
|
||||
RecommendedPlugins = conf.GetOrDefault("recommended-plugins", "").ToLowerInvariant().Split('\r', '\n', '\t', ' ').Where(s => !string.IsNullOrEmpty(s)).Distinct().ToArray();
|
||||
CheatMode = conf.GetOrDefault("cheatmode", false);
|
||||
if (CheatMode && this.NetworkType == ChainName.Mainnet)
|
||||
|
@ -30,6 +30,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--mysql", $"DEPRECATED: Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
|
||||
app.Option("--sqlitefile", $"DEPRECATED: File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--deprecated", $"Allow deprecated settings (default:false)", CommandOptionType.BoolValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
|
||||
@ -45,7 +46,6 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
app.Option("--plugin-remote", "Obsolete, do not use", CommandOptionType.SingleValue);
|
||||
app.Option("--recommended-plugins", "Plugins which would be marked as recommended to be installed. Separated by newline or space", CommandOptionType.MultipleValue);
|
||||
app.Option("--xforwardedproto", "If specified, set X-Forwarded-Proto to the specified value, this may be useful if your reverse proxy handle https but is not configured to add X-Forwarded-Proto (example: --xforwardedproto https)", CommandOptionType.SingleValue);
|
||||
app.Option("--cheatmode", "Add some helper UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
|
||||
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
@ -49,12 +50,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return CreateUserAPIKey(_userManager.GetUserId(User), request);
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/users/{userId}/api-keys")]
|
||||
[HttpPost("~/api/v1/users/{idOrEmail}/api-keys")]
|
||||
[Authorize(Policy = Policies.CanManageUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreateUserAPIKey(string userId, CreateApiKeyRequest request)
|
||||
public async Task<IActionResult> CreateUserAPIKey(string idOrEmail, CreateApiKeyRequest request)
|
||||
{
|
||||
request ??= new CreateApiKeyRequest();
|
||||
request.Permissions ??= System.Array.Empty<Permission>();
|
||||
|
||||
var userId = (await _userManager.FindByIdOrEmail(idOrEmail))?.Id;
|
||||
if (userId is null)
|
||||
return this.UserNotFound();
|
||||
var key = new APIKeyData()
|
||||
{
|
||||
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
|
||||
@ -88,10 +93,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return RevokeAPIKey(_userManager.GetUserId(User), apikey);
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/users/{userId}/api-keys/{apikey}", Order = 1)]
|
||||
[HttpDelete("~/api/v1/users/{idOrEmail}/api-keys/{apikey}", Order = 1)]
|
||||
[Authorize(Policy = Policies.CanManageUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> RevokeAPIKey(string userId, string apikey)
|
||||
public async Task<IActionResult> RevokeAPIKey(string idOrEmail, string apikey)
|
||||
{
|
||||
var userId = (await _userManager.FindByIdOrEmail(idOrEmail))?.Id;
|
||||
if (userId is null)
|
||||
return this.UserNotFound();
|
||||
if (!string.IsNullOrEmpty(apikey) &&
|
||||
await _apiKeyRepository.Remove(apikey, userId))
|
||||
return Ok();
|
||||
|
@ -7,6 +7,8 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -15,6 +17,7 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
@ -63,7 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Name = request.AppName,
|
||||
AppType = AppType.Crowdfund.ToString()
|
||||
AppType = CrowdfundAppType.AppType
|
||||
};
|
||||
|
||||
appData.SetSettings(ToCrowdfundSettings(request));
|
||||
@ -94,7 +97,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Name = request.AppName,
|
||||
AppType = AppType.PointOfSale.ToString()
|
||||
AppType = PointOfSaleAppType.AppType
|
||||
};
|
||||
|
||||
appData.SetSettings(ToPointOfSaleSettings(request));
|
||||
@ -108,7 +111,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -181,7 +184,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetPosApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -194,7 +197,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetCrowdfundApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, AppType.Crowdfund);
|
||||
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -242,7 +245,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
|
||||
NotificationUrl = request.NotificationUrl?.Trim(),
|
||||
Tagline = request.Tagline?.Trim(),
|
||||
PerksTemplate = request.PerksTemplate != null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate?.Trim(), request.TargetCurrency)) : null,
|
||||
PerksTemplate = request.PerksTemplate is not null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate.Trim(), request.TargetCurrency!)) : null,
|
||||
// If Disqus shortname is not null or empty we assume that Disqus should be enabled
|
||||
DisqusEnabled = !string.IsNullOrEmpty(request.DisqusShortname?.Trim()),
|
||||
DisqusShortname = request.DisqusShortname?.Trim(),
|
||||
@ -264,7 +267,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return new PointOfSaleSettings()
|
||||
{
|
||||
Title = request.Title,
|
||||
DefaultView = (Services.Apps.PosViewType)request.DefaultView,
|
||||
DefaultView = (PosViewType) request.DefaultView,
|
||||
ShowCustomAmount = request.ShowCustomAmount,
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
EnableTips = request.EnableTips,
|
||||
@ -360,7 +363,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
try
|
||||
{
|
||||
_appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency));
|
||||
// Just checking if we can serialize, we don't care about the currency
|
||||
_appService.SerializeTemplate(_appService.Parse(request.Template, "USD"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -449,7 +453,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
try
|
||||
{
|
||||
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, request.TargetCurrency));
|
||||
// Just checking if we can serialize, we don't care about the currency
|
||||
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, "USD"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Custodian;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
@ -20,6 +21,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CustodianAccountData = BTCPayServer.Data.CustodianAccountData;
|
||||
using CustodianAccountDataClient = BTCPayServer.Client.Models.CustodianAccountData;
|
||||
@ -221,6 +223,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(paymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var result = await depositableCustodian.GetDepositAddressAsync(paymentMethod, config, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
@ -247,26 +255,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (custodian is ICanTrade tradableCustodian)
|
||||
{
|
||||
bool isPercentage = request.Qty.EndsWith("%", StringComparison.InvariantCultureIgnoreCase);
|
||||
string qtyString = isPercentage ? request.Qty.Substring(0, request.Qty.Length - 1) : request.Qty;
|
||||
bool canParseQty = Decimal.TryParse(qtyString, out decimal qty);
|
||||
if (!canParseQty)
|
||||
decimal qty;
|
||||
try
|
||||
{
|
||||
return this.CreateAPIError(400, "bad-qty-format",
|
||||
$"Quantity should be a number or a number ending with '%' for percentages.");
|
||||
qty = await ParseQty(request.Qty, request.FromAsset, custodianAccount, custodian, cancellationToken);
|
||||
}
|
||||
|
||||
if (isPercentage)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Percentage of current holdings => calculate the amount
|
||||
var config = custodianAccount.GetBlob();
|
||||
var balances = custodian.GetAssetBalancesAsync(config, cancellationToken).Result;
|
||||
var fromAssetBalance = balances[request.FromAsset];
|
||||
var priceQuote =
|
||||
await tradableCustodian.GetQuoteForAssetAsync(request.FromAsset, request.ToAsset, config, cancellationToken);
|
||||
qty = fromAssetBalance / priceQuote.Ask * qty / 100;
|
||||
return UnsupportedAsset(request.FromAsset, ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await tradableCustodian.TradeMarketAsync(request.FromAsset, request.ToAsset, qty,
|
||||
@ -338,6 +335,44 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
$"Fetching past trade info on \"{custodian.Name}\" is not supported.");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation")]
|
||||
[Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> SimulateWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var custodianAccount = await GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(request.PaymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var asset = pm.CryptoCode;
|
||||
decimal qty;
|
||||
try
|
||||
{
|
||||
qty = await ParseQty(request.Qty, asset, custodianAccount, custodian, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return UnsupportedAsset(asset, ex.Message);
|
||||
}
|
||||
|
||||
var simulateWithdrawResult =
|
||||
await withdrawableCustodian.SimulateWithdrawalAsync(request.PaymentMethod, qty, custodianAccount.GetBlob(), cancellationToken);
|
||||
var result = new WithdrawalSimulationResponseData(simulateWithdrawResult.PaymentMethod, simulateWithdrawResult.Asset,
|
||||
accountId, custodian.Code, simulateWithdrawResult.LedgerEntries, simulateWithdrawResult.MinQty, simulateWithdrawResult.MaxQty);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "withdrawals-not-supported",
|
||||
$"Withdrawals are not supported for \"{custodian.Name}\".");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals")]
|
||||
[Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts,
|
||||
@ -350,8 +385,25 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(request.PaymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var asset = pm.CryptoCode;
|
||||
decimal qty;
|
||||
try
|
||||
{
|
||||
qty = await ParseQty(request.Qty, asset, custodianAccount, custodian, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return UnsupportedAsset(asset, ex.Message);
|
||||
}
|
||||
|
||||
var withdrawResult =
|
||||
await withdrawableCustodian.WithdrawAsync(request.PaymentMethod, request.Qty, custodianAccount.GetBlob(), cancellationToken);
|
||||
await withdrawableCustodian.WithdrawToStoreWalletAsync(request.PaymentMethod, qty, custodianAccount.GetBlob(), cancellationToken);
|
||||
var result = new WithdrawalResponseData(withdrawResult.PaymentMethod, withdrawResult.Asset, withdrawResult.LedgerEntries,
|
||||
withdrawResult.WithdrawalId, accountId, custodian.Code, withdrawResult.Status, withdrawResult.CreatedTime, withdrawResult.TargetAddress, withdrawResult.TransactionId);
|
||||
return Ok(result);
|
||||
@ -361,6 +413,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
$"Withdrawals are not supported for \"{custodian.Name}\".");
|
||||
}
|
||||
|
||||
private IActionResult UnsupportedAsset(string asset, string err)
|
||||
{
|
||||
return this.CreateAPIError(400, "invalid-qty", $"It is impossible to use % quantity with this asset ({err})");
|
||||
}
|
||||
|
||||
private async Task<decimal> ParseQty(TradeQuantity qty, string asset, CustodianAccountData custodianAccount, ICustodian custodian, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (qty.Type == TradeQuantity.ValueType.Exact)
|
||||
return qty.Value;
|
||||
// Percentage of current holdings => calculate the amount
|
||||
var config = custodianAccount.GetBlob();
|
||||
var balances = await custodian.GetAssetBalancesAsync(config, cancellationToken);
|
||||
if (!balances.TryGetValue(asset, out var assetBalance))
|
||||
return 0.0m;
|
||||
return (assetBalance * qty.Value) / 100m;
|
||||
}
|
||||
|
||||
async Task<CustodianAccountData> GetCustodianAccount(string storeId, string accountId)
|
||||
{
|
||||
|
@ -256,7 +256,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
return result.Result switch
|
||||
{
|
||||
PayResult.CouldNotFindRoute => this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer"),
|
||||
PayResult.CouldNotFindRoute => this.CreateAPIError("could-not-find-route", result.ErrorDetail ?? "Impossible to find a route to the peer"),
|
||||
PayResult.Error => this.CreateAPIError("generic-error", result.ErrorDetail),
|
||||
PayResult.Unknown => Accepted(new LightningPaymentData
|
||||
{
|
||||
@ -287,7 +287,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
var param = new ListInvoicesParams { PendingOnly = pendingOnly, OffsetIndex = offsetIndex };
|
||||
var invoices = await lightningClient.ListInvoices(param, cancellationToken);
|
||||
return Ok(invoices.Select(ToModel));
|
||||
return Ok(invoices.Select(ToModel).ToArray());
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetPayments(string cryptoCode, [FromQuery] bool? includePending, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
|
||||
@ -295,7 +295,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
var param = new ListPaymentsParams { IncludePending = includePending, OffsetIndex = offsetIndex };
|
||||
var payments = await lightningClient.ListPayments(param, cancellationToken);
|
||||
return Ok(payments.Select(ToModel));
|
||||
return Ok(payments.Select(ToModel).ToArray());
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default)
|
||||
|
@ -9,6 +9,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
using PayoutProcessorData = BTCPayServer.Client.Models.PayoutProcessorData;
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldPayoutProcessorsController : ControllerBase
|
||||
{
|
||||
private readonly IEnumerable<IPayoutProcessorFactory> _factories;
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
||||
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreAutomatedLightningPayoutProcessorsController : ControllerBase
|
||||
{
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
@ -30,9 +32,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory))]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory) +
|
||||
"/{paymentMethod}")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}")]
|
||||
public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors(
|
||||
string storeId, string? paymentMethod)
|
||||
{
|
||||
@ -64,8 +65,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory) +
|
||||
"/{paymentMethod}")]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}")]
|
||||
public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor(
|
||||
string storeId, string paymentMethod, LightningAutomatedPayoutSettings request)
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.OnChain;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
||||
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreAutomatedOnChainPayoutProcessorsController : ControllerBase
|
||||
{
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
@ -30,9 +32,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory))]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory) +
|
||||
"/{paymentMethod}")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/OnChainAutomatedPayoutSenderFactory")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/OnChainAutomatedPayoutSenderFactory/{paymentMethod}")]
|
||||
public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors(
|
||||
string storeId, string? paymentMethod)
|
||||
{
|
||||
@ -70,8 +71,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory) +
|
||||
"/{paymentMethod}")]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/OnChainAutomatedPayoutSenderFactory/{paymentMethod}")]
|
||||
public async Task<IActionResult> UpdateStoreOnchainAutomatedPayoutProcessor(
|
||||
string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request)
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -25,24 +26,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreLNURLPayPaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ISettingsRepository _settingsRepository;
|
||||
|
||||
public GreenfieldStoreLNURLPayPaymentMethodsController(
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
ISettingsRepository settingsRepository)
|
||||
BTCPayNetworkProvider btcPayNetworkProvider)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_authorizationService = authorizationService;
|
||||
_settingsRepository = settingsRepository;
|
||||
}
|
||||
|
||||
public static IEnumerable<LNURLPayPaymentMethodData> GetLNURLPayPaymentMethods(StoreData store,
|
||||
|
@ -6,6 +6,7 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
|
||||
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
|
||||
@ -14,6 +15,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreLightningAddressesController : ControllerBase
|
||||
{
|
||||
private readonly LightningAddressService _lightningAddressService;
|
||||
|
@ -18,6 +18,7 @@ using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -26,6 +27,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreLightningNetworkPaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
|
@ -8,6 +8,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.Models;
|
||||
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate")]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> GenerateOnChainWallet(string storeId, string cryptoCode,
|
||||
GenerateWalletRequest request)
|
||||
{
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -24,6 +25,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public partial class GreenfieldStoreOnChainPaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
|
@ -8,6 +8,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
@ -15,6 +16,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStorePaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
|
@ -7,12 +7,14 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStorePayoutProcessorsController : ControllerBase
|
||||
{
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RateSource = BTCPayServer.Client.Models.RateSource;
|
||||
|
||||
@ -21,6 +22,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
[ApiController]
|
||||
[Route("api/v1/stores/{storeId}/rates/configuration")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreRateConfigurationController : ControllerBase
|
||||
{
|
||||
private readonly RateFetcher _rateProviderFactory;
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
@ -18,6 +19,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
[ApiController]
|
||||
[Route("api/v1/stores/{storeId}/rates")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreRatesController : ControllerBase
|
||||
{
|
||||
private readonly RateFetcher _rateProviderFactory;
|
||||
|
@ -20,10 +20,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public class GreenfieldStoreUsersController : ControllerBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public GreenfieldStoreUsersController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
}
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/users")]
|
||||
@ -34,8 +36,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return store == null ? StoreNotFound() : Ok(FromModel(store));
|
||||
}
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/users/{userId}")]
|
||||
public async Task<IActionResult> RemoveStoreUser(string storeId, string userId)
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/users/{idOrEmail}")]
|
||||
public async Task<IActionResult> RemoveStoreUser(string storeId, string idOrEmail)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
@ -43,9 +45,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return StoreNotFound();
|
||||
}
|
||||
|
||||
if (await _storeRepository.RemoveStoreUser(storeId, userId))
|
||||
var userId = await _userManager.FindByIdOrEmail(idOrEmail);
|
||||
if (userId != null && await _storeRepository.RemoveStoreUser(storeId, idOrEmail))
|
||||
{
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return Ok(FromModel(store));
|
||||
}
|
||||
|
||||
private Client.Models.StoreData FromModel(Data.StoreData data)
|
||||
internal static Client.Models.StoreData FromModel(Data.StoreData data)
|
||||
{
|
||||
var storeBlob = data.GetStoreBlob();
|
||||
return new Client.Models.StoreData()
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
@ -5,6 +6,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -16,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Route("api/test/apikey")]
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldTestApiKeyController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
@ -52,9 +55,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
[HttpGet("me/stores")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public StoreData[] GetCurrentUserStores()
|
||||
public BTCPayServer.Client.Models.StoreData[] GetCurrentUserStores()
|
||||
{
|
||||
return this.HttpContext.GetStoresData();
|
||||
return this.HttpContext.GetStoresData().Select(Greenfield.GreenfieldStoresController.FromModel).ToArray();
|
||||
}
|
||||
|
||||
[HttpGet("me/stores/{storeId}/can-view")]
|
||||
|
@ -69,22 +69,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpGet("~/api/v1/users/{idOrEmail}")]
|
||||
public async Task<IActionResult> GetUser(string idOrEmail)
|
||||
{
|
||||
var user = (await _userManager.FindByIdAsync(idOrEmail)) ?? await _userManager.FindByEmailAsync(idOrEmail);
|
||||
var user = await _userManager.FindByIdOrEmail(idOrEmail);
|
||||
if (user != null)
|
||||
{
|
||||
return Ok(await FromModel(user));
|
||||
}
|
||||
return UserNotFound();
|
||||
return this.UserNotFound();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/users/{idOrEmail}/lock")]
|
||||
public async Task<IActionResult> LockUser(string idOrEmail, LockUserRequest request)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(idOrEmail) ?? await _userManager.FindByEmailAsync(idOrEmail);
|
||||
var user = await _userManager.FindByIdOrEmail(idOrEmail);
|
||||
if (user is null)
|
||||
{
|
||||
return UserNotFound();
|
||||
return this.UserNotFound();
|
||||
}
|
||||
|
||||
var success = await _userService.ToggleUser(user.Id, request.Locked ? DateTimeOffset.MaxValue : null);
|
||||
@ -223,7 +223,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return UserNotFound();
|
||||
return this.UserNotFound();
|
||||
}
|
||||
|
||||
// We can safely delete the user if it's not an admin user
|
||||
@ -251,12 +251,5 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var roles = (await _userManager.GetRolesAsync(data)).ToArray();
|
||||
return UserService.FromModel(data, roles);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private IActionResult UserNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "user-not-found", "The user was not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -223,6 +223,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return GetFromActionResult<MarketTradeResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().MarketTradeCustodianAccountAsset(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<WithdrawalSimulationResponseData> SimulateCustodianAccountWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<WithdrawalSimulationResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().SimulateWithdrawal(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<WithdrawalResponseData> CreateCustodianAccountWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<WithdrawalResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().CreateWithdrawal(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode,
|
||||
GetWalletObjectsRequest query = null,
|
||||
|
@ -7,10 +7,12 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
@ -83,6 +85,24 @@ namespace BTCPayServer.Controllers
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet("/cheat/permissions")]
|
||||
[HttpGet("/cheat/permissions/stores/{storeId}")]
|
||||
[CheatModeRoute]
|
||||
public async Task<IActionResult> CheatPermissions([FromServices]IAuthorizationService authorizationService, string storeId = null)
|
||||
{
|
||||
var vm = new CheatPermissionsViewModel();
|
||||
vm.StoreId = storeId;
|
||||
var results = new System.Collections.Generic.List<(string, Task<AuthorizationResult>)>();
|
||||
foreach (var p in Policies.AllPolicies.Concat(new[] { Policies.CanModifyStoreSettingsUnscoped }))
|
||||
{
|
||||
results.Add((p, authorizationService.AuthorizeAsync(User, storeId, p)));
|
||||
}
|
||||
await Task.WhenAll(results.Select(r => r.Item2));
|
||||
results = results.OrderBy(r => r.Item1).ToList();
|
||||
vm.Permissions = results.Select(r => (r.Item1, r.Item2.Result)).ToArray();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("/login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
|
||||
|
@ -21,8 +21,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
app.StoreData = GetCurrentStore();
|
||||
|
||||
var vm = new AppTopItemsViewModel { App = app };
|
||||
return ViewComponent("AppTopItems", new { vm });
|
||||
return ViewComponent("AppTopItems", new { appId = app.Id, appType = app.AppType });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -34,9 +33,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
app.StoreData = GetCurrentStore();
|
||||
|
||||
var vm = new AppSalesViewModel { App = app };
|
||||
return ViewComponent("AppSales", new { vm });
|
||||
return ViewComponent("AppSales", new { appId = app.Id, appType = app.AppType });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -6,8 +5,6 @@ using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -57,13 +54,14 @@ namespace BTCPayServer.Controllers
|
||||
var app = await _appService.GetApp(appId, null);
|
||||
if (app is null)
|
||||
return NotFound();
|
||||
|
||||
return app.AppType switch
|
||||
|
||||
var res = await _appService.ViewLink(app);
|
||||
if (res is null)
|
||||
{
|
||||
nameof(AppType.Crowdfund) => RedirectToAction(nameof(UICrowdfundController.ViewCrowdfund), "UICrowdfund", new { appId }),
|
||||
nameof(AppType.PointOfSale) => RedirectToAction(nameof(UIPointOfSaleController.ViewPointOfSale), "UIPointOfSale", new { appId }),
|
||||
_ => NotFound()
|
||||
};
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Redirect(res);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -113,24 +111,29 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("/stores/{storeId}/apps/create")]
|
||||
public IActionResult CreateApp(string storeId)
|
||||
[HttpGet("/stores/{storeId}/apps/create/{appType?}")]
|
||||
public IActionResult CreateApp(string storeId, string appType = null)
|
||||
{
|
||||
return View(new CreateAppViewModel
|
||||
var vm = new CreateAppViewModel(_appService)
|
||||
{
|
||||
StoreId = GetCurrentStore().Id
|
||||
});
|
||||
StoreId = storeId,
|
||||
AppType = appType,
|
||||
SelectedAppType = appType
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("/stores/{storeId}/apps/create")]
|
||||
[HttpPost("/stores/{storeId}/apps/create/{appType?}")]
|
||||
public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
vm.StoreId = store.Id;
|
||||
|
||||
if (!Enum.TryParse(vm.SelectedAppType, out AppType appType))
|
||||
var type = _appService.GetAppType(vm.AppType ?? vm.SelectedAppType);
|
||||
if (type is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -141,34 +144,19 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
StoreDataId = store.Id,
|
||||
Name = vm.AppName,
|
||||
AppType = appType.ToString()
|
||||
AppType = type!.Type
|
||||
};
|
||||
|
||||
var defaultCurrency = await GetStoreDefaultCurrentIfEmpty(appData.StoreDataId, null);
|
||||
switch (appType)
|
||||
{
|
||||
case AppType.Crowdfund:
|
||||
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
|
||||
appData.SetSettings(emptyCrowdfund);
|
||||
break;
|
||||
case AppType.PointOfSale:
|
||||
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
|
||||
appData.SetSettings(empty);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
await _appService.SetDefaultSettings(appData, defaultCurrency);
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
|
||||
CreatedAppId = appData.Id;
|
||||
|
||||
return appType switch
|
||||
{
|
||||
AppType.PointOfSale => RedirectToAction(nameof(UIPointOfSaleController.UpdatePointOfSale), "UIPointOfSale", new { appId = appData.Id }),
|
||||
AppType.Crowdfund => RedirectToAction(nameof(UICrowdfundController.UpdateCrowdfund), "UICrowdfund", new { appId = appData.Id }),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
var url = await type.ConfigureLink(appData);
|
||||
return Redirect(url);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
|
@ -11,8 +11,10 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.Models.CustodianAccountViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -20,6 +22,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog.Config;
|
||||
using CustodianAccountData = BTCPayServer.Data.CustodianAccountData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
@ -32,27 +35,30 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly IEnumerable<ICustodian> _custodianRegistry;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly BTCPayServerClient _btcPayServerClient;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly FormDataService _formDataService;
|
||||
|
||||
public UICustodianAccountsController(
|
||||
CurrencyNameTable currencyNameTable,
|
||||
DisplayFormatter displayFormatter,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
IEnumerable<ICustodian> custodianRegistry,
|
||||
BTCPayServerClient btcPayServerClient,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
LinkGenerator linkGenerator
|
||||
LinkGenerator linkGenerator,
|
||||
FormDataService formDataService
|
||||
)
|
||||
{
|
||||
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_displayFormatter = displayFormatter;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_custodianRegistry = custodianRegistry;
|
||||
_btcPayServerClient = btcPayServerClient;
|
||||
_networkProvider = networkProvider;
|
||||
_linkGenerator = linkGenerator;
|
||||
_formDataService = formDataService;
|
||||
}
|
||||
|
||||
public string CreatedCustodianAccountId { get; set; }
|
||||
@ -144,7 +150,7 @@ namespace BTCPayServer.Controllers
|
||||
if (asset.Equals(defaultCurrency))
|
||||
{
|
||||
assetBalance.FormattedFiatValue =
|
||||
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty, defaultCurrency);
|
||||
_displayFormatter.Currency(pair.Value.Qty, defaultCurrency);
|
||||
assetBalance.FiatValue = pair.Value.Qty;
|
||||
}
|
||||
else
|
||||
@ -156,11 +162,11 @@ namespace BTCPayServer.Controllers
|
||||
assetBalance.Bid = quote.Bid;
|
||||
assetBalance.Ask = quote.Ask;
|
||||
assetBalance.FormattedBid =
|
||||
_currencyNameTable.DisplayFormatCurrency(quote.Bid, quote.FromAsset);
|
||||
_displayFormatter.Currency(quote.Bid, quote.FromAsset);
|
||||
assetBalance.FormattedAsk =
|
||||
_currencyNameTable.DisplayFormatCurrency(quote.Ask, quote.FromAsset);
|
||||
_displayFormatter.Currency(quote.Ask, quote.FromAsset);
|
||||
assetBalance.FormattedFiatValue =
|
||||
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty * quote.Bid,
|
||||
_displayFormatter.Currency(pair.Value.Qty * quote.Bid,
|
||||
defaultCurrency);
|
||||
assetBalance.FiatValue = pair.Value.Qty * quote.Bid;
|
||||
}
|
||||
@ -174,14 +180,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var withdrawableePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods();
|
||||
foreach (var withdrawableePaymentMethod in withdrawableePaymentMethods)
|
||||
var withdrawablePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods();
|
||||
foreach (var withdrawablePaymentMethod in withdrawablePaymentMethods)
|
||||
{
|
||||
var withdrawableAsset = withdrawableePaymentMethod.Split("-")[0];
|
||||
var withdrawableAsset = withdrawablePaymentMethod.Split("-")[0];
|
||||
if (assetBalances.ContainsKey(withdrawableAsset))
|
||||
{
|
||||
var assetBalance = assetBalances[withdrawableAsset];
|
||||
assetBalance.CanWithdraw = true;
|
||||
assetBalance.WithdrawablePaymentMethods.Add(withdrawablePaymentMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -215,11 +221,14 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var configForm = await custodian.GetConfigForm(custodianAccount.GetBlob(), "en-US");
|
||||
var blob = custodianAccount.GetBlob();
|
||||
var configForm = await custodian.GetConfigForm(blob, HttpContext.RequestAborted);
|
||||
configForm.SetValues(blob);
|
||||
|
||||
var vm = new EditCustodianAccountViewModel();
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
vm.ConfigForm = configForm;
|
||||
vm.Config = _formDataService.GetValues(configForm).ToString();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -227,9 +236,6 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> EditCustodianAccount(string storeId, string accountId,
|
||||
EditCustodianAccountViewModel vm)
|
||||
{
|
||||
// The locale is not important yet, but keeping it here so we can find it easily when localization becomes a thing.
|
||||
var locale = "en-US";
|
||||
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
@ -240,41 +246,51 @@ namespace BTCPayServer.Controllers
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
var configForm = await GetNextForm(custodian, vm.Config);
|
||||
|
||||
var configForm = await custodian.GetConfigForm(custodianAccount.GetBlob(), locale);
|
||||
|
||||
var newData = new JObject();
|
||||
foreach (var pair in Request.Form)
|
||||
if (configForm.IsValid())
|
||||
{
|
||||
if ("CustodianAccount.Name".Equals(pair.Key))
|
||||
{
|
||||
custodianAccount.Name = pair.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO support posted array notation, like a field called "WithdrawToAddressNamePerPaymentMethod[BTC-OnChain]". The data should be nested in the JSON.
|
||||
newData.Add(pair.Key, pair.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
var newConfigData = RemoveUnusedFieldsFromConfig(custodianAccount.GetBlob(), newData, configForm);
|
||||
var newConfigForm = await custodian.GetConfigForm(newConfigData, locale);
|
||||
|
||||
if (newConfigForm.IsValid())
|
||||
{
|
||||
custodianAccount.SetBlob(newConfigData);
|
||||
var newData = _formDataService.GetValues(configForm);
|
||||
custodianAccount.SetBlob(newData);
|
||||
custodianAccount.Name = vm.CustodianAccount.Name;
|
||||
custodianAccount = await _custodianAccountRepository.CreateOrUpdate(custodianAccount);
|
||||
|
||||
return RedirectToAction(nameof(ViewCustodianAccount),
|
||||
new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id });
|
||||
}
|
||||
|
||||
// Form not valid: The user must fix the errors before we can save
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
vm.ConfigForm = newConfigForm;
|
||||
vm.ConfigForm = configForm;
|
||||
vm.Config = _formDataService.GetValues(configForm).ToString();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task<Form> GetNextForm(ICustodian custodian, string config)
|
||||
{
|
||||
JObject b = null;
|
||||
try
|
||||
{
|
||||
if (config != null)
|
||||
b = JObject.Parse(config);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
b ??= new JObject();
|
||||
// First, we restore the previous form based on the previous blob that was
|
||||
// stored in config
|
||||
var form = await custodian.GetConfigForm(b, HttpContext.RequestAborted);
|
||||
form.SetValues(b);
|
||||
// Then we apply new values overriding the previous blob from the Form params
|
||||
form.ApplyValuesFromForm(Request.Form);
|
||||
// We extract the new resulting blob, and request what is the next form based on it
|
||||
b = _formDataService.GetValues(form);
|
||||
form = await custodian.GetConfigForm(_formDataService.GetValues(form), HttpContext.RequestAborted);
|
||||
// We set all the values to this blob, and validate the form
|
||||
form.SetValues(b);
|
||||
_formDataService.Validate(form, ModelState);
|
||||
return form;
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/create")]
|
||||
public IActionResult CreateCustodianAccount(string storeId)
|
||||
@ -312,17 +328,12 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
|
||||
|
||||
var configData = new JObject();
|
||||
foreach (var pair in Request.Form)
|
||||
{
|
||||
configData.Add(pair.Key, pair.Value.ToString());
|
||||
}
|
||||
|
||||
var configForm = await custodian.GetConfigForm(configData, "en-US");
|
||||
var configForm = await GetNextForm(custodian, vm.Config);
|
||||
if (configForm.IsValid())
|
||||
{
|
||||
// configForm.removeUnusedKeys();
|
||||
var configData = _formDataService.GetValues(configForm);
|
||||
custodianAccountData.SetBlob(configData);
|
||||
custodianAccountData.Name = vm.Name;
|
||||
custodianAccountData = await _custodianAccountRepository.CreateOrUpdate(custodianAccountData);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Custodian account successfully created";
|
||||
CreatedCustodianAccountId = custodianAccountData.Id;
|
||||
@ -333,6 +344,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
// Ask for more data
|
||||
vm.ConfigForm = configForm;
|
||||
vm.Config = _formDataService.GetValues(configForm).ToString();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -357,37 +369,11 @@ namespace BTCPayServer.Controllers
|
||||
new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id });
|
||||
}
|
||||
|
||||
// The JObject may contain too much data because we used ALL post values and this may be more than we needed.
|
||||
// Because we don't know the form fields beforehand, we will filter out the superfluous data afterwards.
|
||||
// We will keep all the old keys + merge the new keys as per the current form.
|
||||
// Since the form can differ by circumstances, we will never remove any keys that were previously stored. We just limit what we add.
|
||||
private JObject RemoveUnusedFieldsFromConfig(JObject storedData, JObject newData, Form form)
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/trade/simulate")]
|
||||
public async Task<IActionResult> SimulateTradeJson(string storeId, string accountId,
|
||||
[FromBody] TradeRequestData request)
|
||||
{
|
||||
JObject filteredData = new JObject();
|
||||
var storedKeys = new List<string>();
|
||||
foreach (var item in storedData)
|
||||
{
|
||||
storedKeys.Add(item.Key);
|
||||
}
|
||||
|
||||
var formKeys = form.GetAllFields().Select(f => f.FullName).ToHashSet();
|
||||
|
||||
foreach (var item in newData)
|
||||
{
|
||||
if (storedKeys.Contains(item.Key) || formKeys.Contains(item.Key))
|
||||
{
|
||||
filteredData[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/trade/prepare")]
|
||||
public async Task<IActionResult> GetTradePrepareJson(string storeId, string accountId,
|
||||
[FromQuery] string assetToTrade, [FromQuery] string assetToTradeInto)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetToTrade) || string.IsNullOrEmpty(assetToTradeInto))
|
||||
if (string.IsNullOrEmpty(request.FromAsset) || string.IsNullOrEmpty(request.ToAsset))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
@ -421,12 +407,12 @@ namespace BTCPayServer.Controllers
|
||||
foreach (var pair in assetBalancesData)
|
||||
{
|
||||
var oneAsset = pair.Key;
|
||||
if (assetToTrade.Equals(oneAsset))
|
||||
if (request.FromAsset.Equals(oneAsset))
|
||||
{
|
||||
vm.MaxQtyToTrade = pair.Value;
|
||||
vm.MaxQty = pair.Value;
|
||||
//vm.FormattedMaxQtyToTrade = pair.Value;
|
||||
|
||||
if (assetToTrade.Equals(assetToTradeInto))
|
||||
if (request.FromAsset.Equals(request.ToAsset))
|
||||
{
|
||||
// We cannot trade the asset for itself
|
||||
return BadRequest();
|
||||
@ -434,7 +420,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var quote = await tradingCustodian.GetQuoteForAssetAsync(assetToTrade, assetToTradeInto,
|
||||
var quote = await tradingCustodian.GetQuoteForAssetAsync(request.FromAsset,
|
||||
request.ToAsset,
|
||||
config, default);
|
||||
|
||||
// TODO Ask is normally a higher number than Bid!! Let's check this!! Maybe a Unit Test?
|
||||
@ -574,6 +561,93 @@ namespace BTCPayServer.Controllers
|
||||
return Request.GetRelativePathOrAbsolute(res);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/withdraw/simulate")]
|
||||
public async Task<IActionResult> SimulateWithdrawJson(string storeId, string accountId,
|
||||
[FromBody] WithdrawRequestData withdrawRequestData)
|
||||
{
|
||||
if (string.IsNullOrEmpty(withdrawRequestData.PaymentMethod))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var vm = new WithdrawalPrepareViewModel();
|
||||
|
||||
try
|
||||
{
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
|
||||
try
|
||||
{
|
||||
var simulateWithdrawal =
|
||||
await _btcPayServerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, withdrawRequestData,
|
||||
default);
|
||||
vm = new WithdrawalPrepareViewModel(simulateWithdrawal);
|
||||
|
||||
// There are no bad config fields, so we need an empty array
|
||||
vm.BadConfigFields = Array.Empty<string>();
|
||||
}
|
||||
catch (BadConfigException e)
|
||||
{
|
||||
Form configForm = await custodian.GetConfigForm(config);
|
||||
configForm.SetValues(config);
|
||||
string[] badConfigFields = new string[e.BadConfigKeys.Length];
|
||||
int i = 0;
|
||||
foreach (var oneField in configForm.GetAllFields())
|
||||
{
|
||||
foreach (var badConfigKey in e.BadConfigKeys)
|
||||
{
|
||||
if (oneField.FullName.Equals(badConfigKey))
|
||||
{
|
||||
var field = configForm.GetFieldByFullName(oneField.FullName);
|
||||
badConfigFields[i] = field.Label;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.BadConfigFields = badConfigFields;
|
||||
return Ok(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
vm.ErrorMessage = e.Message;
|
||||
}
|
||||
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/withdraw")]
|
||||
public async Task<IActionResult> Withdraw(string storeId, string accountId,
|
||||
[FromBody] WithdrawRequestData request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _btcPayServerClient.CreateCustodianAccountWithdrawal(storeId, accountId, request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (GreenfieldAPIException e)
|
||||
{
|
||||
var result = new ObjectResult(e.APIError) { StatusCode = e.HttpCode };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
@ -44,6 +45,9 @@ namespace BTCPayServer.Controllers
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private IHttpClientFactory HttpClientFactory { get; }
|
||||
private SignInManager<ApplicationUser> SignInManager { get; }
|
||||
|
||||
private IFileProvider _WebRootFileProvider;
|
||||
|
||||
public LanguageService LanguageService { get; }
|
||||
|
||||
public UIHomeController(IHttpClientFactory httpClientFactory,
|
||||
@ -51,6 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
LanguageService languageService,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
IWebHostEnvironment environment,
|
||||
SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_theme = theme;
|
||||
@ -59,6 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
_networkProvider = networkProvider;
|
||||
_storeRepository = storeRepository;
|
||||
SignInManager = signInManager;
|
||||
_WebRootFileProvider = environment.WebRootFileProvider;
|
||||
}
|
||||
|
||||
[HttpGet("home")]
|
||||
@ -121,6 +127,45 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return Json(Client.Models.PermissionMetadata.PermissionNodes, new JsonSerializerSettings { Formatting = Formatting.Indented });
|
||||
}
|
||||
[Route("misc/translations/{resource}/{lang}")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetTranslations(string resource, string lang)
|
||||
{
|
||||
string path;
|
||||
if (resource == "checkout-v1")
|
||||
path = "locales";
|
||||
else if (resource == "checkout-v2")
|
||||
path = "locales/checkout";
|
||||
else
|
||||
return NotFound();
|
||||
var enLang = Lang(path + "/en.json");
|
||||
var en = (enLang as JsonResult)?.Value as JObject;
|
||||
if (en is null || lang == "en" || lang == "en-US")
|
||||
return enLang;
|
||||
lang = LanguageService.FindLanguage(lang)?.Code;
|
||||
if (lang is null)
|
||||
return enLang;
|
||||
var oLang = Lang(path + $"/{lang}.json");
|
||||
var o = (oLang as JsonResult)?.Value as JObject;
|
||||
if (o is null)
|
||||
return enLang;
|
||||
en.Merge(o);
|
||||
return Json(en);
|
||||
}
|
||||
|
||||
private IActionResult Lang(string path)
|
||||
{
|
||||
var fi = _WebRootFileProvider.GetFileInfo(path);
|
||||
try
|
||||
{
|
||||
using var fs = fi.CreateReadStream();
|
||||
return Json(JObject.Load(new JsonTextReader(new StreamReader(fs, leaveOpen: true))));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[Route("swagger/v1/swagger.json")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie + "," + AuthenticationSchemes.Greenfield)]
|
||||
@ -177,7 +222,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public RedirectToActionResult RedirectToStore(StoreData store)
|
||||
{
|
||||
return store.Role == StoreRoles.Owner
|
||||
return store.HasPermission(Policies.CanModifyStoreSettings)
|
||||
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
|
||||
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user