Compare commits

..

1 Commits

Author SHA1 Message Date
11154ff19d Fix PluginPacker crash 2023-01-31 23:28:41 +09:00
579 changed files with 6268 additions and 17364 deletions

View File

@ -1,14 +1,8 @@
blank_issues_enabled: true
contact_links:
- 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: 🚀 Discussions
url: https://github.com/btcpayserver/btcpayserver/discussions
about: Technical discussions, questions and feature requests
- name: 📝 Official Documentation
url: https://docs.btcpayserver.org
about: Check our documentation for answers to common questions

View File

@ -1,5 +1,3 @@
using System.IO;
namespace BTCPayServer.Configuration
{
public class DataDirectories
@ -9,12 +7,5 @@ namespace BTCPayServer.Configuration
public string TempStorageDir { get; set; }
public string StorageDir { get; set; }
public string TempDir { get; set; }
public string ToDatadirFullPath(string path)
{
if (Path.IsPathRooted(path))
return path;
return Path.Combine(DataDir, path);
}
}
}

View File

@ -19,8 +19,6 @@ namespace BTCPayServer.Abstractions.Contracts
public class NotificationViewModel
{
public string Id { get; set; }
public string Identifier { get; set; }
public string Type { get; set; }
public DateTimeOffset Created { get; set; }
public string Body { get; set; }
public string ActionLink { get; set; }

View File

@ -5,8 +5,4 @@ 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}")
{
}
}

View File

@ -1,28 +0,0 @@
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;
}
}

View File

@ -5,14 +5,9 @@ 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> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken);
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken);

View File

@ -20,6 +20,7 @@ public interface ICustodian
*/
Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken);
public Task<Form.Form> GetConfigForm(CancellationToken cancellationToken = default);
public Task<Form.Form> GetConfigForm(JObject config, string locale,
CancellationToken cancellationToken = default);
}

View File

@ -7,10 +7,6 @@ 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());

View File

@ -12,7 +12,7 @@ public class Field
{
public static Field Create(string label, string name, string value, bool required, string helpText, string type = "text")
{
return new Field
return new Field()
{
Label = label,
Name = name,
@ -26,14 +26,14 @@ public class Field
// The name of the HTML5 node. Should be used as the key for the posted data.
public string Name;
public bool Constant;
public bool Hidden;
// HTML5 compatible type string like "text", "textarea", "email", "password", etc.
// 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).
public string Type;
public static Field CreateFieldset()
{
return new Field { Type = "fieldset" };
return new Field() { Type = "fieldset" };
}
// The value field is what is currently in the DB or what the user entered, but possibly not saved yet due to validation errors.
@ -52,10 +52,10 @@ public class Field
public string HelpText;
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new ();
public List<Field> Fields { get; set; } = new();
// The field is considered "valid" if there are no validation errors
public List<string> ValidationErrors = new ();
public List<string> ValidationErrors = new List<string>();
public virtual bool IsValid()
{

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json.Linq;
using Npgsql.Internal.TypeHandlers.GeometricHandlers;
namespace BTCPayServer.Abstractions.Form;
@ -22,7 +20,6 @@ public class Form
return JObject.FromObject(this, CamelCaseSerializerSettings.Serializer).ToString(Newtonsoft.Json.Formatting.Indented);
}
#nullable restore
// Messages to be shown at the top of the form indicating user feedback like "Saved successfully" or "Please change X because of Y." or a warning, etc...
public List<AlertMessage> TopMessages { get; set; } = new();
@ -35,125 +32,126 @@ public class Form
return Fields.Select(f => f.IsValid()).All(o => o);
}
public Field GetFieldByFullName(string fullName)
public Field GetFieldByName(string name)
{
foreach (var f in GetAllFields())
return GetFieldByName(name, Fields, null);
}
private static Field GetFieldByName(string name, List<Field> fields, string prefix)
{
prefix ??= string.Empty;
foreach (var field in fields)
{
if (f.FullName == fullName)
return f.Field;
var currentPrefix = prefix;
if (!string.IsNullOrEmpty(field.Name))
{
currentPrefix = $"{prefix}{field.Name}";
if (currentPrefix.Equals(name, StringComparison.InvariantCultureIgnoreCase))
{
return field;
}
currentPrefix += "_";
}
var subFieldResult = GetFieldByName(name, field.Fields, currentPrefix);
if (subFieldResult is not null)
{
return subFieldResult;
}
}
return null;
}
public IEnumerable<(string FullName, List<string> Path, Field Field)> GetAllFields()
public List<string> GetAllNames()
{
HashSet<string> nameReturned = new();
foreach (var f in GetAllFieldsCore(new List<string>(), Fields))
{
var fullName = string.Join('_', f.Path.Where(s => !string.IsNullOrEmpty(s)));
if (!nameReturned.Add(fullName))
continue;
yield return (fullName, f.Path, f.Field);
}
return GetAllNames(Fields);
}
public bool ValidateFieldNames(out List<string> errors)
private static List<string> GetAllNames(List<Field> fields)
{
errors = new List<string>();
HashSet<string> nameReturned = new();
foreach (var f in GetAllFieldsCore(new List<string>(), Fields))
{
var fullName = string.Join('_', f.Path.Where(s => !string.IsNullOrEmpty(s)));
if (!nameReturned.Add(fullName))
{
errors.Add($"Form contains duplicate field names '{fullName}'");
continue;
}
}
return errors.Count == 0;
}
var names = new List<string>();
IEnumerable<(List<string> Path, Field Field)> GetAllFieldsCore(List<string> path, List<Field> fields)
{
foreach (var field in fields)
{
List<string> thisPath = new(path.Count + 1);
thisPath.AddRange(path);
string prefix = string.Empty;
if (!string.IsNullOrEmpty(field.Name))
{
thisPath.Add(field.Name);
yield return (thisPath, field);
names.Add(field.Name);
prefix = $"{field.Name}_";
}
foreach (var child in field.Fields)
if (field.Fields.Any())
{
if (field.Constant)
child.Constant = true;
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
{
yield return descendant;
}
names.AddRange(GetAllNames(field.Fields).Select(s => $"{prefix}{s}"));
}
}
return names;
}
public void ApplyValuesFromOtherForm(Form form)
{
foreach (var fieldset in Fields)
{
foreach (var field in fieldset.Fields)
{
field.Value = form
.GetFieldByName(
$"{(string.IsNullOrEmpty(fieldset.Name) ? string.Empty : fieldset.Name + "_")}{field.Name}")
?.Value;
}
}
}
public void ApplyValuesFromForm(IEnumerable<KeyValuePair<string, StringValues>> form)
public void ApplyValuesFromForm(IFormCollection form)
{
var values = form.GroupBy(f => f.Key, f => f.Value).ToDictionary(g => g.Key, g => g.First());
foreach (var f in GetAllFields())
var names = GetAllNames();
foreach (var name in names)
{
if (f.Field.Constant || !values.TryGetValue(f.FullName, out var val))
var field = GetFieldByName(name);
if (field is null || !form.TryGetValue(name, out var val))
{
continue;
}
f.Field.Value = val;
field.Value = val;
}
}
public void SetValues(JObject values)
public Dictionary<string, object> GetValues()
{
var fields = GetAllFields().ToDictionary(k => k.FullName, k => k.Field);
SetValues(fields, new List<string>(), values);
return GetValues(Fields);
}
private void SetValues(Dictionary<string, Field> fields, List<string> path, JObject values)
private static Dictionary<string, object> GetValues(List<Field> fields)
{
foreach (var prop in values.Properties())
var result = new Dictionary<string, object>();
foreach (Field field in fields)
{
List<string> propPath = new List<string>(path.Count + 1);
propPath.AddRange(path);
propPath.Add(prop.Name);
if (prop.Value.Type == JTokenType.Object)
var name = field.Name ?? string.Empty;
if (field.Fields.Any())
{
SetValues(fields, propPath, (JObject)prop.Value);
}
else if (prop.Value.Type == JTokenType.String)
{
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>();
}
}
}
var values = GetValues(fields);
values.Remove(string.Empty, out var keylessValue);
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)
result.TryAdd(name, values);
if (keylessValue is not Dictionary<string, object> dict)
continue;
foreach (KeyValuePair<string, object> keyValuePair in dict)
{
child = new JObject();
node[p] = child;
result.TryAdd(keyValuePair.Key, keyValuePair.Value);
}
node = child;
}
node[f.Field.Name] = f.Field.Value;
else
{
result.TryAdd(name, field.Value);
}
}
return r;
return result;
}
}

View File

@ -114,11 +114,6 @@ 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;

View File

@ -23,19 +23,10 @@ public class SVGUse : UrlResolutionTagHelper2
{
_fileVersionProvider = fileVersionProvider;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var attr = output.Attributes["href"].Value.ToString();
var symbolIndex = attr!.IndexOf("#", StringComparison.InvariantCulture);
var start = attr.IndexOf("~", StringComparison.InvariantCulture) + 1;
var length = (symbolIndex != -1 ? symbolIndex : attr.Length) - start;
var filePath = attr.Substring(start, length);
if (!string.IsNullOrEmpty(filePath))
{
var versioned = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, filePath);
attr = attr.Replace(filePath, versioned);
}
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
output.Attributes.SetAttribute("href", attr);
base.Process(context, output);
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -23,15 +22,6 @@ namespace BTCPayServer.Client
return await HandleResponse<ApiKeyData>(response);
}
public virtual async Task<ApiKeyData> CreateAPIKey(string userId, CreateApiKeyRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{userId}/api-keys",
bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<ApiKeyData>(response);
}
public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token);
@ -45,14 +35,5 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/api-keys/{apikey}", null, HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task RevokeAPIKey(string userId, string apikey, CancellationToken token = default)
{
if (apikey == null)
throw new ArgumentNullException(nameof(apikey));
if (userId is null)
throw new ArgumentNullException(nameof(userId));
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{userId}/api-keys/{apikey}", null, HttpMethod.Delete), token);
await HandleResponse(response);
}
}
}

View File

@ -51,44 +51,6 @@ namespace BTCPayServer.Client
method: HttpMethod.Get), token);
return await HandleResponse<AppDataBase>(response);
}
public virtual async Task<AppDataBase[]> GetAllApps(string storeId, CancellationToken token = default)
{
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/apps",
method: HttpMethod.Get), token);
return await HandleResponse<AppDataBase[]>(response);
}
public virtual async Task<AppDataBase[]> GetAllApps(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/apps",
method: HttpMethod.Get), token);
return await HandleResponse<AppDataBase[]>(response);
}
public virtual async Task<PointOfSaleAppData> GetPosApp(string appId, CancellationToken token = default)
{
if (appId == null)
throw new ArgumentNullException(nameof(appId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/apps/pos/{appId}",
method: HttpMethod.Get), token);
return await HandleResponse<PointOfSaleAppData>(response);
}
public virtual async Task<CrowdfundAppData> GetCrowdfundApp(string appId, CancellationToken token = default)
{
if (appId == null)
throw new ArgumentNullException(nameof(appId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/apps/crowdfund/{appId}",
method: HttpMethod.Get), token);
return await HandleResponse<CrowdfundAppData>(response);
}
public virtual async Task DeleteApp(string appId, CancellationToken token = default)
{

View File

@ -50,7 +50,7 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<DepositAddressData> GetCustodianAccountDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
public virtual async Task<DepositAddressData> GetDepositAddress(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,6 +58,7 @@ 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,
@ -66,13 +67,13 @@ namespace BTCPayServer.Client
return await HandleResponse<MarketTradeResponseData>(response);
}
public virtual async Task<MarketTradeResponseData> GetCustodianAccountTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
public virtual async Task<MarketTradeResponseData> GetTradeInfo(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> GetCustodianAccountTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
public virtual async Task<TradeQuoteResponseData> GetTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
queryPayload.Add("fromAsset", fromAsset);
@ -80,20 +81,14 @@ 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> CreateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
public virtual async Task<WithdrawalResponseData> CreateWithdrawal(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> GetCustodianAccountWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
public virtual async Task<WithdrawalResponseData> GetWithdrawalInfo(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);

View File

@ -113,24 +113,6 @@ namespace BTCPayServer.Client
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", queryPayload), token);
return await HandleResponse<LightningInvoiceData[]>(response);
}
public virtual async Task<LightningPaymentData[]> GetLightningPayments(string cryptoCode,
bool? includePending = null, long? offsetIndex = null, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (includePending is bool v)
{
queryPayload.Add("includePending", v.ToString());
}
if (offsetIndex is > 0)
{
queryPayload.Add("offsetIndex", offsetIndex);
}
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/payments", queryPayload), token);
return await HandleResponse<LightningPaymentData[]>(response);
}
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
CancellationToken token = default)

View File

@ -115,24 +115,6 @@ namespace BTCPayServer.Client
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", queryPayload), token);
return await HandleResponse<LightningInvoiceData[]>(response);
}
public virtual async Task<LightningPaymentData[]> GetLightningPayments(string storeId, string cryptoCode,
bool? includePending = null, long? offsetIndex = null, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (includePending is bool v)
{
queryPayload.Add("includePending", v.ToString());
}
if (offsetIndex is > 0)
{
queryPayload.Add("offsetIndex", offsetIndex);
}
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/payments", queryPayload), token);
return await HandleResponse<LightningPaymentData[]>(response);
}
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request, CancellationToken token = default)

View File

@ -97,15 +97,5 @@ namespace BTCPayServer.Client
method: HttpMethod.Post, bodyPayload: request), cancellationToken);
await HandleResponse(response);
}
public virtual async Task<PullPaymentLNURL> GetPullPaymentLNURL(string pullPaymentId,
CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest(
$"/api/v1/pull-payments/{pullPaymentId}/lnurl",
method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PullPaymentLNURL>(response);
}
}
}

View File

@ -37,7 +37,7 @@ namespace BTCPayServer.Client
return await HandleResponse<StoreRateConfiguration>(response);
}
public virtual async Task<List<StoreRateResult>> PreviewUpdateStoreRateConfiguration(string storeId,
public virtual async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
StoreRateConfiguration request,
string[] currencyPair,
CancellationToken token = default)
@ -47,18 +47,7 @@ namespace BTCPayServer.Client
queryPayload: new Dictionary<string, object>() { { "currencyPair", currencyPair } },
method: HttpMethod.Post),
token);
return await HandleResponse<List<StoreRateResult>>(response);
}
public virtual async Task<List<StoreRateResult>> GetStoreRates(string storeId, string[] currencyPair,
CancellationToken token = default)
{
using var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/rates",
queryPayload: new Dictionary<string, object>() { { "currencyPair", currencyPair } },
method: HttpMethod.Get),
token);
return await HandleResponse<List<StoreRateResult>>(response);
return await HandleResponse<List<StoreRatePreviewResult>>(response);
}
}
}

View File

@ -1,29 +0,0 @@
using System;
using System.Globalization;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.JsonConverters
{
public class TradeQuantityJsonConverter : JsonConverter<TradeQuantity>
{
public override TradeQuantity ReadJson(JsonReader reader, Type objectType, TradeQuantity existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException("Invalid TradeQuantity, expected string. Expected: \"1.50\" or \"50%\"", reader);
if (TradeQuantity.TryParse((string)reader.Value, out var q))
return q;
throw new JsonObjectException("Invalid format for TradeQuantity. 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());
}
}
}

View File

@ -1,4 +1,3 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@ -7,7 +6,6 @@ namespace BTCPayServer.Client.Models;
public class LedgerEntryData
{
public string Asset { get; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Qty { get; }
[JsonConverter(typeof(StringEnumConverter))]

View File

@ -3,7 +3,7 @@ namespace BTCPayServer.Client.Models;
public class LightningAddressData
{
public string Username { get; set; }
public string CurrencyCode { get; set; }
public string? CurrencyCode { get; set; }
public decimal? Min { get; set; }
public decimal? Max { get; set; }

View File

@ -6,8 +6,6 @@ namespace BTCPayServer.Client.Models
public class NotificationData
{
public string Id { get; set; }
public string Identifier { get; set; }
public string Type { get; set; }
public string Body { get; set; }
public bool Seen { get; set; }
public Uri Link { get; set; }

View File

@ -1,13 +0,0 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class PaymentMethodCriteriaData
{
public string PaymentMethod { get; set; }
public string CurrencyCode { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
public bool Above { get; set; }
}

View File

@ -15,53 +15,11 @@ namespace BTCPayServer.Client.Models
public class PointOfSaleAppData : AppDataBase
{
public string Title { get; set; }
public string DefaultView { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public string Currency { get; set; }
public object Items { get; set; }
public string FixedAmountPayButtonText { get; set; }
public string CustomAmountPayButtonText { get; set; }
public string TipText { get; set; }
public string CustomCSSLink { get; set; }
public string NotificationUrl { get; set; }
public string RedirectUrl { get; set; }
public string Description { get; set; }
public string EmbeddedCSS { get; set; }
public bool? RedirectAutomatically { get; set; }
public bool? RequiresRefundEmail { get; set; }
// We can add POS specific things here later
}
public class CrowdfundAppData : AppDataBase
{
public string Title { get; set; }
public bool Enabled { get; set; }
public bool EnforceTargetAmount { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? StartDate { get; set; }
public string TargetCurrency { get; set; }
public string Description { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? EndDate { get; set; }
public decimal? TargetAmount { get; set; }
public string CustomCSSLink { get; set; }
public string MainImageUrl { get; set; }
public string EmbeddedCSS { get; set; }
public string NotificationUrl { get; set; }
public string Tagline { get; set; }
public object Perks { get; set; }
public bool DisqusEnabled { get; set; }
public string DisqusShortname { get; set; }
public bool SoundsEnabled { get; set; }
public bool AnimationsEnabled { get; set; }
public int ResetEveryAmount { get; set; }
public string ResetEvery { get; set; }
public bool DisplayPerksValue { get; set; }
public bool DisplayPerksRanking { get; set; }
public bool SortPerksByPopularity { get; set; }
public string[] Sounds { get; set; }
public string[] AnimationColors { get; set; }
// We can add Crowdfund specific things here later
}
}

View File

@ -1,8 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class PullPaymentLNURL
{
public string LNURLBech32 { get; set; }
public string LNURLUri { get; set; }
}
}

View File

@ -63,9 +63,6 @@ namespace BTCPayServer.Client.Models
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public List<PaymentMethodCriteriaData> PaymentMethodCriteria { get; set; }
public bool PayJoinEnabled { get; set; }
public InvoiceData.ReceiptOptions Receipt { get; set; }

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class StoreRatePreviewResult
{
public string CurrencyPair { get; set; }
public decimal? Rate { get; set; }
public List<string> Errors { get; set; }
}

View File

@ -1,13 +1,7 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class StoreRateResult
{
public string CurrencyPair { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Rate { get; set; }
public List<string> Errors { get; set; }
public decimal Rate { get; set; }
}

View File

@ -1,13 +1,8 @@
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; }

View File

@ -1,85 +1,13 @@
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; }
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
public TradeQuantity Qty { set; get; }
public decimal Qty { set; get; }
public WithdrawRequestData()
{
}
public WithdrawRequestData(string paymentMethod, TradeQuantity qty)
public WithdrawRequestData(string paymentMethod, decimal 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;
}
}

View File

@ -1,22 +0,0 @@
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;
}
}

View File

@ -5,13 +5,18 @@ using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models;
public class WithdrawalResponseData : WithdrawalBaseResponseData
public class WithdrawalResponseData
{
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; }
@ -19,10 +24,14 @@ public class WithdrawalResponseData : WithdrawalBaseResponseData
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) : base(paymentMethod, asset, ledgerEntries, accountId,
custodianCode)
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
WithdrawalId = withdrawalId;
AccountId = accountId;
CustodianCode = custodianCode;
TargetAddress = targetAddress;
TransactionId = transactionId;
Status = status;

View File

@ -1,21 +0,0 @@
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;
}
}

View File

@ -28,11 +28,8 @@ namespace BTCPayServer.Client
public const string CanViewNotificationsForUser = "btcpay.user.canviewnotificationsforuser";
public const string CanViewUsers = "btcpay.server.canviewusers";
public const string CanCreateUser = "btcpay.server.cancreateuser";
public const string CanManageUsers = "btcpay.server.canmanageusers";
public const string CanDeleteUser = "btcpay.user.candeleteuser";
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
public const string CanManageCustodianAccounts = "btcpay.store.canmanagecustodianaccounts";
public const string CanDepositToCustodianAccounts = "btcpay.store.candeposittocustodianaccount";
@ -67,14 +64,11 @@ namespace BTCPayServer.Client
yield return CanViewLightningInvoiceInStore;
yield return CanCreateLightningInvoiceInStore;
yield return CanManagePullPayments;
yield return CanCreatePullPayments;
yield return CanCreateNonApprovedPullPayments;
yield return CanViewCustodianAccounts;
yield return CanManageCustodianAccounts;
yield return CanDepositToCustodianAccounts;
yield return CanWithdrawFromCustodianAccounts;
yield return CanTradeCustodianAccount;
yield return CanManageUsers;
}
}
public static bool IsValidPolicy(string policy)
@ -98,45 +92,9 @@ 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
{
static Permission()
{
Init();
}
public static Permission Create(string policy, string scope = null)
{
if (TryCreatePermission(policy, scope, out var r))
@ -152,7 +110,7 @@ namespace BTCPayServer.Client
policy = policy.Trim().ToLowerInvariant();
if (!Policies.IsValidPolicy(policy))
return false;
if (!string.IsNullOrEmpty(scope) && !Policies.IsStorePolicy(policy))
if (scope != null && !Policies.IsStorePolicy(policy))
return false;
permission = new Permission(policy, scope);
return true;
@ -205,7 +163,7 @@ namespace BTCPayServer.Client
}
if (!Policies.IsStorePolicy(subpermission.Policy))
return true;
return Scope == null || subpermission.Scope == Scope;
return Scope == null || subpermission.Scope == this.Scope;
}
public static IEnumerable<Permission> ToPermissions(string[] permissions)
@ -221,61 +179,37 @@ namespace BTCPayServer.Client
private bool ContainsPolicy(string subpolicy)
{
return ContainsPolicy(Policy, subpolicy);
}
private static bool ContainsPolicy(string policy, string subpolicy)
{
if (policy == Policies.Unrestricted)
if (this.Policy == Policies.Unrestricted)
return true;
if (policy == subpolicy)
if (this.Policy == subpolicy)
return true;
if (!PolicyMap.TryGetValue(policy, out var subPolicies))
return false;
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
}
private static Dictionary<string, HashSet<string>> PolicyMap = new();
private static void Init()
{
PolicyHasChild(Policies.CanModifyStoreSettings,
Policies.CanManageCustodianAccounts,
Policies.CanManagePullPayments,
Policies.CanModifyInvoices,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreWebhooks,
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.CanModifyServerSettings,
Policies.CanUseInternalLightningNode,
Policies.CanManageUsers);
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)
{
if (PolicyMap.TryGetValue(policy, out var existingSubPolicies))
switch (subpolicy)
{
foreach (string subPolicy in subPolicies)
{
existingSubPolicies.Add(subPolicy);
}
}
else
{
PolicyMap.Add(policy, subPolicies.ToHashSet());
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyInvoices:
case Policies.CanModifyStoreWebhooks when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings:
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanModifyInvoices when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanManagePullPayments when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyPaymentRequests:
case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
case Policies.CanViewLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
case Policies.CanViewLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
case Policies.CanViewNotificationsForUser when this.Policy == Policies.CanManageNotificationsForUser:
case Policies.CanUseInternalLightningNode when this.Policy == Policies.CanModifyServerSettings:
case Policies.CanViewCustodianAccounts when this.Policy == Policies.CanManageCustodianAccounts:
case Policies.CanViewCustodianAccounts when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanManageCustodianAccounts when this.Policy == Policies.CanModifyStoreSettings:
return true;
default:
return false;
}
}
@ -284,17 +218,23 @@ namespace BTCPayServer.Client
public override string ToString()
{
return Scope != null ? $"{Policy}:{Scope}" : Policy;
if (Scope != null)
{
return $"{Policy}:{Scope}";
}
return Policy;
}
public override bool Equals(object obj)
{
Permission item = obj as Permission;
return item != null && ToString().Equals(item.ToString());
if (item == null)
return false;
return ToString().Equals(item.ToString());
}
public static bool operator ==(Permission a, Permission b)
{
if (ReferenceEquals(a, b))
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;

View File

@ -1,6 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data.Data;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
@ -30,12 +30,7 @@ namespace BTCPayServer.Data
{
_designTime = designTime;
}
#nullable enable
public async Task<string?> GetMigrationState()
{
return (await Settings.FromSqlRaw("SELECT \"Id\", \"Value\" FROM \"Settings\" WHERE \"Id\"='MigrationData'").AsNoTracking().FirstOrDefaultAsync())?.Value;
}
#nullable restore
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
public DbSet<APIKeyData> ApiKeys { get; set; }
public DbSet<AppData> Apps { get; set; }
@ -74,7 +69,6 @@ namespace BTCPayServer.Data
public DbSet<WebhookData> Webhooks { get; set; }
public DbSet<LightningAddressData> LightningAddresses { get; set; }
public DbSet<PayoutProcessorData> PayoutProcessors { get; set; }
public DbSet<FormData> Forms { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@ -89,23 +83,23 @@ namespace BTCPayServer.Data
// some of the data models don't have OnModelCreating for now, commenting them
ApplicationUser.OnModelCreating(builder, Database);
ApplicationUser.OnModelCreating(builder);
AddressInvoiceData.OnModelCreating(builder);
APIKeyData.OnModelCreating(builder, Database);
APIKeyData.OnModelCreating(builder);
AppData.OnModelCreating(builder);
CustodianAccountData.OnModelCreating(builder, Database);
CustodianAccountData.OnModelCreating(builder);
//StoredFile.OnModelCreating(builder);
InvoiceEventData.OnModelCreating(builder);
InvoiceSearchData.OnModelCreating(builder);
InvoiceWebhookDeliveryData.OnModelCreating(builder);
InvoiceData.OnModelCreating(builder, Database);
NotificationData.OnModelCreating(builder, Database);
InvoiceData.OnModelCreating(builder);
NotificationData.OnModelCreating(builder);
//OffchainTransactionData.OnModelCreating(builder);
BTCPayServer.Data.PairedSINData.OnModelCreating(builder);
PairingCodeData.OnModelCreating(builder);
//PayjoinLock.OnModelCreating(builder);
PaymentRequestData.OnModelCreating(builder, Database);
PaymentData.OnModelCreating(builder, Database);
PaymentRequestData.OnModelCreating(builder);
PaymentData.OnModelCreating(builder);
PayoutData.OnModelCreating(builder);
PendingInvoiceData.OnModelCreating(builder);
//PlannedTransaction.OnModelCreating(builder);
@ -116,7 +110,7 @@ namespace BTCPayServer.Data
StoreWebhookData.OnModelCreating(builder);
StoreData.OnModelCreating(builder, Database);
U2FDevice.OnModelCreating(builder);
Fido2Credential.OnModelCreating(builder, Database);
Fido2Credential.OnModelCreating(builder);
BTCPayServer.Data.UserStore.OnModelCreating(builder);
//WalletData.OnModelCreating(builder);
WalletObjectData.OnModelCreating(builder, Database);
@ -124,14 +118,13 @@ namespace BTCPayServer.Data
#pragma warning disable CS0612 // Type or member is obsolete
WalletTransactionData.OnModelCreating(builder);
#pragma warning restore CS0612 // Type or member is obsolete
WebhookDeliveryData.OnModelCreating(builder, Database);
LightningAddressData.OnModelCreating(builder, Database);
PayoutProcessorData.OnModelCreating(builder, Database);
WebhookData.OnModelCreating(builder, Database);
FormData.OnModelCreating(builder, Database);
WebhookDeliveryData.OnModelCreating(builder);
LightningAddressData.OnModelCreating(builder);
PayoutProcessorData.OnModelCreating(builder);
//WebhookData.OnModelCreating(builder);
if (Database.IsSqlite() && !_designTime)
if (Database.IsSqlite() && !_designTime)
{
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations

View File

@ -1,11 +1,9 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class APIKeyData : IHasBlob<APIKeyBlob>
public class APIKeyData
{
[MaxLength(50)]
public string Id { get; set; }
@ -18,15 +16,13 @@ namespace BTCPayServer.Data
public APIKeyType Type { get; set; } = APIKeyType.Legacy;
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public StoreData StoreData { get; set; }
public ApplicationUser User { get; set; }
public string Label { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<APIKeyData>()
.HasOne(o => o.StoreData)
@ -40,13 +36,6 @@ namespace BTCPayServer.Data
builder.Entity<APIKeyData>()
.HasIndex(o => o.StoreId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<APIKeyData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}

View File

@ -2,13 +2,11 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
// Add profile data for application users by adding properties to the ApplicationUser class
public class ApplicationUser : IdentityUser, IHasBlob<UserBlob>
public class ApplicationUser : IdentityUser
{
public bool RequiresEmailConfirmation { get; set; }
public List<StoredFile> StoredFiles { get; set; }
@ -22,28 +20,15 @@ namespace BTCPayServer.Data
public List<UserStore> UserStores { get; set; }
public List<Fido2Credential> Fido2Credentials { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public List<IdentityUserRole<string>> UserRoles { get; set; }
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
public static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<ApplicationUser>()
.HasMany<IdentityUserRole<string>>(user => user.UserRoles)
.WithOne().HasForeignKey(role => role.UserId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<ApplicationUser>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}
public class UserBlob
{
public bool ShowInvoiceStatusChangeHint { get; set; }
}
}

View File

@ -1,13 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data;
public class CustodianAccountData : IHasBlob<JObject>
public class CustodianAccountData
{
[Required]
[MaxLength(50)]
@ -26,29 +24,19 @@ public class CustodianAccountData : IHasBlob<JObject>
public string Name { get; set; }
[JsonIgnore]
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
[JsonIgnore]
public string Blob2 { get; set; }
[JsonIgnore]
public StoreData StoreData { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<CustodianAccountData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.CustodianAccounts)
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
builder.Entity<CustodianAccountData>()
builder.Entity<APIKeyData>()
.HasIndex(o => o.StoreId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<CustodianAccountData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}

View File

@ -2,11 +2,10 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class Fido2Credential : IHasBlobUntyped
public class Fido2Credential
{
public string Name { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
@ -15,7 +14,6 @@ namespace BTCPayServer.Data
public string ApplicationUserId { get; set; }
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public CredentialType Type { get; set; }
public enum CredentialType
{
@ -24,18 +22,12 @@ namespace BTCPayServer.Data
[Display(Name = "Lightning node (LNURL Auth)")]
LNURLAuth
}
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
public static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Fido2Credential>()
.HasOne(o => o.ApplicationUser)
.WithMany(i => i.Fido2Credentials)
.HasForeignKey(i => i.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())
{
builder.Entity<Fido2Credential>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
public ApplicationUser ApplicationUser { get; set; }

View File

@ -2,30 +2,11 @@ using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data;
namespace BTCPayServer.Data.Data;
public class FormData
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
public string Name { get; set; }
public string StoreId { get; set; }
public StoreData Store { get; set; }
public string Config { get; set; }
public bool Public { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<FormData>()
.HasOne(o => o.Store)
.WithMany(o => o.Forms).OnDelete(DeleteBehavior.Cascade);
builder.Entity<FormData>().HasIndex(o => o.StoreId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<FormData>()
.Property(o => o.Config)
.HasColumnType("JSONB");
}
}
}

View File

@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public interface IHasBlob<T>
{
[Obsolete("Use Blob2 instead")]
byte[] Blob { get; set; }
string Blob2 { get; set; }
}
public interface IHasBlob
{
[Obsolete("Use Blob2 instead")]
byte[] Blob { get; set; }
string Blob2 { get; set; }
public Type Type { get; set; }
}
public interface IHasBlobUntyped
{
[Obsolete("Use Blob2 instead")]
byte[] Blob { get; set; }
string Blob2 { get; set; }
}
}

View File

@ -2,11 +2,10 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class InvoiceData : IHasBlobUntyped
public class InvoiceData
{
public string Id { get; set; }
@ -17,9 +16,7 @@ namespace BTCPayServer.Data
public List<PaymentData> Payments { get; set; }
public List<InvoiceEventData> Events { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string ItemCode { get; set; }
public string OrderId { get; set; }
public string Status { get; set; }
@ -35,7 +32,7 @@ namespace BTCPayServer.Data
public RefundData CurrentRefund { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<InvoiceData>()
.HasOne(o => o.StoreData)
@ -45,13 +42,6 @@ namespace BTCPayServer.Data
builder.Entity<InvoiceData>()
.HasOne(o => o.CurrentRefund);
builder.Entity<InvoiceData>().HasIndex(o => o.Created);
if (databaseFacade.IsNpgsql())
{
builder.Entity<InvoiceData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -1,21 +1,17 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data;
public class LightningAddressData : IHasBlob<LightningAddressDataBlob>
public class LightningAddressData
{
public string Username { get; set; }
public string StoreDataId { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public StoreData Store { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<LightningAddressData>()
.HasOne(o => o.Store)
@ -24,12 +20,6 @@ public class LightningAddressData : IHasBlob<LightningAddressDataBlob>
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<LightningAddressData>().HasKey(o => o.Username);
if (databaseFacade.IsNpgsql())
{
builder.Entity<LightningAddressData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}

View File

@ -1,12 +1,10 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
namespace BTCPayServer.Data
{
public class NotificationData : IHasBlobUntyped
public class NotificationData
{
[MaxLength(36)]
public string Id { get; set; }
@ -19,23 +17,15 @@ namespace BTCPayServer.Data
[Required]
public string NotificationType { get; set; }
public bool Seen { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<NotificationData>()
.HasOne(o => o.ApplicationUser)
.WithMany(n => n.Notifications)
.HasForeignKey(k => k.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())
{
builder.Entity<NotificationData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -1,34 +1,24 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class PaymentData : IHasBlobUntyped
public class PaymentData
{
public string Id { get; set; }
public string InvoiceDataId { get; set; }
public InvoiceData InvoiceData { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string Type { get; set; }
public bool Accounted { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PaymentData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<PaymentData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -1,10 +1,9 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class PaymentRequestData : IHasBlobUntyped
public class PaymentRequestData
{
public string Id { get; set; }
public DateTimeOffset Created { get; set; }
@ -15,12 +14,10 @@ namespace BTCPayServer.Data
public Client.Models.PaymentRequestData.PaymentRequestStatus Status { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PaymentRequestData>()
.HasOne(o => o.StoreData)
@ -31,13 +28,6 @@ namespace BTCPayServer.Data
.HasDefaultValue(new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero));
builder.Entity<PaymentRequestData>()
.HasIndex(o => o.Status);
if (databaseFacade.IsNpgsql())
{
builder.Entity<PaymentRequestData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -1,15 +1,9 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data;
namespace BTCPayServer.Data.Data;
public class AutomatedPayoutBlob
{
public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1);
}
public class PayoutProcessorData : IHasBlobUntyped
public class PayoutProcessorData
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
@ -18,22 +12,14 @@ public class PayoutProcessorData : IHasBlobUntyped
public string PaymentMethod { get; set; }
public string Processor { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PayoutProcessorData>()
.HasOne(o => o.Store)
.WithMany(data => data.PayoutProcessors).OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())
{
builder.Entity<PayoutProcessorData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
public override string ToString()

View File

@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
namespace BTCPayServer.Data
{
@ -27,6 +25,7 @@ namespace BTCPayServer.Data
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy { get; set; }
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategies { get; set; }
public string StoreName { get; set; }
@ -51,7 +50,6 @@ namespace BTCPayServer.Data
public IEnumerable<PayoutData> Payouts { get; set; }
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
public IEnumerable<StoreSettingData> Settings { get; set; }
public IEnumerable<FormData> Forms { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -60,20 +58,6 @@ namespace BTCPayServer.Data
builder.Entity<StoreData>()
.Property(o => o.StoreBlob)
.HasColumnType("JSONB");
builder.Entity<StoreData>()
.Property(o => o.DerivationStrategies)
.HasColumnType("JSONB");
}
else if (databaseFacade.IsMySql())
{
builder.Entity<StoreData>()
.Property(o => o.StoreBlob)
.HasConversion(new ValueConverter<string, byte[]>
(
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
));
}
}
}

View File

@ -12,13 +12,6 @@ namespace BTCPayServer.Data
{
public class Types
{
public static readonly HashSet<string> AllTypes;
static Types()
{
AllTypes = typeof(Types).GetFields()
.Where(f => f.FieldType == typeof(string))
.Select(f => (string)f.GetValue(null)).ToHashSet(StringComparer.OrdinalIgnoreCase);
}
public const string Label = "label";
public const string Tx = "tx";
public const string Payjoin = "payjoin";

View File

@ -1,29 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class WebhookData : IHasBlobUntyped
public class WebhookData
{
[Key]
[MaxLength(25)]
public string Id { get; set; }
[Obsolete("Use Blob2 instead")]
[Required]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public List<WebhookDeliveryData> Deliveries { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
if (databaseFacade.IsNpgsql())
{
builder.Entity<WebhookData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -1,11 +1,10 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class WebhookDeliveryData : IHasBlobUntyped
public class WebhookDeliveryData
{
[Key]
[MaxLength(25)]
@ -17,24 +16,17 @@ namespace BTCPayServer.Data
[Required]
public DateTimeOffset Timestamp { get; set; }
[Obsolete("Use Blob2 instead")]
[Required]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<WebhookDeliveryData>()
.HasOne(o => o.Webhook)
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<WebhookDeliveryData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
@ -17,21 +17,20 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<string>(
name: "StoreDataId",
table: "Payouts",
nullable: true,
maxLength: maxLength);
type: "TEXT",
nullable: true);
migrationBuilder.CreateTable(
name: "PayoutProcessors",
columns: table => new
{
Id = table.Column<string>(nullable: false, maxLength: maxLength),
StoreId = table.Column<string>(nullable: true, maxLength: maxLength),
PaymentMethod = table.Column<string>(nullable: true),
Processor = table.Column<string>(nullable: true),
Id = table.Column<string>(type: "TEXT", nullable: false),
StoreId = table.Column<string>(type: "TEXT", nullable: true),
PaymentMethod = table.Column<string>(type: "TEXT", nullable: true),
Processor = table.Column<string>(type: "TEXT", nullable: true),
Blob = table.Column<byte[]>(nullable: true)
},
constraints: table =>

View File

@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
@ -17,13 +17,12 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "LightningAddresses",
columns: table => new
{
Username = table.Column<string>(nullable: false, maxLength: maxLength),
StoreDataId = table.Column<string>(nullable: false, maxLength: maxLength),
Username = table.Column<string>(type: "TEXT", nullable: false),
StoreDataId = table.Column<string>(type: "TEXT", nullable: false),
Blob = table.Column<byte[]>( nullable: true)
},
constraints: table =>

View File

@ -14,13 +14,12 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxlength = migrationBuilder.IsMySql() ? 255 : null;
migrationBuilder.CreateTable(
name: "StoreSettings",
columns: table => new
{
Name = table.Column<string>(nullable: false, maxLength: maxlength),
StoreId = table.Column<string>(nullable: false, maxLength: maxlength),
Name = table.Column<string>(type: "TEXT", nullable: false),
StoreId = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
},
constraints: table =>

View File

@ -17,15 +17,13 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxlength = migrationBuilder.IsMySql() ? 255 : null;
migrationBuilder.CreateTable(
name: "WalletObjects",
columns: table => new
{
WalletId = table.Column<string>(nullable: false, maxLength: maxlength),
Type = table.Column<string>(nullable: false, maxLength: maxlength),
Id = table.Column<string>(nullable: false, maxLength: maxlength),
WalletId = table.Column<string>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(type: "TEXT", nullable: false),
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
},
constraints: table =>
@ -37,17 +35,15 @@ namespace BTCPayServer.Migrations
table: "WalletObjects",
columns: new[] { "Type", "Id" });
maxlength = migrationBuilder.IsMySql() ? 100 : null;
migrationBuilder.CreateTable(
name: "WalletObjectLinks",
columns: table => new
{
WalletId = table.Column<string>(nullable: false, maxLength: maxlength),
AType = table.Column<string>(nullable: false, maxLength: maxlength),
AId = table.Column<string>(nullable: false, maxLength: maxlength),
BType = table.Column<string>(nullable: false, maxLength: maxlength),
BId = table.Column<string>(nullable: false, maxLength: maxlength),
WalletId = table.Column<string>(type: "TEXT", nullable: false),
AType = table.Column<string>(type: "TEXT", nullable: false),
AId = table.Column<string>(type: "TEXT", nullable: false),
BType = table.Column<string>(type: "TEXT", nullable: false),
BId = table.Column<string>(type: "TEXT", nullable: false),
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
},
constraints: table =>

View File

@ -1,30 +0,0 @@
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("20230123062447_migrateoldratesource")]
public partial class migrateoldratesource : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("UPDATE \"Stores\" SET \"StoreBlob\"=jsonb_set(\"StoreBlob\", \'{preferredExchange}\', \'{\"oasis_trade\": \"oasisdev\", \"gdax\":\"coinbasepro\", \"coinaverage\":\"coingecko\"}\'::jsonb->(\"StoreBlob\"->>\'preferredExchange\')) WHERE \"StoreBlob\"->>\'preferredExchange\' = ANY (ARRAY[\'oasis_trade\', \'gdax\', \'coinaverage\']);");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Not supported
}
}
}

View File

@ -1,54 +0,0 @@
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("20230125085242_AddForms")]
public partial class AddForms : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxlength = migrationBuilder.IsMySql() ? 255 : null;
migrationBuilder.CreateTable(
name: "Forms",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxlength),
Name = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxlength),
StoreId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxlength),
Config = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true),
Public = table.Column<bool>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Forms", x => x.Id);
table.ForeignKey(
name: "FK_Forms_Stores_StoreId",
column: x => x.StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Forms_StoreId",
table: "Forms",
column: "StoreId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Forms");
}
}
}

View File

@ -1,150 +0,0 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230130040047_blob2")]
public partial class blob2 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
var type = migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT";
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "Webhooks",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "WebhookDeliveries",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "PaymentRequests",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "Notifications",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "LightningAddresses",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "Fido2Credentials",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "AspNetUsers",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "ApiKeys",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "Invoices",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "Payments",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "PayoutProcessors",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Blob2",
table: "CustodianAccount",
type: type,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Type",
table: "Payments",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Blob2",
table: "Webhooks");
migrationBuilder.DropColumn(
name: "Blob2",
table: "WebhookDeliveries");
migrationBuilder.DropColumn(
name: "Blob2",
table: "PaymentRequests");
migrationBuilder.DropColumn(
name: "Blob2",
table: "Notifications");
migrationBuilder.DropColumn(
name: "Blob2",
table: "LightningAddresses");
migrationBuilder.DropColumn(
name: "Blob2",
table: "Fido2Credentials");
migrationBuilder.DropColumn(
name: "Blob2",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "Blob2",
table: "ApiKeys");
migrationBuilder.DropColumn(
name: "Blob2",
table: "Invoices");
migrationBuilder.DropColumn(
name: "Blob2",
table: "Payments");
migrationBuilder.DropColumn(
name: "Blob2",
table: "PayoutProcessors");
migrationBuilder.DropColumn(
name: "Blob2",
table: "CustodianAccount");
migrationBuilder.DropColumn(
name: "Type",
table: "Payments");
}
}
}

View File

@ -1,30 +0,0 @@
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("20230130062447_jsonb2")]
public partial class jsonb2 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("ALTER TABLE \"Stores\" ALTER COLUMN \"DerivationStrategies\" TYPE JSONB USING \"DerivationStrategies\"::JSONB");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Not supported
}
}
}

View File

@ -1,31 +0,0 @@
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
}
}
}

View File

@ -45,9 +45,6 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("Label")
.HasColumnType("TEXT");
@ -112,9 +109,6 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
@ -189,9 +183,6 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("CustodianCode")
.IsRequired()
.HasMaxLength(50)
@ -214,32 +205,7 @@ namespace BTCPayServer.Migrations
b.ToTable("CustodianAccount");
});
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Config")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("Public")
.HasColumnType("INTEGER");
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("Forms");
});
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
modelBuilder.Entity("BTCPayServer.Data.Data.PayoutProcessorData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
@ -276,9 +242,6 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
@ -303,9 +266,6 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
@ -416,9 +376,6 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("StoreDataId")
.IsRequired()
.HasColumnType("TEXT");
@ -444,9 +401,6 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
@ -558,15 +512,9 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
@ -585,9 +533,6 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Created")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
@ -760,8 +705,8 @@ namespace BTCPayServer.Migrations
b.Property<int>("SpeedPolicy")
.HasColumnType("INTEGER");
b.Property<string>("StoreBlob")
.HasColumnType("TEXT");
b.Property<byte[]>("StoreBlob")
.HasColumnType("BLOB");
b.Property<byte[]>("StoreCertificate")
.HasColumnType("BLOB");
@ -975,11 +920,9 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.IsRequired()
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Webhooks");
@ -992,11 +935,9 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.IsRequired()
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
@ -1188,17 +1129,7 @@ namespace BTCPayServer.Migrations
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("Forms")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
modelBuilder.Entity("BTCPayServer.Data.Data.PayoutProcessorData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("PayoutProcessors")
@ -1588,8 +1519,6 @@ namespace BTCPayServer.Migrations
b.Navigation("CustodianAccounts");
b.Navigation("Forms");
b.Navigation("Invoices");
b.Navigation("LightningAddresses");

View File

@ -28,7 +28,7 @@ namespace BTCPayServer.PluginPacker
var name = args[1];
var outputDir = Path.Combine(args[2], name);
var outputFile = Path.Combine(outputDir, name);
var rootDLLPath = Path.GetFullPath(Path.Combine(directory, name + ".dll"));
var rootDLLPath = Path.Combine(directory, name + ".dll");
if (!File.Exists(rootDLLPath))
{
throw new Exception($"{rootDLLPath} could not be found");

View File

@ -12,15 +12,17 @@ namespace BTCPayServer.Rating
public string Name { get; }
public string Url { get; }
public string Id { get; }
public string SourceId { get; }
public RateSource Source { get; }
public AvailableRateProvider(string id, string name, string url) : this(id, name, url, RateSource.Direct)
public AvailableRateProvider(string id, string name, string url) : this(id, id, name, url, RateSource.Direct)
{
}
public AvailableRateProvider(string id, string name, string url, RateSource source)
public AvailableRateProvider(string id, string sourceId, string name, string url, RateSource source)
{
Id = id;
SourceId = sourceId;
Name = name;
Url = url;
Source = source;

View File

@ -1305,7 +1305,7 @@
"name":"Satoshis",
"code":"SATS",
"divisibility":0,
"symbol":"sats",
"symbol":"Sats",
"crypto":true
},
{

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using BTCPayServer.Rating;
using NBitcoin;
using Newtonsoft.Json;
@ -27,6 +28,14 @@ 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)
{
@ -47,7 +56,6 @@ namespace BTCPayServer.Services.Rates
currencyInfo.CurrencySymbol = currency;
return currencyInfo;
}
public NumberFormatInfo GetNumberFormatInfo(string currency)
{
var curr = GetCurrencyProvider(currency);
@ -57,7 +65,6 @@ namespace BTCPayServer.Services.Rates
return ni;
return null;
}
public IFormatProvider GetCurrencyProvider(string currency)
{
lock (_CurrencyProviders)
@ -97,6 +104,30 @@ 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()

View File

@ -15,8 +15,6 @@ namespace BTCPayServer.Services.Rates
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new("argoneum", "Argoneum", "https://rates.argoneum.net/rates");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
// Example result: AGM to BTC rate: {"agm":5000000.000000}

View File

@ -19,9 +19,6 @@ namespace BTCPayServer.Rating.Providers
public decimal? ask { get; set; }
}
private readonly HttpClient _httpClient;
public RateSourceInfo RateSourceInfo => new RateSourceInfo("btcturk", "BtcTurk", "https://api.btcturk.com/api/v2/ticker");
public BtcTurkRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();

View File

@ -222,8 +222,6 @@ namespace BTCPayServer.Services.Rates
}
}
public RateSourceInfo RateSourceInfo => _Inner.RateSourceInfo;
private async Task<LatestFetch> Fetch(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

View File

@ -11,9 +11,6 @@ namespace BTCPayServer.Services.Rates
public class BitbankRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public RateSourceInfo RateSourceInfo => new("bitbank", "Bitbank", "https://public.bitbank.cc/tickers");
public BitbankRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();

View File

@ -15,8 +15,6 @@ namespace BTCPayServer.Services.Rates
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new RateSourceInfo("bitflyer", "Bitflyer", "https://api.bitflyer.com/v1/ticker");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://api.bitflyer.jp/v1/ticker", cancellationToken);

View File

@ -15,8 +15,6 @@ namespace BTCPayServer.Services.Rates
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new("bitpay", "Bitpay", "https://bitpay.com/rates");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://bitpay.com/rates", cancellationToken);

View File

@ -14,8 +14,6 @@ public class BudaRateProvider : IRateProvider
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new RateSourceInfo("buda", "Buda", "https://www.buda.com/api/v2/markets/btc-clp/ticker");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://www.buda.com/api/v2/markets/btc-clp/ticker", cancellationToken);

View File

@ -14,8 +14,6 @@ namespace BTCPayServer.Services.Rates
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new RateSourceInfo("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken);

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,6 @@ namespace BTCPayServer.Services.Rates
{
public class CryptoMarketExchangeRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("cryptomarket", "CryptoMarket", "https://api.exchange.cryptomkt.com/api/3/public/ticker/");
private readonly HttpClient _httpClient;
public CryptoMarketExchangeRateProvider(HttpClient httpClient)
{

View File

@ -40,9 +40,6 @@ namespace BTCPayServer.Services.Rates
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
readonly ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
public RateSourceInfo RateSourceInfo { get; set; }
private async Task<PairRate> CreateExchangeRate(T exchangeAPI, KeyValuePair<string, ExchangeTicker> ticker)
{
if (notFoundSymbols.TryGetValue(ticker.Key, out _))

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class FallbackRateProvider : IRateProvider
{
readonly IRateProvider[] _Providers;
public FallbackRateProvider(IRateProvider[] providers)
{
ArgumentNullException.ThrowIfNull(providers);
_Providers = providers;
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
foreach (var p in _Providers)
{
try
{
return await p.GetRatesAsync(cancellationToken).ConfigureAwait(false);
}
catch when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) { Exceptions.Add(ex); }
}
return Array.Empty<PairRate>();
}
public List<Exception> Exceptions { get; set; } = new List<Exception>();
}
}

View File

@ -12,7 +12,6 @@ namespace BTCPayServer.Rating
{
public class HitBTCRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker");
private readonly HttpClient _httpClient;
public HitBTCRateProvider(HttpClient httpClient)
{

View File

@ -6,7 +6,6 @@ namespace BTCPayServer.Services.Rates
{
public interface IRateProvider
{
RateSourceInfo RateSourceInfo { get; }
Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken);
}
}

View File

@ -16,7 +16,7 @@ namespace BTCPayServer.Services.Rates
// Make sure that only one request is sent to kraken in general
public class KrakenExchangeRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
public HttpClient HttpClient
{
get

View File

@ -19,9 +19,6 @@ namespace BTCPayServer.Services.Rates
return _Instance;
}
}
public RateSourceInfo RateSourceInfo => new RateSourceInfo("NULL","NULL", "https://NULL.NULL");
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Array.Empty<PairRate>());

View File

@ -13,7 +13,6 @@ namespace BTCPayServer.Services.Rates
{
public class RipioExchangeProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
private readonly HttpClient _httpClient;
public RipioExchangeProvider(HttpClient httpClient)
{

View File

@ -14,7 +14,6 @@ namespace BTCPayServer.Services.Rates
{
public class YadioRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("yadio", "Yadio", "https://api.yadio.io/exrates/BTC");
private readonly HttpClient _httpClient;
public YadioRateProvider(HttpClient httpClient)
{

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Rating
{
public class RateSourceInfo
{
public RateSourceInfo(string id, string displayName, string url)
{
Id = id;
DisplayName = displayName;
Url = url;
}
public string Id { get; set; }
public string DisplayName { get; set; }
public string Url { get; set; }
}
}

View File

@ -2,13 +2,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using BTCPayServer.Rating.Providers;
using ExchangeSharp;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
@ -17,7 +15,6 @@ namespace BTCPayServer.Services.Rates
{
class WrapperRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => _inner.RateSourceInfo;
private readonly IRateProvider _inner;
public Exception Exception { get; private set; }
public TimeSpan Latency { get; set; }
@ -50,49 +47,148 @@ namespace BTCPayServer.Services.Rates
public ExchangeException Exception { get; internal set; }
public string Exchange { get; internal set; }
}
public RateProviderFactory(IHttpClientFactory httpClientFactory, IEnumerable<IRateProvider> rateProviders)
public RateProviderFactory(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
foreach (var prov in rateProviders)
{
Providers.Add(prov.RateSourceInfo.Id, prov);
}
InitExchanges();
}
private readonly IHttpClientFactory _httpClientFactory;
public Dictionary<string, IRateProvider> Providers { get; } = new Dictionary<string, IRateProvider>();
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> Providers
{
get
{
return _DirectProviders;
}
}
internal IEnumerable<AvailableRateProvider> GetDirectlySupportedExchanges()
{
yield return new AvailableRateProvider("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr");
yield return new AvailableRateProvider("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries");
yield return new AvailableRateProvider("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker");
yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker");
yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker");
yield return new AvailableRateProvider("coingecko", "CoinGecko", "https://api.coingecko.com/api/v3/exchange_rates");
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD");
yield return new AvailableRateProvider("buda", "Buda", "https://www.buda.com/api/v2/markets/btc-clp/ticker");
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/tickers");
yield return new AvailableRateProvider("bitflyer", "Bitflyer", "https://api.bitflyer.com/v1/ticker");
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates");
yield return new AvailableRateProvider("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
yield return new AvailableRateProvider("cryptomarket", "CryptoMarket", "https://api.exchange.cryptomkt.com/api/3/public/ticker/");
yield return new AvailableRateProvider("btcturk", "BtcTurk", "https://api.btcturk.com/api/v2/ticker");
yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0");
yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker");
yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products");
yield return new AvailableRateProvider("argoneum", "Argoneum", "https://rates.argoneum.net/rates");
yield return new AvailableRateProvider("yadio", "Yadio", "https://api.yadio.io/exrates/BTC");
}
void InitExchanges()
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
AddExchangeSharpProviders<ExchangeBinanceAPI>("binance");
AddExchangeSharpProviders<ExchangeBittrexAPI>("bittrex");
AddExchangeSharpProviders<ExchangePoloniexAPI>("poloniex");
AddExchangeSharpProviders<ExchangeNDAXAPI>("ndax");
// Handmade providers
Providers.Add("hitbtc", new HitBTCRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_HITBTC")));
Providers.Add("coingecko", new CoinGeckoRateProvider(_httpClientFactory));
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("buda", new BudaRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BUDA")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
Providers.Add("bitpay", new BitpayRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITPAY")));
Providers.Add("ripio", new RipioExchangeProvider(_httpClientFactory?.CreateClient("EXCHANGE_RIPIO")));
Providers.Add("cryptomarket", new CryptoMarketExchangeRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_CRYPTOMARKET")));
Providers.Add("bitflyer", new BitflyerRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITFLYER")));
Providers.Add("yadio", new YadioRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_YADIO")));
Providers.Add("btcturk", new BtcTurkRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BTCTURK")));
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
// Backward compatibility: coinaverage should be using coingecko to prevent stores from breaking
Providers.Add("coinaverage", new CoinGeckoRateProvider(_httpClientFactory));
AddExchangeSharpProviders<ExchangeBitfinexAPI>("bitfinex");
AddExchangeSharpProviders<ExchangeOKExAPI>("okex");
AddExchangeSharpProviders<ExchangeCoinbaseAPI>("coinbasepro");
// Those exchanges make too many requests, exchange sharp do not parallelize so it is too slow...
//AddExchangeSharpProviders<ExchangeGeminiAPI>("gemini");
//AddExchangeSharpProviders<ExchangeBitstampAPI>("bitstamp");
//AddExchangeSharpProviders<ExchangeBitMEXAPI>("bitmex");
foreach (var provider in Providers.ToArray())
{
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
prov.RefreshRate = TimeSpan.FromMinutes(1.0);
prov.ValidatyTime = TimeSpan.FromMinutes(5.0);
Providers[provider.Key] = prov;
var rsi = provider.Value.RateSourceInfo;
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url));
}
Providers["gdax"] = Providers["coinbasepro"];
foreach (var supportedExchange in CoinGeckoRateProvider.SupportedExchanges.Values)
foreach (var supportedExchange in GetCoinGeckoSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Id) && supportedExchange.Id != CoinGeckoRateProvider.CoinGeckoName)
{
var coingecko = new CoinGeckoRateProvider(_httpClientFactory)
{
UnderlyingExchange = supportedExchange.Id
UnderlyingExchange = supportedExchange.SourceId
};
var bgFetcher = new BackgroundFetcherRateProvider(coingecko);
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
Providers.Add(supportedExchange.Id, bgFetcher);
var rsi = coingecko.RateSourceInfo;
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url, RateSource.Coingecko));
}
}
AvailableRateProviders.Sort((a,b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
}
public List<AvailableRateProvider> AvailableRateProviders { get; } = new List<AvailableRateProvider>();
private IRateProvider AddExchangeSharpProviders<T>(string providerName) where T : ExchangeAPI
{
var provider = new ExchangeSharpRateProvider<T>(_httpClientFactory.CreateClient($"EXCHANGE_{providerName}".ToUpperInvariant()));
Providers.Add(providerName, provider);
return provider;
}
IEnumerable<AvailableRateProvider> _AvailableRateProviders = null;
public IEnumerable<AvailableRateProvider> GetSupportedExchanges()
{
if (_AvailableRateProviders == null)
{
var availableProviders = new Dictionary<string, AvailableRateProvider>();
foreach (var exchange in GetDirectlySupportedExchanges())
{
availableProviders.Add(exchange.Id, exchange);
}
foreach (var exchange in GetCoinGeckoSupportedExchanges())
{
availableProviders.TryAdd(exchange.Id, exchange);
}
_AvailableRateProviders = availableProviders.Values.OrderBy(o => o.Name).ToArray();
}
return _AvailableRateProviders;
}
internal IEnumerable<AvailableRateProvider> GetCoinGeckoSupportedExchanges()
{
return JArray.Parse(CoinGeckoRateProvider.SupportedExchanges).Select(token =>
new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["id"].ToString().ToLowerInvariant(), token["name"].ToString(),
$"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko));
}
private string Normalize(string name)
{
if (name == "oasis_trade")
return "oasisdex";
if (name == "gdax")
return "coinbasepro";
return name;
}
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
{

View File

@ -11,7 +11,6 @@ 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;
@ -387,7 +386,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));
@ -397,21 +396,21 @@ namespace BTCPayServer.Tests
}
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
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
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
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 USD", s.Driver.PageSource);
Assert.Contains("$5,500.00", s.Driver.PageSource);
if (rateSelection == "CurrentOption")
Assert.Contains("2.20000000 BTC", s.Driver.PageSource);
Assert.Contains("2.20000000 ", s.Driver.PageSource);
if (rateSelection == "RateThenOption")
Assert.Contains("1.10000000 BTC", s.Driver.PageSource);
Assert.Contains("1.10000000 ", s.Driver.PageSource);
s.GoToInvoice(invoice.Id);
s.Driver.FindElement(By.Id("IssueRefund")).Click();
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
@ -585,7 +584,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
@ -623,11 +622,10 @@ 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 = PointOfSaleAppType.AppType;
var appType = AppType.PointOfSale.ToString();
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
Assert.IsType<RedirectToActionResult>(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 };
@ -682,7 +680,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);
@ -737,7 +735,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";
@ -758,7 +756,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
@ -821,13 +819,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
@ -845,7 +843,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);
@ -857,7 +855,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();

View File

@ -286,7 +286,7 @@ namespace BTCPayServer.Tests
if (permissions.Contains(canModifyAllStores) || storePermissions.Any())
{
var resultStores =
await TestApiAgainstAccessToken<Client.Models.StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
tester.PayTester.HttpClient);
foreach (var selectiveStorePermission in storePermissions)

View File

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="110.0.5481.7700" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="108.0.5359.7100" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>

View File

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

View File

@ -6,7 +6,6 @@ using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
@ -34,8 +33,7 @@ namespace BTCPayServer.Tests
s.CreateNewStore();
s.EnableCheckoutV2();
s.AddLightningNode();
// Use non-legacy derivation scheme
s.AddDerivationScheme("BTC", "tpubDD79XF4pzhmPSJ9AyUay9YbXAeD1c6nkUqC32pnKARJH6Ja5hGUfGc76V82ahXpsKqN6UcSGXMkzR34aZq4W23C6DAdZFaVrzWqzj24F8BC");
s.AddDerivationScheme();
// Configure store url
var storeUrl = "https://satoshisteaks.com/";
@ -61,53 +59,30 @@ namespace BTCPayServer.Tests
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
Assert.Contains("Bitcoin", s.Driver.FindElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddress = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
Assert.Equal($"bitcoin:{address}", payUrl);
Assert.StartsWith("bcrt", s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"));
Assert.DoesNotContain("lightning=", payUrl);
Assert.Equal(address, copyAddress);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
// 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);
var payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.StartsWith("bitcoin:", payUrl);
Assert.DoesNotContain("&lightning=", payUrl);
// Switch to LNURL
s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click();
TestUtils.Eventually(() =>
{
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
Assert.StartsWith("lightning:lnurl", payUrl);
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.Id("Lightning_BTC")).GetAttribute("value"));
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.StartsWith("lightning:", payUrl);
});
// Default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("Bitcoin", s.Driver.WaitForElement(By.CssSelector(".payment-method")).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");
copyAddress = s.Driver.FindElement(By.Id("Lightning_BTC_LightningLike")).GetAttribute("value");
Assert.Equal($"lightning:{address}", payUrl);
Assert.Equal(address, copyAddress);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
Assert.DoesNotContain("LNURL", s.Driver.PageSource);
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.StartsWith("lightning:", payUrl);
// Lightning amount in sats
// Lightning amount in Sats
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
s.GoToHome();
s.GoToLightningSettings();
@ -115,16 +90,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("save")).Click();
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);
// 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);
Assert.Contains("Sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
// Expire
var expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
@ -137,7 +103,7 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("Please send", paymentInfo.Text);
TestUtils.Eventually(() =>
{
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
var expiredSection = s.Driver.FindElement(By.Id("expired"));
Assert.True(expiredSection.Displayed);
Assert.Contains("Invoice Expired", expiredSection.Text);
});
@ -148,7 +114,6 @@ namespace BTCPayServer.Tests
s.GoToHome();
invoiceId = s.CreateInvoice();
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
// Details
s.Driver.ToggleCollapse("PaymentDetails");
@ -158,14 +123,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);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
@ -178,27 +139,12 @@ namespace BTCPayServer.Tests
{
Assert.Contains("Created transaction",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
s.Server.ExplorerNode.Generate(2);
s.Server.ExplorerNode.Generate(1);
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 Sent", 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(() =>
@ -206,15 +152,18 @@ namespace BTCPayServer.Tests
Assert.Contains("Mined 1 block",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
});
// Settled
// 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();
TestUtils.Eventually(() =>
{
var settledSection = s.Driver.WaitForElement(By.Id("settled"));
Assert.True(settledSection.Displayed);
Assert.Contains("Invoice Paid", settledSection.Text);
s.Server.ExplorerNode.Generate(1);
var paidSection = s.Driver.WaitForElement(By.Id("paid"));
Assert.True(paidSection.Displayed);
Assert.Contains("Invoice Paid", paidSection.Text);
});
s.Driver.FindElement(By.Id("confetti"));
s.Driver.FindElement(By.Id("ReceiptLink"));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
@ -222,106 +171,36 @@ 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);
invoiceId = s.CreateInvoice();
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");
var copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
var copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
Assert.Contains("?amount=", payUrl);
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&lightning=", payUrl);
Assert.StartsWith("bcrt", copyAddressOnchain);
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnbcrt", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue);
Assert.Contains("&lightning=LNBCRT", qrValue);
// 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");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&lightning=lnbcrt", payUrl);
// 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);
Assert.Contains("&lightning=", payUrl);
// Ensure LNURL is enabled
// BIP21 with topup invoice
s.GoToHome();
s.GoToLightningSettings();
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
Assert.True(s.Driver.FindElement(By.Id("LNURLStandardInvoiceEnabled")).Selected);
// BIP21 with top-up invoice
invoiceId = s.CreateInvoice(amount: null);
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
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");
copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
Assert.StartsWith($"bitcoin:{address}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl);
Assert.StartsWith("bcrt", copyAddressOnchain);
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnurl", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue);
// 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"));
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
Assert.Contains("Bitcoin", s.Driver.FindElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
Assert.DoesNotContain("&lightning=", payUrl);
// Expiry message should not show amount for top-up invoice
// Expiry message should not show amount for topup invoice
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
expirySeconds.Clear();
expirySeconds.SendKeys("5");
@ -345,7 +224,6 @@ namespace BTCPayServer.Tests
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
paymentInfo = s.Driver.FindElement(By.Id("PaymentInfo"));
Assert.False(paymentInfo.Displayed);
Assert.DoesNotContain("This invoice will expire in", paymentInfo.Text);
@ -359,42 +237,6 @@ namespace BTCPayServer.Tests
Assert.True(paymentInfo.Displayed);
Assert.Contains("This invoice will expire in", paymentInfo.Text);
Assert.Contains("09:5", paymentInfo.Text);
// Disable LNURL again
s.GoToHome();
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
// Test:
// - NFC/LNURL-W available with just Lightning
// - BIP21 works correctly even though Lightning is default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&lightning=lnbcrt", payUrl);
// 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)]

View File

@ -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,16 +34,18 @@ 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 appType = CrowdfundAppType.AppType;
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
Assert.Equal(appType, vm.SelectedAppType);
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = AppType.Crowdfund.ToString();
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.AppName);
vm.AppName = "test";
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
vm.SelectedAppType = appType;
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.Equal(nameof(crowdfund.UpdateCrowdfund), redirectToAction.ActionName);
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 };
@ -59,8 +61,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));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps);
}
@ -77,11 +79,10 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
var appType = CrowdfundAppType.AppType;
var appType = AppType.Crowdfund.ToString();
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
Assert.IsType<RedirectToActionResult>(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 };
@ -104,7 +105,7 @@ namespace BTCPayServer.Tests
Amount = new decimal(0.01)
}, default));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
//Scenario 2: Not Enabled But Admin - Allowed
Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
@ -112,8 +113,8 @@ namespace BTCPayServer.Tests
RedirectToCheckout = false,
Amount = new decimal(0.01)
}, default));
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
//Scenario 3: Enabled But Start Date > Now - Not Allowed
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
@ -169,10 +170,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 = CrowdfundAppType.AppType;
var appType = AppType.Crowdfund.ToString();
vm.AppName = "test";
vm.SelectedAppType = appType;
Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.IsType<RedirectToActionResult>(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 };
@ -192,7 +193,7 @@ namespace BTCPayServer.Tests
var publicApps = user.GetController<UICrowdfundController>();
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount);
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate);
@ -216,7 +217,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
Assert.Equal(0m, model.Info.CurrentAmount);
Assert.Equal(1m, model.Info.CurrentPendingAmount);
@ -225,12 +226,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);
await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, invoice.BtcDue);
await tester.ExplorerNode.GenerateAsync(1); // By default invoice confirmed at 1 block
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
Assert.Equal(1m, model.Info.CurrentAmount);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
});
@ -278,7 +279,7 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
});
}

View File

@ -16,7 +16,6 @@ using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.JsonConverters;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
@ -30,7 +29,6 @@ using BTCPayServer.Validation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -51,6 +49,7 @@ namespace BTCPayServer.Tests
{
public FastTests(ITestOutputHelper helper) : base(helper)
{
}
class DockerImage
{
@ -325,7 +324,7 @@ namespace BTCPayServer.Tests
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
@ -511,7 +510,7 @@ namespace BTCPayServer.Tests
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
@ -580,35 +579,18 @@ namespace BTCPayServer.Tests
return new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero) + TimeSpan.FromDays(days);
}
[Fact]
public void CanDetectImage()
{
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.bmp"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, ".bmp"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.svg"));
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0xFF, 0xD8, 0xFF, 0xD9 }, "test.jpg"));
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0xFF, 0xD8, 0xFF, 0xD9 }, "test.jpeg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF, 0xD8, 0xFF, 0xDA }, "test.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF, 0xD8, 0xFF }, "test.jpg"));
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.svg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF }, "e.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { }, "empty.jpg"));
}
[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 = displayFormatter.Currency(test.Item1, test.Item3);
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}
@ -706,69 +688,22 @@ 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 settings, out error));
mainnet, out var settings, out var error));
Assert.Null(error);
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint,
HDFingerprint.TryParse("8bafd160", out hd) ? hd : default);
HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label);
Assert.Equal("49'/0'/0'", settings.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal(
@ -776,26 +711,28 @@ 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 { Segwit: false });
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
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 { Inner: DirectDerivationStrategy { Segwit: true } });
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
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 { Segwit: true });
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
Assert.Null(error);
// Specter
@ -850,19 +787,13 @@ namespace BTCPayServer.Tests
public static RateProviderFactory CreateBTCPayRateFactory()
{
ServiceCollection services = new ServiceCollection();
services.AddHttpClient();
BTCPayServerServices.RegisterRateSources(services);
var o = services.BuildServiceProvider();
return new RateProviderFactory(TestUtils.CreateHttpFactory(), o.GetService<IEnumerable<IRateProvider>>());
return new RateProviderFactory(TestUtils.CreateHttpFactory());
}
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public RateSourceInfo RateSourceInfo => new("spy", "SPY", "https://spy.org");
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
Hit = true;
@ -1263,14 +1194,21 @@ namespace BTCPayServer.Tests
{(null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{("non-json-content", new Dictionary<string, object>() {{string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, object>() {{string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>() {{"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})},
{
("{ invalidjson file here}",
new Dictionary<string, object>() {{String.Empty, "{ invalidjson file here}"}})
},
// Duplicate keys should not crash things
{("{ \"key\": true, \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})}
};
testCases.ForEach(tuple =>
{
Assert.Equal(tuple.expectedOutput, UIInvoiceController.PosDataParser.ParsePosData(string.IsNullOrEmpty(tuple.input) ? null : JToken.Parse(tuple.input)));
Assert.Equal(tuple.expectedOutput, UIInvoiceController.PosDataParser.ParsePosData(tuple.input));
});
}
[Fact]
@ -1511,14 +1449,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));
@ -1760,7 +1698,7 @@ namespace BTCPayServer.Tests
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var networkBTC = networkProvider.GetNetwork("BTC");
@ -1844,70 +1782,6 @@ namespace BTCPayServer.Tests
}
}
}
[Fact]
public void CanParseMetadata()
{
var metadata = InvoiceMetadata.FromJObject(JObject.Parse("{\"posData\": {\"test\":\"a\"}}"));
Assert.Equal(JObject.Parse("{\"test\":\"a\"}").ToString(), metadata.PosDataLegacy);
Assert.Equal(JObject.Parse("{\"test\":\"a\"}").ToString(), metadata.PosData.ToString());
// Legacy, as string
metadata = InvoiceMetadata.FromJObject(JObject.Parse("{\"posData\": \"{\\\"test\\\":\\\"a\\\"}\"}"));
Assert.Equal("{\"test\":\"a\"}", metadata.PosDataLegacy);
Assert.Equal(JObject.Parse("{\"test\":\"a\"}").ToString(), metadata.PosData.ToString());
metadata = InvoiceMetadata.FromJObject(JObject.Parse("{\"posData\": \"nobject\"}"));
Assert.Equal("nobject", metadata.PosDataLegacy);
Assert.Null(metadata.PosData);
metadata = InvoiceMetadata.FromJObject(JObject.Parse("{\"posData\": null}"));
Assert.Null(metadata.PosDataLegacy);
Assert.Null(metadata.PosData);
metadata = InvoiceMetadata.FromJObject(JObject.Parse("{}"));
Assert.Null(metadata.PosDataLegacy);
Assert.Null(metadata.PosData);
}
[Fact]
public void CanParseInvoiceEntityDerivationStrategies()
{
// We have 3 ways of serializing the derivation strategies:
// through "derivationStrategy", through "derivationStrategies" as a string, through "derivationStrategies" as JObject
// Let's check that InvoiceEntity is similar in all cases.
var legacy = new JObject()
{
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"
};
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", new BTCPayNetworkProvider(ChainName.Regtest).BTC);
scheme.Source = "ManualDerivationScheme";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf";
var legacy2 = new JObject()
{
["derivationStrategies"] = scheme.ToJson()
};
var newformat = new JObject()
{
["derivationStrategies"] = JObject.Parse(scheme.ToJson())
};
//new BTCPayNetworkProvider(ChainName.Regtest)
#pragma warning disable CS0618 // Type or member is obsolete
var formats = new[] { legacy, legacy2, newformat }
.Select(o =>
{
var entity = JsonConvert.DeserializeObject<InvoiceEntity>(o.ToString());
entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest);
return entity.DerivationStrategies.ToString();
})
.ToHashSet();
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Single(formats);
}
[Fact]
public void PaymentMethodIdConverterIsGraceful()
{

View File

@ -1,199 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Forms;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests;
[Trait("Fast", "Fast")]
public class FormTests : UnitTestBase
{
public FormTests(ITestOutputHelper helper) : base(helper)
{
}
[Fact]
public void CanParseForm()
{
var form = new Form()
{
Fields = new List<Field>
{
Field.Create("Enter your email", "item1", 1.ToString(), true, null, "email"),
Field.Create("Name", "item2", 2.ToString(), true, null),
Field.Create("Name", "invoice_test", 2.ToString(), true, null),
new Field
{
Name = "invoice",
Type = "fieldset",
Fields = new List<Field>
{
Field.Create("Name", "test", 3.ToString(), true, null),
Field.Create("Name", "item4", 4.ToString(), true, null),
Field.Create("Name", "item5", 5.ToString(), true, null),
}
}
}
};
var service = new FormDataService(null, null);
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
form = new Form()
{
Fields = new List<Field>
{
Field.Create("Enter your email", "item1", 1.ToString(), true, null, "email"),
Field.Create("Name", "item2", 2.ToString(), true, null),
Field.Create("Name", "invoice_item3", 2.ToString(), true, null),
new Field
{
Name = "invoice",
Type = "fieldset",
Fields = new List<Field> {Field.Create("Name", "test", 3.ToString(), true, null),}
}
}
};
Assert.True(service.IsFormSchemaValid(form.ToString(), out _, out _));
form.ApplyValuesFromForm(new FormCollection(new Dictionary<string, StringValues>()
{
{"item1", new StringValues("updated")},
{"item2", new StringValues("updated")},
{"invoice_item3", new StringValues("updated")},
{"invoice_test", new StringValues("updated")}
}));
foreach (var f in form.GetAllFields())
{
if (f.Field.Type == "fieldset")
continue;
Assert.Equal("updated", f.Field.Value);
}
form = new Form()
{
Fields = new List<Field>
{
Field.Create("Enter your email", "item1", 1.ToString(), true, null, "email"),
Field.Create("Name", "item2", 2.ToString(), true, null),
Field.Create("Name", "invoice_item3", 2.ToString(), true, null),
new Field
{
Name = "invoice",
Type = "fieldset",
Fields = new List<Field>
{
new() {Name = "test", Type = "text", Constant = true, Value = "original"}
}
}
}
};
form.ApplyValuesFromForm(new FormCollection(new Dictionary<string, StringValues>()
{
{"item1", new StringValues("updated")},
{"item2", new StringValues("updated")},
{"invoice_item3", new StringValues("updated")},
{"invoice_test", new StringValues("updated")}
}));
foreach (var f in form.GetAllFields())
{
var field = f.Field;
if (field.Type == "fieldset")
continue;
switch (f.FullName)
{
case "invoice_test":
Assert.Equal("original", field.Value);
break;
default:
Assert.Equal("updated", field.Value);
break;
}
}
form = new Form()
{
Fields = new List<Field>
{
Field.Create("Enter your email", "item1", 1.ToString(), true, null, "email"),
Field.Create("Name", "item2", 2.ToString(), true, null),
Field.Create("Name", "invoice_item3", 2.ToString(), true, null),
new Field
{
Name = "invoice",
Type = "fieldset",
Constant = true,
Fields = new List<Field>
{
new() {Name = "test", Type = "text", Value = "original"}
}
}
}
};
form.ApplyValuesFromForm(new FormCollection(new Dictionary<string, StringValues>()
{
{"item1", new StringValues("updated")},
{"item2", new StringValues("updated")},
{"invoice_item3", new StringValues("updated")},
{"invoice_test", new StringValues("updated")}
}));
foreach (var f in form.GetAllFields())
{
var field = f.Field;
if (field.Type == "fieldset")
continue;
switch (f.FullName)
{
case "invoice_test":
Assert.Equal("original", field.Value);
break;
default:
Assert.Equal("updated", field.Value);
break;
}
}
var obj = form.GetValues();
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
Clear(form);
form.SetValues(obj);
obj = form.GetValues();
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
form = new Form()
{
Fields = new List<Field>(){
new Field
{
Type = "fieldset",
Fields = new List<Field>
{
new() {Name = "test", Type = "text"}
}
}
}
};
form.SetValues(obj);
obj = form.GetValues();
Assert.Null(obj["test"].Value<string>());
form.SetValues(new JObject{ ["test"] = "hello" });
obj = form.GetValues();
Assert.Equal("hello", obj["test"].Value<string>());
}
private void Clear(Form form)
{
foreach (var f in form.Fields.Where(f => !f.Constant))
f.Value = null;
}
}

View File

@ -190,43 +190,6 @@ namespace BTCPayServer.Tests
await unrestricted.RevokeAPIKey(apiKey.ApiKey);
await AssertAPIError("apikey-not-found", () => unrestricted.RevokeAPIKey(apiKey.ApiKey));
// Admin create API key to new user
acc = tester.NewAccount();
await acc.GrantAccessAsync(isAdmin: true);
unrestricted = await acc.CreateClient();
var newUser = await unrestricted.CreateUser(new CreateApplicationUserRequest() { Email = Utils.GenerateEmail(), Password = "Kitten0@" });
var newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Id, new CreateApiKeyRequest()
{
Label = "Hello world",
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
});
var newUserClient = acc.CreateClientFromAPIKey(newUserAPIKey.ApiKey);
Assert.Equal(newUser.Id, (await newUserClient.GetCurrentUser()).Id);
// Admin delete it
await unrestricted.RevokeAPIKey(newUser.Id, newUserAPIKey.ApiKey);
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetCurrentUser());
// Admin create store
var store = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "Pouet lol" });
// Grant right to another user
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));
// if user is a guest or owner, then it should be ok
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id, Role = "Guest" });
await newUserClient.GetInvoices(store.Id);
}
[Fact(Timeout = TestTimeout)]
@ -288,23 +251,18 @@ namespace BTCPayServer.Tests
new CreatePointOfSaleAppRequest()
{
AppName = "test app from API",
Currency = "JPY",
Title = "test app title"
Currency = "JPY"
}
);
Assert.Equal("test app from API", app.Name);
Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("PointOfSale", app.AppType);
Assert.Equal("test app title", app.Title);
// Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () =>
{
await client.GetApp("some random ID lol");
});
await AssertHttpError(404, async () => {
await client.GetPosApp("some random ID lol");
});
// Test that we can retrieve the app data
var retrievedApp = await client.GetApp(app.Id);
@ -313,23 +271,10 @@ namespace BTCPayServer.Tests
Assert.Equal(app.AppType, retrievedApp.AppType);
// Test that we can update the app data
await client.UpdatePointOfSaleApp(
app.Id,
new CreatePointOfSaleAppRequest()
{
AppName = "new app name",
Title = "new app title"
}
);
// Test generic GET app endpoint first
await client.UpdatePointOfSaleApp(app.Id, new CreatePointOfSaleAppRequest() { AppName = "new app name" });
retrievedApp = await client.GetApp(app.Id);
Assert.Equal("new app name", retrievedApp.Name);
// Test the POS-specific endpoint also
var retrievedPosApp = await client.GetPosApp(app.Id);
Assert.Equal("new app name", retrievedPosApp.Name);
Assert.Equal("new app title", retrievedPosApp.Title);
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () =>
{
@ -346,7 +291,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateReadAndDeleteCrowdfundApp()
public async Task CanCreateCrowdfundApp()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -449,106 +394,10 @@ namespace BTCPayServer.Tests
);
// Test creating a crowdfund app
var app = await client.CreateCrowdfundApp(
user.StoreId,
new CreateCrowdfundAppRequest()
{
AppName = "test app from API",
Title = "test app title"
}
);
var app = await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() { AppName = "test app from API" });
Assert.Equal("test app from API", app.Name);
Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("Crowdfund", app.AppType);
// Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () => {
await client.GetApp("some random ID lol");
});
await AssertHttpError(404, async () => {
await client.GetCrowdfundApp("some random ID lol");
});
// Test that we can retrieve the app data
var retrievedApp = await client.GetApp(app.Id);
Assert.Equal(app.Name, retrievedApp.Name);
Assert.Equal(app.StoreId, retrievedApp.StoreId);
Assert.Equal(app.AppType, retrievedApp.AppType);
// Test the crowdfund-specific endpoint also
var retrievedPosApp = await client.GetCrowdfundApp(app.Id);
Assert.Equal(app.Name, retrievedPosApp.Name);
Assert.Equal(app.Title, retrievedPosApp.Title);
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () =>
{
await client.DeleteApp("some random ID lol");
});
// Test deleting the newly created app
await client.DeleteApp(retrievedApp.Id);
await AssertHttpError(404, async () => {
await client.GetApp(retrievedApp.Id);
});
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanGetAllApps()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
var posApp = await client.CreatePointOfSaleApp(
user.StoreId,
new CreatePointOfSaleAppRequest()
{
AppName = "test app from API",
Currency = "JPY"
}
);
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() { AppName = "test app from API" });
// Create another store and one app on it so we can get all apps from all stores for the user below
var newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" });
var newApp = await client.CreateCrowdfundApp(newStore.Id, new CreateCrowdfundAppRequest() { AppName = "new app" });
Assert.NotEqual(newApp.Id, user.StoreId);
// Get all apps for a specific store first
var apps = await client.GetAllApps(user.StoreId);
Assert.Equal(2, apps.Length);
Assert.Equal(posApp.Name, apps[0].Name);
Assert.Equal(posApp.StoreId, apps[0].StoreId);
Assert.Equal(posApp.AppType, apps[0].AppType);
Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
// Get all apps for all store now
apps = await client.GetAllApps();
Assert.Equal(3, apps.Length);
Assert.Equal(posApp.Name, apps[0].Name);
Assert.Equal(posApp.StoreId, apps[0].StoreId);
Assert.Equal(posApp.AppType, apps[0].AppType);
Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
Assert.Equal(newApp.Name, apps[2].Name);
Assert.Equal(newApp.StoreId, apps[2].StoreId);
Assert.Equal(newApp.AppType, apps[2].AppType);
}
[Fact(Timeout = TestTimeout)]
@ -692,8 +541,15 @@ namespace BTCPayServer.Tests
tester.PayTester.DisableRegistration = true;
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
await AssertValidationError(new[] { "Email" },
await AssertValidationError(new[] { "Email", "Password" },
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
await AssertValidationError(new[] { "Password" },
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() { Email = "test@gmail.com" }));
// Pass too simple
await AssertValidationError(new[] { "Password" },
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" }));
// We have no admin, so it should work
var user1 = await unauthClient.CreateUser(
@ -816,12 +672,10 @@ namespace BTCPayServer.Tests
public async Task CanUsePullPaymentViaAPI()
{
using var tester = CreateServerTester();
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var acc = tester.NewAccount();
await acc.GrantAccessAsync(true);
acc.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
acc.Register();
await acc.CreateStoreAsync();
var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId;
var client = await acc.CreateClient();
var result = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
@ -1002,8 +856,6 @@ namespace BTCPayServer.Tests
PaymentMethods = new[] { "BTC" }
});
await this.AssertAPIError("lnurl-not-supported", async () => await unauthenticated.GetPullPaymentLNURL(pp.Id));
destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
TestLogs.LogInformation("Try to pay it in BTC");
payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest()
@ -1054,60 +906,6 @@ namespace BTCPayServer.Tests
payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payout.State);
await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
// Test LNURL values
var test4 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
{
Name = "Test 3",
Amount = 12.303228134m,
Currency = "BTC",
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
});
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
Assert.IsType<string>(lnrURLs.LNURLBech32);
Assert.IsType<string>(lnrURLs.LNURLUri);
//permission test around auto approved pps and payouts
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
var approved = await acc.CreateClient(Policies.CanCreatePullPayments);
await AssertPermissionError(Policies.CanCreatePullPayments, async () =>
{
var pullPayment = await nonApproved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
{
Amount = 100,
Currency = "USD",
Name = "pull payment",
PaymentMethods = new[] { "BTC" },
AutoApproveClaims = true
});
});
await AssertPermissionError(Policies.CanCreatePullPayments, async () =>
{
var pullPayment = await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
PaymentMethod = "BTC",
Approved = true,
Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
});
});
var pullPayment = await approved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
{
Amount = 100,
Currency = "USD",
Name = "pull payment",
PaymentMethods = new[] { "BTC" },
AutoApproveClaims = true
});
var p = await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
PaymentMethod = "BTC",
Approved = true,
Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
});
}
[Fact]
@ -1254,30 +1052,10 @@ namespace BTCPayServer.Tests
var newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" });
//update store
Assert.Empty(newStore.PaymentMethodCriteria);
await client.GenerateOnChainWallet(newStore.Id, "BTC", new GenerateOnChainWalletRequest());
var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B", PaymentMethodCriteria = new List<PaymentMethodCriteriaData>()
{
new()
{
Amount = 10,
Above = true,
PaymentMethod = "BTC",
CurrencyCode = "USD"
}
}});
var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" });
Assert.Equal("B", updatedStore.Name);
var s = (await client.GetStore(newStore.Id));
Assert.Equal("B", s.Name);
var pmc = Assert.Single(s.PaymentMethodCriteria);
//check that pmc equals the one we set
Assert.Equal(10, pmc.Amount);
Assert.True(pmc.Above);
Assert.Equal("BTC", pmc.PaymentMethod);
Assert.Equal("USD", pmc.CurrencyCode);
updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B"});
Assert.Empty(newStore.PaymentMethodCriteria);
Assert.Equal("B", (await client.GetStore(newStore.Id)).Name);
//list stores
var stores = await client.GetStores();
var storeIds = stores.Select(data => data.Id);
@ -1305,21 +1083,15 @@ 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)
@ -1403,6 +1175,10 @@ namespace BTCPayServer.Tests
Password = Guid.NewGuid().ToString()
}));
await AssertValidationError(new[] { "Password" }, async () =>
await clientServer.CreateUser(
new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", }));
await AssertValidationError(new[] { "Email" }, async () =>
await clientServer.CreateUser(
new CreateApplicationUserRequest() { Password = Guid.NewGuid().ToString() }));
@ -1759,9 +1535,7 @@ namespace BTCPayServer.Tests
var db = tester.PayTester.GetService<Data.ApplicationDbContextFactory>();
using var ctx = db.CreateContext();
var dbInvoice = await ctx.Invoices.FindAsync(oldInvoice.Id);
#pragma warning disable CS0618 // Type or member is obsolete
dbInvoice.Blob = ZipUtils.Zip(invoiceV1);
#pragma warning restore CS0618 // Type or member is obsolete
await ctx.SaveChangesAsync();
var newInvoice = await AssertInvoiceMetadata();
@ -2063,7 +1837,7 @@ namespace BTCPayServer.Tests
//get
var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
Assert.True(JObject.DeepEquals(newInvoice.Metadata, invoice.Metadata));
Assert.Equal(newInvoice.Metadata, invoice.Metadata);
var paymentMethods = await viewOnly.GetInvoicePaymentMethods(user.StoreId, newInvoice.Id);
Assert.Single(paymentMethods);
var paymentMethod = paymentMethods.First();
@ -2442,13 +2216,7 @@ namespace BTCPayServer.Tests
// Amount received might be bigger because of internal implementation shit from lightning
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
// check payments list for store node
var payments = await client.GetLightningPayments(user.StoreId, "BTC");
Assert.NotEmpty(payments);
Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11);
// Node info
info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
@ -3704,9 +3472,6 @@ namespace BTCPayServer.Tests
new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1", Spread = 10m, }))
.IsCustomScript);
Assert.Equal(0.9m,
Assert.Single(await clientBasic.GetStoreRates(user.StoreId, new[] { "BTC_XYZ" })).Rate);
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
Assert.NotNull(config);
Assert.NotNull(config.EffectiveScript);
@ -3940,7 +3705,8 @@ 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;
@ -3980,22 +3746,22 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
// Test: GetDepositAddress, unauth
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
await AssertHttpError(401, async () => await unauthClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
// Test: GetDepositAddress, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
await AssertHttpError(403, async () => await managerClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
// Test: GetDepositAddress, wrong payment method
await AssertApiError( 400, "unsupported-payment-method", async () => await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
await AssertHttpError(400, async () => await depositClient.GetDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
// Test: GetDepositAddress, wrong store ID
await AssertHttpError(403, async () => await depositClient.GetCustodianAccountDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
await AssertHttpError(403, async () => await depositClient.GetDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
// Test: GetDepositAddress, wrong account ID
await AssertHttpError(404, async () => await depositClient.GetCustodianAccountDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
await AssertHttpError(404, async () => await depositClient.GetDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
// Test: GetDepositAddress, correct payment method
var depositAddress = await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
var depositAddress = await depositClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
Assert.NotNull(depositAddress);
Assert.Equal(MockCustodian.DepositAddress, depositAddress.Address);
@ -4053,13 +3819,13 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
// Test: GetTradeQuote, unauth
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
await AssertHttpError(401, async () => await unauthClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
// Test: GetTradeQuote, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
await AssertHttpError(403, async () => await managerClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
// Test: GetTradeQuote, auth, correct permission
var tradeQuote = await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
var tradeQuote = await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
Assert.NotNull(tradeQuote);
Assert.Equal(MockCustodian.TradeFromAsset, tradeQuote.FromAsset);
Assert.Equal(MockCustodian.TradeToAsset, tradeQuote.ToAsset);
@ -4067,30 +3833,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.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
// Test: GetTradeQuote, 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"));
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"));
// Test: wrong account ID
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
// Test: wrong store ID
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
await AssertHttpError(403, async () => await tradeClient.GetTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
// Test: GetTradeInfo, unauth
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
await AssertHttpError(401, async () => await unauthClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
// Test: GetTradeInfo, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
await AssertHttpError(403, async () => await managerClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
// Test: GetTradeInfo, auth, correct permission
var tradeResult = await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId);
var tradeResult = await tradeClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId);
Assert.NotNull(tradeResult);
Assert.Equal(accountId, tradeResult.AccountId);
Assert.Equal(mockCustodian.Code, tradeResult.CustodianCode);
@ -4110,93 +3876,66 @@ 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.GetCustodianAccountTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
// Test: wrong account ID
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
// Test: wrong store ID
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
// Test: wrong account ID
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
// Test: wrong store ID
await AssertHttpError(403, async () => await tradeClient.GetTradeInfo("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, qty);
var createWithdrawalRequestPercentage = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
await AssertHttpError(401, async () => await unauthClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalAmount);
await AssertHttpError(401, async () => await unauthClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
// Test: CreateWithdrawal, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
await AssertHttpError(403, async () => await managerClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
// Test: CreateWithdrawal, correct payment method, correct amount
var withdrawResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest);
var withdrawResponse = await withdrawalClient.CreateWithdrawal(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", qty);
await AssertApiError( 400, "unsupported-payment-method", async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", MockCustodian.WithdrawalAmount);
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
// Test: CreateWithdrawal, wrong account ID
await AssertHttpError(404, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
await AssertHttpError(404, async () => await withdrawalClient.CreateWithdrawal(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.CreateCustodianAccountWithdrawal( "WRONG-STORE-ID",accountId, createWithdrawalRequest));
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal("WRONG-STORE-ID", accountId, createWithdrawalRequest));
// Test: CreateWithdrawal, correct payment method, wrong amount
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
await AssertHttpError(400, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, new decimal(0.666));
await AssertHttpError(400, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
// Test: GetWithdrawalInfo, unauth
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
await AssertHttpError(401, async () => await unauthClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
// Test: GetWithdrawalInfo, auth, but wrong permission
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
await AssertHttpError(403, async () => await managerClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
// Test: GetWithdrawalInfo, auth, correct permission
var withdrawalInfo = await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
var withdrawalInfo = await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
AssertMockWithdrawal(withdrawalInfo, custodianAccountData);
// Test: GetWithdrawalInfo, wrong withdrawal ID
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
// Test: wrong account ID
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(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.GetCustodianAccountWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
await AssertHttpError(403, async () => await withdrawalClient.GetWithdrawalInfo("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
@ -4204,11 +3943,12 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
// TODO create a mock custodian with only ICustodian + ICanDeposit
}
private void AssertMockWithdrawal(WithdrawalBaseResponseData withdrawResponse, CustodianAccountData account)
private void AssertMockWithdrawal(WithdrawalResponseData 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);
@ -4222,20 +3962,10 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
Assert.Equal(MockCustodian.WithdrawalFee, withdrawResponse.LedgerEntries[1].Qty);
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, withdrawResponse.LedgerEntries[1].Type);
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);
}
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawResponse.TargetAddress);
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawResponse.TransactionId);
Assert.Equal(MockCustodian.WithdrawalId, withdrawResponse.WithdrawalId);
Assert.NotEqual(default, withdrawResponse.CreatedTime);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians;
@ -25,9 +24,6 @@ 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";
@ -56,7 +52,7 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
return Task.FromResult(r);
}
public Task<Form> GetConfigForm(CancellationToken cancellationToken = default)
public Task<Form> GetConfigForm(JObject config, string locale, CancellationToken cancellationToken = default)
{
return null;
}
@ -139,38 +135,14 @@ 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> 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)
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
{
if (paymentMethod == WithdrawalPaymentMethod)
{
if (amount == WithdrawalAmount)
{
return Task.FromResult(CreateWithdrawSimulationResult());
return Task.FromResult(CreateWithdrawResult());
}
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}");

View File

@ -10,8 +10,6 @@ namespace BTCPayServer.Tests.Mocks
{
public List<PairRate> ExchangeRates { get; set; } = new List<PairRate>();
public RateSourceInfo RateSourceInfo => new RateSourceInfo("mock", "Mock", "https://mock.rf");
public MockRateProvider()
{

View File

@ -2,9 +2,10 @@ 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;
@ -31,11 +32,10 @@ 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 = PointOfSaleAppType.AppType;
var appType = AppType.PointOfSale.ToString();
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
Assert.IsType<RedirectToActionResult>(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 };

View File

@ -109,12 +109,13 @@ namespace BTCPayServer.Tests
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var controller = tester.PayTester.GetService<PayJoinEndpointController>();
var utxos = new[] { FakeUTXO(1m) };
//Only one utxo, so obvious result
var utxos = new[] { FakeUTXO(1.0m) };
var paymentAmount = 0.5m;
var otherOutputs = new[] { 0.5m };
var inputs = new[] { 1m };
var result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
Assert.Contains(result.selectedUTXO, utxo => utxos.Contains(utxo));
//no matter what here, no good selection, it seems that payment with 1 utxo generally makes payjoin coin selection unperformant
@ -123,7 +124,7 @@ namespace BTCPayServer.Tests
otherOutputs = new[] { 0.5m };
inputs = new[] { 1m };
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
//when there is no change, anything works
utxos = new[] { FakeUTXO(1), FakeUTXO(0.1m), FakeUTXO(0.001m), FakeUTXO(0.003m) };
@ -131,31 +132,7 @@ namespace BTCPayServer.Tests
otherOutputs = new decimal[0];
inputs = new[] { 0.03m, 0.07m };
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
// We want to make a transaction such that
// min(out) < min(in)
// Original transaction is:
// 0.5 -> 0.3 , 0.1
// When chosing a new utxo x, we have the modified tx
// 0.5 , x -> 0.3 , (0.1+x)
// We need:
// min(0.3, 0.1+x) < min(0.5, x)
// Any x > 0.3 should be fine
utxos = new[] { FakeUTXO(0.2m), FakeUTXO(0.3m), FakeUTXO(0.31m) };
paymentAmount = 0.1m;
otherOutputs = new decimal[] { 0.3m };
inputs = new[] { 0.5m };
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType);
Assert.Equal(0.31m, result.selectedUTXO[0].Value.GetValue(network));
// If the 0.31m wasn't available, no selection heuristic based
utxos = new[] { FakeUTXO(0.2m), FakeUTXO(0.3m) };
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
Assert.Equal(0.2m, result.selectedUTXO[0].Value.GetValue(network));
}
@ -221,7 +198,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));

View File

@ -86,14 +86,8 @@ namespace BTCPayServer.Tests
Driver.AssertNoError();
}
public void PayInvoice(bool mine = false, decimal? amount= null)
public void PayInvoice(bool mine = false)
{
if (amount is not null)
{
Driver.FindElement(By.Id("test-payment-amount")).Clear();
Driver.FindElement(By.Id("test-payment-amount")).SendKeys(amount.ToString());
}
Driver.FindElement(By.Id("FakePayment")).Click();
if (mine)
{
@ -209,6 +203,7 @@ namespace BTCPayServer.Tests
{
var isImport = !string.IsNullOrEmpty(seed);
GoToWalletSettings(cryptoCode);
// Replace previous wallet case
if (Driver.PageSource.Contains("id=\"ChangeWalletLink\""))
{
@ -555,7 +550,7 @@ namespace BTCPayServer.Tests
walletId ??= WalletId;
GoToWallet(walletId, WalletsNavPages.Receive);
Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("value");
var addressStr = Driver.FindElement(By.Id("address")).GetAttribute("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
for (var i = 0; i < coins; i++)
{

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