Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
3192db6fec |
@ -41,10 +41,9 @@ jobs:
|
||||
- run:
|
||||
command: |
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
GIT_COMMIT=$(git rev-parse HEAD)
|
||||
#
|
||||
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile .
|
||||
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 -f amd64.Dockerfile .
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile .
|
||||
sudo docker build --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 -f amd64.Dockerfile .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64
|
||||
@ -58,10 +57,9 @@ jobs:
|
||||
command: |
|
||||
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
GIT_COMMIT=$(git rev-parse HEAD)
|
||||
#
|
||||
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile .
|
||||
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 -f arm32v7.Dockerfile .
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile .
|
||||
sudo docker build --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 -f arm32v7.Dockerfile .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7
|
||||
@ -75,10 +73,9 @@ jobs:
|
||||
command: |
|
||||
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
GIT_COMMIT=$(git rev-parse HEAD)
|
||||
#
|
||||
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f arm64v8.Dockerfile .
|
||||
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --build-arg CONFIGURATION_NAME=Altcoins-Release --pull -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 -f arm64v8.Dockerfile .
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f arm64v8.Dockerfile .
|
||||
sudo docker build --build-arg CONFIGURATION_NAME=Altcoins-Release --pull -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 -f arm64v8.Dockerfile .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8
|
||||
|
@ -12,7 +12,7 @@ indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
|
||||
[*.json]
|
||||
[launchSettings.json]
|
||||
indent_size = 2
|
||||
|
||||
# C# files
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -288,6 +288,10 @@ __pycache__/
|
||||
*.xsd.cs
|
||||
/BTCPayServer/Build/dockerfiles
|
||||
|
||||
# Bundling JS/CSS
|
||||
BTCPayServer/wwwroot/bundles/*
|
||||
!BTCPayServer/wwwroot/bundles/.gitignore
|
||||
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
|
@ -32,9 +32,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -1,18 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Abstractions
|
||||
{
|
||||
public class CamelCaseSerializerSettings
|
||||
{
|
||||
static CamelCaseSerializerSettings()
|
||||
{
|
||||
Settings = new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
Serializer = JsonSerializer.Create(Settings);
|
||||
}
|
||||
public static readonly JsonSerializerSettings Settings;
|
||||
public static readonly JsonSerializer Serializer;
|
||||
}
|
||||
}
|
@ -14,12 +14,6 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
private const string ACTIVE_ID_KEY = "ActiveId";
|
||||
private const string ActivePageClass = "active";
|
||||
|
||||
public enum DateDisplayFormat
|
||||
{
|
||||
Localized,
|
||||
Relative
|
||||
}
|
||||
|
||||
public static void SetActivePage<T>(this ViewDataDictionary viewData, T activePage, string title = null, string activeId = null)
|
||||
where T : IConvertible
|
||||
{
|
||||
@ -92,29 +86,26 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
return categoryAndPageMatch && idMatch ? ActivePageClass : null;
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate(this DateTimeOffset date, DateDisplayFormat format = DateDisplayFormat.Localized)
|
||||
public static HtmlString ToBrowserDate(this DateTimeOffset date)
|
||||
{
|
||||
var relative = date.ToTimeAgo();
|
||||
var initial = format.ToString().ToLower();
|
||||
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
var displayDate = format == DateDisplayFormat.Relative ? relative : date.ToString("g", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<time datetime=\"{dateTime}\" data-relative=\"{relative}\" data-initial=\"{initial}\">{displayDate}</time>");
|
||||
var displayDate = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<span class='localizeDate'>{displayDate}</span>");
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate(this DateTime date, DateDisplayFormat format = DateDisplayFormat.Localized)
|
||||
public static HtmlString ToBrowserDate(this DateTime date)
|
||||
{
|
||||
var relative = date.ToTimeAgo();
|
||||
var initial = format.ToString().ToLower();
|
||||
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
var displayDate = format == DateDisplayFormat.Relative ? relative : date.ToString("g", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<time datetime=\"{dateTime}\" data-relative=\"{relative}\" data-initial=\"{initial}\">{displayDate}</time>");
|
||||
var displayDate = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<span class='localizeDate'>{displayDate}</span>");
|
||||
}
|
||||
|
||||
public static string ToTimeAgo(this DateTimeOffset date) => (DateTimeOffset.UtcNow - date).ToTimeAgo();
|
||||
|
||||
public static string ToTimeAgo(this DateTime date) => (DateTimeOffset.UtcNow - date).ToTimeAgo();
|
||||
|
||||
public static string ToTimeAgo(this TimeSpan diff) => diff.TotalSeconds > 0 ? $"{diff.TimeString()} ago" : $"in {diff.Negate().TimeString()}";
|
||||
public static string ToTimeAgo(this DateTimeOffset date)
|
||||
{
|
||||
var diff = DateTimeOffset.UtcNow - date;
|
||||
var formatted = diff.TotalSeconds > 0
|
||||
? $"{diff.TimeString()} ago"
|
||||
: $"in {diff.Negate().TimeString()}";
|
||||
return formatted;
|
||||
}
|
||||
|
||||
public static string TimeString(this TimeSpan timeSpan)
|
||||
{
|
||||
@ -126,14 +117,16 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
{
|
||||
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
|
||||
}
|
||||
return timeSpan.Days < 1
|
||||
? $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}"
|
||||
: $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
|
||||
if (timeSpan.Days < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
|
||||
}
|
||||
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
|
||||
}
|
||||
|
||||
private static string Plural(int value)
|
||||
{
|
||||
return value == 1 ? string.Empty : "s";
|
||||
return value > 1 ? "s" : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,64 +1,35 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Form;
|
||||
|
||||
public class Field
|
||||
public abstract class Field
|
||||
{
|
||||
public static Field Create(string label, string name, string value, bool required, string helpText, string type = "text")
|
||||
{
|
||||
return new Field()
|
||||
{
|
||||
Label = label,
|
||||
Name = name,
|
||||
Value = value,
|
||||
OriginalValue = value,
|
||||
Required = required,
|
||||
HelpText = helpText,
|
||||
Type = type
|
||||
};
|
||||
}
|
||||
// The name of the HTML5 node. Should be used as the key for the posted data.
|
||||
public string Name;
|
||||
|
||||
public bool Hidden;
|
||||
|
||||
// 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" };
|
||||
}
|
||||
|
||||
|
||||
// The name of the HTML5 node. Should be used as the key for the posted data.
|
||||
public string Name;
|
||||
|
||||
// The translated label of the field.
|
||||
public string Label;
|
||||
|
||||
// The value field is what is currently in the DB or what the user entered, but possibly not saved yet due to validation errors.
|
||||
// If this is the first the user sees the form, then value and original value are the same. Value changes as the user starts interacting with the form.
|
||||
public string Value;
|
||||
|
||||
public bool Required;
|
||||
|
||||
// The translated label of the field.
|
||||
public string Label;
|
||||
|
||||
// The original value is the value that is currently saved in the backend. A "reset" button can be used to revert back to this. Should only be set from the constructor.
|
||||
public string OriginalValue;
|
||||
|
||||
// A useful note shown below the field or via a tooltip / info icon. Should be translated for the user.
|
||||
public string HelpText;
|
||||
|
||||
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
public List<Field> Fields { get; set; } = new();
|
||||
|
||||
|
||||
// The field is considered "valid" if there are no validation errors
|
||||
public List<string> ValidationErrors = new List<string>();
|
||||
|
||||
public virtual bool IsValid()
|
||||
public bool Required = false;
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
|
||||
return ValidationErrors.Count == 0;
|
||||
}
|
||||
}
|
||||
|
14
BTCPayServer.Abstractions/Form/Fieldset.cs
Normal file
14
BTCPayServer.Abstractions/Form/Fieldset.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Form;
|
||||
|
||||
public class Fieldset
|
||||
{
|
||||
public Fieldset()
|
||||
{
|
||||
this.Fields = new List<Field>();
|
||||
}
|
||||
|
||||
public string Label { get; set; }
|
||||
public List<Field> Fields { get; set; }
|
||||
}
|
@ -1,156 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Form;
|
||||
|
||||
public class Form
|
||||
{
|
||||
#nullable enable
|
||||
public static Form Parse(string str)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(str);
|
||||
return JObject.Parse(str).ToObject<Form>(CamelCaseSerializerSettings.Serializer) ?? throw new InvalidOperationException("Impossible to deserialize Form");
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
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();
|
||||
|
||||
// Groups of fields in the form
|
||||
public List<Field> Fields { get; set; } = new();
|
||||
public List<Fieldset> Fieldsets { get; set; } = new();
|
||||
|
||||
|
||||
// Are all the fields valid in the form?
|
||||
public bool IsValid()
|
||||
{
|
||||
return Fields.Select(f => f.IsValid()).All(o => o);
|
||||
foreach (var fieldset in Fieldsets)
|
||||
{
|
||||
foreach (var field in fieldset.Fields)
|
||||
{
|
||||
if (!field.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Field GetFieldByName(string name)
|
||||
{
|
||||
return GetFieldByName(name, Fields, null);
|
||||
}
|
||||
|
||||
private static Field GetFieldByName(string name, List<Field> fields, string prefix)
|
||||
{
|
||||
prefix ??= string.Empty;
|
||||
foreach (var field in fields)
|
||||
foreach (var fieldset in Fieldsets)
|
||||
{
|
||||
var currentPrefix = prefix;
|
||||
if (!string.IsNullOrEmpty(field.Name))
|
||||
foreach (var field in fieldset.Fields)
|
||||
{
|
||||
|
||||
currentPrefix = $"{prefix}{field.Name}";
|
||||
if (currentPrefix.Equals(name, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (name.Equals(field.Name))
|
||||
{
|
||||
return field;
|
||||
}
|
||||
|
||||
currentPrefix += "_";
|
||||
}
|
||||
|
||||
var subFieldResult = GetFieldByName(name, field.Fields, currentPrefix);
|
||||
if (subFieldResult is not null)
|
||||
{
|
||||
return subFieldResult;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<string> GetAllNames()
|
||||
{
|
||||
return GetAllNames(Fields);
|
||||
}
|
||||
|
||||
private static List<string> GetAllNames(List<Field> fields)
|
||||
{
|
||||
var names = new List<string>();
|
||||
|
||||
foreach (var field in fields)
|
||||
foreach (var fieldset in Fieldsets)
|
||||
{
|
||||
string prefix = string.Empty;
|
||||
if (!string.IsNullOrEmpty(field.Name))
|
||||
foreach (var field in fieldset.Fields)
|
||||
{
|
||||
names.Add(field.Name);
|
||||
prefix = $"{field.Name}_";
|
||||
}
|
||||
|
||||
if (field.Fields.Any())
|
||||
{
|
||||
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(IFormCollection form)
|
||||
{
|
||||
var names = GetAllNames();
|
||||
foreach (var name in names)
|
||||
{
|
||||
var field = GetFieldByName(name);
|
||||
if (field is null || !form.TryGetValue(name, out var val))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
field.Value = val;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetValues()
|
||||
{
|
||||
return GetValues(Fields);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> GetValues(List<Field> fields)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
foreach (Field field in fields)
|
||||
{
|
||||
var name = field.Name ?? string.Empty;
|
||||
if (field.Fields.Any())
|
||||
{
|
||||
var values = GetValues(fields);
|
||||
values.Remove(string.Empty, out var keylessValue);
|
||||
|
||||
result.TryAdd(name, values);
|
||||
|
||||
if (keylessValue is not Dictionary<string, object> dict) continue;
|
||||
foreach (KeyValuePair<string,object> keyValuePair in dict)
|
||||
{
|
||||
result.TryAdd(keyValuePair.Key, keyValuePair.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TryAdd(name, field.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
19
BTCPayServer.Abstractions/Form/TextField.cs
Normal file
19
BTCPayServer.Abstractions/Form/TextField.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace BTCPayServer.Abstractions.Form;
|
||||
|
||||
public class TextField : Field
|
||||
{
|
||||
public TextField(string label, string name, string value, bool required, string helpText)
|
||||
{
|
||||
this.Label = label;
|
||||
this.Name = name;
|
||||
this.Value = value;
|
||||
this.OriginalValue = value;
|
||||
this.Required = required;
|
||||
this.HelpText = helpText;
|
||||
this.Type = "text";
|
||||
}
|
||||
|
||||
// TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types.
|
||||
|
||||
|
||||
}
|
@ -1,12 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
@ -15,7 +8,7 @@ namespace BTCPayServer.Abstractions.TagHelpers;
|
||||
|
||||
// Make sure that <svg><use href=/ are correctly working if rootpath is present
|
||||
[HtmlTargetElement("use", Attributes = "href")]
|
||||
public class SVGUse : UrlResolutionTagHelper2
|
||||
public class SVGUse : UrlResolutionTagHelper
|
||||
{
|
||||
private readonly IFileVersionProvider _fileVersionProvider;
|
||||
|
||||
@ -28,6 +21,5 @@ public class SVGUse : UrlResolutionTagHelper2
|
||||
var attr = output.Attributes["href"].Value.ToString();
|
||||
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
|
||||
output.Attributes.SetAttribute("href", attr);
|
||||
base.Process(context, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,314 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
|
||||
namespace BTCPayServer.Abstractions.TagHelpers
|
||||
{
|
||||
// A copy of https://github.com/dotnet/aspnetcore/blob/39f0e0b8f40b4754418f81aef0de58a9204a1fe5/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs
|
||||
// slightly modified to also work on use tag.
|
||||
public class UrlResolutionTagHelper2 : TagHelper
|
||||
{
|
||||
// Valid whitespace characters defined by the HTML5 spec.
|
||||
private static readonly char[] ValidAttributeWhitespaceChars =
|
||||
new[] { '\t', '\n', '\u000C', '\r', ' ' };
|
||||
private static readonly Dictionary<string, string[]> ElementAttributeLookups =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "use", new[] { "href" } },
|
||||
{ "a", new[] { "href" } },
|
||||
{ "applet", new[] { "archive" } },
|
||||
{ "area", new[] { "href" } },
|
||||
{ "audio", new[] { "src" } },
|
||||
{ "base", new[] { "href" } },
|
||||
{ "blockquote", new[] { "cite" } },
|
||||
{ "button", new[] { "formaction" } },
|
||||
{ "del", new[] { "cite" } },
|
||||
{ "embed", new[] { "src" } },
|
||||
{ "form", new[] { "action" } },
|
||||
{ "html", new[] { "manifest" } },
|
||||
{ "iframe", new[] { "src" } },
|
||||
{ "img", new[] { "src", "srcset" } },
|
||||
{ "input", new[] { "src", "formaction" } },
|
||||
{ "ins", new[] { "cite" } },
|
||||
{ "link", new[] { "href" } },
|
||||
{ "menuitem", new[] { "icon" } },
|
||||
{ "object", new[] { "archive", "data" } },
|
||||
{ "q", new[] { "cite" } },
|
||||
{ "script", new[] { "src" } },
|
||||
{ "source", new[] { "src", "srcset" } },
|
||||
{ "track", new[] { "src" } },
|
||||
{ "video", new[] { "poster", "src" } },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="UrlResolutionTagHelper"/>.
|
||||
/// </summary>
|
||||
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
|
||||
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
|
||||
public UrlResolutionTagHelper2(IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder)
|
||||
{
|
||||
UrlHelperFactory = urlHelperFactory;
|
||||
HtmlEncoder = htmlEncoder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Order => -1000 - 999;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IUrlHelperFactory"/>.
|
||||
/// </summary>
|
||||
protected IUrlHelperFactory UrlHelperFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HtmlEncoder"/>.
|
||||
/// </summary>
|
||||
protected HtmlEncoder HtmlEncoder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ViewContext"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeNotBound]
|
||||
[ViewContext]
|
||||
public ViewContext ViewContext { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(output);
|
||||
|
||||
if (output.TagName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ElementAttributeLookups.TryGetValue(output.TagName, out var attributeNames))
|
||||
{
|
||||
for (var i = 0; i < attributeNames.Length; i++)
|
||||
{
|
||||
ProcessUrlAttribute(attributeNames[i], output);
|
||||
}
|
||||
}
|
||||
|
||||
// itemid can be present on any HTML element.
|
||||
ProcessUrlAttribute("itemid", output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves and updates URL values starting with '~/' (relative to the application's 'webroot' setting) for
|
||||
/// <paramref name="output"/>'s <see cref="TagHelperOutput.Attributes"/> whose
|
||||
/// <see cref="TagHelperAttribute.Name"/> is <paramref name="attributeName"/>.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">The attribute name used to lookup values to resolve.</param>
|
||||
/// <param name="output">The <see cref="TagHelperOutput"/>.</param>
|
||||
protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(attributeName);
|
||||
ArgumentNullException.ThrowIfNull(output);
|
||||
|
||||
var attributes = output.Attributes;
|
||||
// Read interface .Count once rather than per iteration
|
||||
var attributesCount = attributes.Count;
|
||||
for (var i = 0; i < attributesCount; i++)
|
||||
{
|
||||
var attribute = attributes[i];
|
||||
if (!string.Equals(attribute.Name, attributeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attribute.Value is string stringValue)
|
||||
{
|
||||
if (TryResolveUrl(stringValue, resolvedUrl: out string? resolvedUrl))
|
||||
{
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
resolvedUrl,
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (attribute.Value is IHtmlContent htmlContent)
|
||||
{
|
||||
var htmlString = htmlContent as HtmlString;
|
||||
if (htmlString != null)
|
||||
{
|
||||
// No need for a StringWriter in this case.
|
||||
stringValue = htmlString.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var writer = new StringWriter();
|
||||
htmlContent.WriteTo(writer, HtmlEncoder);
|
||||
stringValue = writer.ToString();
|
||||
}
|
||||
|
||||
if (TryResolveUrl(stringValue, resolvedUrl: out IHtmlContent? resolvedUrl))
|
||||
{
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
resolvedUrl,
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
else if (htmlString == null)
|
||||
{
|
||||
// Not a ~/ URL. Just avoid re-encoding the attribute value later.
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
new HtmlString(stringValue),
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to resolve.</param>
|
||||
/// <param name="resolvedUrl">Absolute URL beginning with the application's virtual root. <c>null</c> if
|
||||
/// <paramref name="url"/> could not be resolved.</param>
|
||||
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
|
||||
protected bool TryResolveUrl(string url, out string? resolvedUrl)
|
||||
{
|
||||
resolvedUrl = null;
|
||||
var start = FindRelativeStart(url);
|
||||
if (start == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmedUrl = CreateTrimmedString(url, start);
|
||||
|
||||
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
|
||||
resolvedUrl = urlHelper.Content(trimmedUrl);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to resolve.</param>
|
||||
/// <param name="resolvedUrl">
|
||||
/// Absolute URL beginning with the application's virtual root. <c>null</c> if <paramref name="url"/> could
|
||||
/// not be resolved.
|
||||
/// </param>
|
||||
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
|
||||
protected bool TryResolveUrl(string url, [NotNullWhen(true)] out IHtmlContent? resolvedUrl)
|
||||
{
|
||||
resolvedUrl = null;
|
||||
var start = FindRelativeStart(url);
|
||||
if (start == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmedUrl = CreateTrimmedString(url, start);
|
||||
|
||||
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
|
||||
var appRelativeUrl = urlHelper.Content(trimmedUrl);
|
||||
var postTildeSlashUrlValue = trimmedUrl.Substring(2);
|
||||
|
||||
if (!appRelativeUrl.EndsWith(postTildeSlashUrlValue, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
resolvedUrl = new EncodeFirstSegmentContent(
|
||||
appRelativeUrl,
|
||||
appRelativeUrl.Length - postTildeSlashUrlValue.Length,
|
||||
postTildeSlashUrlValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindRelativeStart(string url)
|
||||
{
|
||||
if (url == null || url.Length < 2)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var maxTestLength = url.Length - 2;
|
||||
|
||||
var start = 0;
|
||||
for (; start < url.Length; start++)
|
||||
{
|
||||
if (start > maxTestLength)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!IsCharWhitespace(url[start]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Before doing more work, ensure that the URL we're looking at is app-relative.
|
||||
if (url[start] != '~' || url[start + 1] != '/')
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
private static string CreateTrimmedString(string input, int start)
|
||||
{
|
||||
var end = input.Length - 1;
|
||||
for (; end >= start; end--)
|
||||
{
|
||||
if (!IsCharWhitespace(input[end]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var len = end - start + 1;
|
||||
|
||||
// Substring returns same string if start == 0 && len == Length
|
||||
return input.Substring(start, len);
|
||||
}
|
||||
|
||||
private static bool IsCharWhitespace(char ch)
|
||||
{
|
||||
return ValidAttributeWhitespaceChars.AsSpan().IndexOf(ch) != -1;
|
||||
}
|
||||
|
||||
private sealed class EncodeFirstSegmentContent : IHtmlContent
|
||||
{
|
||||
private readonly string _firstSegment;
|
||||
private readonly int _firstSegmentLength;
|
||||
private readonly string _secondSegment;
|
||||
|
||||
public EncodeFirstSegmentContent(string firstSegment, int firstSegmentLength, string secondSegment)
|
||||
{
|
||||
_firstSegment = firstSegment;
|
||||
_firstSegmentLength = firstSegmentLength;
|
||||
_secondSegment = secondSegment;
|
||||
}
|
||||
|
||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||
{
|
||||
encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength);
|
||||
writer.Write(_secondSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.1</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.0</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@ -28,8 +28,8 @@
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.15" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.14" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.12" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.10" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -19,17 +19,6 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<PointOfSaleAppData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<CrowdfundAppData> CreateCrowdfundApp(string storeId,
|
||||
CreateCrowdfundAppRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/apps/crowdfund", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<CrowdfundAppData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PointOfSaleAppData> UpdatePointOfSaleApp(string appId,
|
||||
CreatePointOfSaleAppRequest request, CancellationToken token = default)
|
||||
|
@ -128,18 +128,5 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PullPaymentData> RefundInvoice(
|
||||
string storeId,
|
||||
string invoiceId,
|
||||
RefundInvoiceRequest request,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/refund", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<PullPaymentData>(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,24 +95,6 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningInvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData[]> GetLightningInvoices(string cryptoCode,
|
||||
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = new Dictionary<string, object>();
|
||||
if (pendingOnly is bool v)
|
||||
{
|
||||
queryPayload.Add("pendingOnly", v.ToString());
|
||||
}
|
||||
if (offsetIndex is > 0)
|
||||
{
|
||||
queryPayload.Add("offsetIndex", offsetIndex);
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", queryPayload), token);
|
||||
return await HandleResponse<LightningInvoiceData[]>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
|
@ -65,7 +65,7 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<string>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningPaymentData> PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
|
||||
public virtual async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
@ -73,7 +73,7 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<LightningPaymentData>(response);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningPaymentData> GetLightningPayment(string storeId, string cryptoCode,
|
||||
@ -97,24 +97,6 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningInvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData[]> GetLightningInvoices(string storeId, string cryptoCode,
|
||||
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = new Dictionary<string, object>();
|
||||
if (pendingOnly is bool v)
|
||||
{
|
||||
queryPayload.Add("pendingOnly", v.ToString());
|
||||
}
|
||||
if (offsetIndex is > 0)
|
||||
{
|
||||
queryPayload.Add("offsetIndex", offsetIndex);
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", queryPayload), token);
|
||||
return await HandleResponse<LightningInvoiceData[]>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
|
||||
CreateLightningInvoiceRequest request, CancellationToken token = default)
|
||||
|
@ -1,82 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<OnChainWalletObjectData> GetOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId, bool? includeNeighbourData = null, CancellationToken token = default)
|
||||
{
|
||||
Dictionary<string, object> parameters = new Dictionary<string, object>();
|
||||
if (includeNeighbourData is bool v)
|
||||
parameters.Add("includeNeighbourData", v);
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, method: HttpMethod.Get), token);
|
||||
try
|
||||
{
|
||||
return await HandleResponse<OnChainWalletObjectData>(response);
|
||||
}
|
||||
catch (GreenfieldAPIException err) when (err.APIError.Code == "wallet-object-not-found")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public virtual async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode, GetWalletObjectsRequest query = null, CancellationToken token = default)
|
||||
{
|
||||
Dictionary<string, object> parameters = new Dictionary<string, object>();
|
||||
if (query?.Type is string s)
|
||||
parameters.Add("type", s);
|
||||
if (query?.Ids is string[] ids)
|
||||
parameters.Add("ids", ids);
|
||||
if (query?.IncludeNeighbourData is bool v)
|
||||
parameters.Add("includeNeighbourData", v);
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", parameters, method:HttpMethod.Get), token);
|
||||
return await HandleResponse<OnChainWalletObjectData[]>(response);
|
||||
}
|
||||
public virtual async Task RemoveOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", method:HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
public virtual async Task<OnChainWalletObjectData> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode, AddOnChainWalletObjectRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", method:HttpMethod.Post, bodyPayload: request), token);
|
||||
return await HandleResponse<OnChainWalletObjectData>(response);
|
||||
}
|
||||
public virtual async Task AddOrUpdateOnChainWalletLink(string storeId, string cryptoCode,
|
||||
OnChainWalletObjectId objectId,
|
||||
AddOnChainWalletObjectLinkRequest request = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links", method:HttpMethod.Post, bodyPayload: request), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
public virtual async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
OnChainWalletObjectId objectId,
|
||||
OnChainWalletObjectId link,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", method:HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
@ -37,20 +37,6 @@ namespace BTCPayServer.Client
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<Client.Models.InvoiceData> PayPaymentRequest(string storeId, string paymentRequestId, PayPaymentRequestRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (storeId is null)
|
||||
throw new ArgumentNullException(nameof(storeId));
|
||||
if (paymentRequestId is null)
|
||||
throw new ArgumentNullException(nameof(paymentRequestId));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<Client.Models.InvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
|
||||
CreatePaymentRequestRequest request, CancellationToken token = default)
|
||||
{
|
||||
|
@ -53,16 +53,6 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
|
||||
return await HandleResponse<PayoutData>(response);
|
||||
}
|
||||
public virtual async Task<PayoutData> GetPullPaymentPayout(string pullPaymentId, string payoutId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts/{payoutId}", method: HttpMethod.Get), cancellationToken);
|
||||
return await HandleResponse<PayoutData>(response);
|
||||
}
|
||||
public virtual async Task<PayoutData> GetStorePayout(string storeId, string payoutId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts/{payoutId}", method: HttpMethod.Get), cancellationToken);
|
||||
return await HandleResponse<PayoutData>(response);
|
||||
}
|
||||
public virtual async Task<PayoutData> CreatePayout(string storeId, CreatePayoutThroughStoreRequest payoutRequest, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
|
||||
@ -79,7 +69,7 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<PayoutData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task MarkPayoutPaid(string storeId, string payoutId,
|
||||
public async Task MarkPayoutPaid(string storeId, string payoutId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
@ -88,14 +78,5 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Post), cancellationToken);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
public virtual async Task MarkPayout(string storeId, string payoutId, MarkPayoutRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest(
|
||||
$"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}/mark",
|
||||
method: HttpMethod.Post, bodyPayload: request), cancellationToken);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<StoreRateConfiguration> GetStoreRateConfiguration(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration", method: HttpMethod.Get),
|
||||
token);
|
||||
return await HandleResponse<StoreRateConfiguration>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<List<RateSource>> GetRateSources(
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"misc/rate-sources", method: HttpMethod.Get),
|
||||
token);
|
||||
return await HandleResponse<List<RateSource>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<StoreRateConfiguration> UpdateStoreRateConfiguration(string storeId,
|
||||
StoreRateConfiguration request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration", bodyPayload: request,
|
||||
method: HttpMethod.Put),
|
||||
token);
|
||||
return await HandleResponse<StoreRateConfiguration>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
|
||||
StoreRateConfiguration request,
|
||||
string[] currencyPair,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration/preview", bodyPayload: request,
|
||||
queryPayload: new Dictionary<string, object>() {{"currencyPair", currencyPair}},
|
||||
method: HttpMethod.Post),
|
||||
token);
|
||||
return await HandleResponse<List<StoreRatePreviewResult>>(response);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
@ -37,48 +36,6 @@ namespace BTCPayServer.Client.Models
|
||||
public string RedirectUrl { get; set; } = null;
|
||||
public bool? RedirectAutomatically { get; set; } = null;
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public string FormId { get; set; } = null;
|
||||
public string EmbeddedCSS { get; set; } = null;
|
||||
public CheckoutType? CheckoutType { get; set; } = null;
|
||||
}
|
||||
|
||||
public enum CrowdfundResetEvery
|
||||
{
|
||||
Never,
|
||||
Hour,
|
||||
Day,
|
||||
Month,
|
||||
Year
|
||||
}
|
||||
|
||||
public class CreateCrowdfundAppRequest : CreateAppRequest
|
||||
{
|
||||
public string Title { get; set; } = null;
|
||||
public bool? Enabled { get; set; } = null;
|
||||
public bool? EnforceTargetAmount { get; set; } = null;
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? StartDate { get; set; } = null;
|
||||
public string TargetCurrency { get; set; } = null;
|
||||
public string Description { get; set; } = null;
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? EndDate { get; set; } = null;
|
||||
public decimal? TargetAmount { get; set; } = null;
|
||||
public string CustomCSSLink { get; set; } = null;
|
||||
public string MainImageUrl { get; set; } = null;
|
||||
public string EmbeddedCSS { get; set; } = null;
|
||||
public string NotificationUrl { get; set; } = null;
|
||||
public string Tagline { get; set; } = null;
|
||||
public string PerksTemplate { get; set; } = null;
|
||||
public bool? SoundsEnabled { get; set; } = null;
|
||||
public string DisqusShortname { get; set; } = null;
|
||||
public bool? AnimationsEnabled { get; set; } = null;
|
||||
public int? ResetEveryAmount { get; set; } = null;
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
|
||||
public bool? DisplayPerksValue { get; set; } = null;
|
||||
public bool? DisplayPerksRanking { get; set; } = null;
|
||||
public bool? SortPerksByPopularity { get; set; } = null;
|
||||
public string[] Sounds { get; set; } = null;
|
||||
public string[] AnimationColors { get; set; } = null;
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,6 @@ namespace BTCPayServer.Client.Models
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public string DefaultLanguage { get; set; }
|
||||
public CheckoutType? CheckoutType { get; set; }
|
||||
}
|
||||
}
|
||||
public class InvoiceData : InvoiceDataBase
|
||||
|
@ -1,11 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
[Obsolete]
|
||||
public class LabelData
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using Newtonsoft.Json;
|
||||
@ -27,8 +26,5 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney AmountReceived { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Dictionary<ulong, string> CustomRecords { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,6 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonProperty("nodeURIs", ItemConverterType = typeof(NodeUriJsonConverter))]
|
||||
public NodeInfo[] NodeURIs { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
public string Alias { get; set; }
|
||||
public string Color { get; set; }
|
||||
public string Version { get; set; }
|
||||
public long? PeersCount { get; set; }
|
||||
public long? ActiveChannelsCount { get; set; }
|
||||
public long? InactiveChannelsCount { get; set; }
|
||||
public long? PendingChannelsCount { get; set; }
|
||||
}
|
||||
|
||||
public class LightningChannelData
|
||||
|
@ -1,14 +0,0 @@
|
||||
#nullable enable
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class MarkPayoutRequest
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PayoutState State { get; set; } = PayoutState.Completed;
|
||||
|
||||
public JObject? PaymentProof { get; set; }
|
||||
}
|
@ -4,94 +4,16 @@ using BTCPayServer.JsonConverters;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class OnChainWalletObjectId
|
||||
{
|
||||
public OnChainWalletObjectId()
|
||||
{
|
||||
|
||||
}
|
||||
public OnChainWalletObjectId(string type, string id)
|
||||
{
|
||||
Type = type;
|
||||
Id = id;
|
||||
}
|
||||
public string Type { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
public class AddOnChainWalletObjectLinkRequest : OnChainWalletObjectId
|
||||
{
|
||||
public AddOnChainWalletObjectLinkRequest()
|
||||
{
|
||||
|
||||
}
|
||||
public AddOnChainWalletObjectLinkRequest(string objectType, string objectId) : base(objectType, objectId)
|
||||
{
|
||||
|
||||
}
|
||||
public JObject Data { get; set; }
|
||||
}
|
||||
|
||||
public class GetWalletObjectsRequest
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string[] Ids { get; set; }
|
||||
public bool? IncludeNeighbourData { get; set; }
|
||||
}
|
||||
|
||||
public class AddOnChainWalletObjectRequest : OnChainWalletObjectId
|
||||
{
|
||||
public AddOnChainWalletObjectRequest()
|
||||
{
|
||||
|
||||
}
|
||||
public AddOnChainWalletObjectRequest(string objectType, string objectId) : base(objectType, objectId)
|
||||
{
|
||||
|
||||
}
|
||||
public JObject Data { get; set; }
|
||||
}
|
||||
|
||||
public class OnChainWalletObjectData : OnChainWalletObjectId
|
||||
{
|
||||
public OnChainWalletObjectData()
|
||||
{
|
||||
|
||||
}
|
||||
public OnChainWalletObjectData(string type, string id) : base(type, id)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class OnChainWalletObjectLink : OnChainWalletObjectId
|
||||
{
|
||||
public OnChainWalletObjectLink()
|
||||
{
|
||||
|
||||
}
|
||||
public OnChainWalletObjectLink(string type, string id) : base(type, id)
|
||||
{
|
||||
|
||||
}
|
||||
public JObject LinkData { get; set; }
|
||||
public JObject ObjectData { get; set; }
|
||||
}
|
||||
public JObject Data { get; set; }
|
||||
public OnChainWalletObjectLink[] Links { get; set; }
|
||||
}
|
||||
|
||||
public class OnChainWalletTransactionData
|
||||
{
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public uint256 TransactionHash { get; set; }
|
||||
|
||||
public string Comment { get; set; }
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
|
@ -15,9 +15,7 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonConverter(typeof(OutpointJsonConverter))]
|
||||
public OutPoint Outpoint { get; set; }
|
||||
public string Link { get; set; }
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
public Dictionary<string, LabelData> Labels { get; set; }
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
[JsonConverter(typeof(KeyPathJsonConverter))]
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -20,8 +19,5 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan? SendTimeout { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class PayPaymentRequestRequest
|
||||
{
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? Amount { get; set; }
|
||||
public bool? AllowPendingInvoiceReuse { get; set; }
|
||||
}
|
||||
}
|
@ -24,9 +24,5 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
public string FormId { get; set; }
|
||||
|
||||
public JObject FormResponse { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset CreatedTime { get; set; }
|
||||
public string Id { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
public enum PaymentRequestStatus
|
||||
{
|
||||
Pending = 0,
|
||||
|
@ -2,7 +2,6 @@ using System;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -30,6 +29,5 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PayoutState State { get; set; }
|
||||
public int Revision { get; set; }
|
||||
public JObject PaymentProof { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,4 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
// We can add POS specific things here later
|
||||
}
|
||||
|
||||
public class CrowdfundAppData : AppDataBase
|
||||
{
|
||||
// We can add Crowdfund specific things here later
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class RateSource
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum RefundVariant
|
||||
{
|
||||
RateThen,
|
||||
CurrentRate,
|
||||
Fiat,
|
||||
Custom
|
||||
}
|
||||
|
||||
public class RefundInvoiceRequest
|
||||
{
|
||||
public string? Name { get; set; } = null;
|
||||
public string? PaymentMethod { get; set; }
|
||||
public string? Description { get; set; } = null;
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public RefundVariant? RefundVariant { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? CustomAmount { get; set; }
|
||||
public string? CustomCurrency { get; set; }
|
||||
}
|
||||
}
|
@ -31,8 +31,6 @@ namespace BTCPayServer.Client.Models
|
||||
public bool AnyoneCanCreateInvoice { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public CheckoutType CheckoutType { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
public bool LightningPrivateRouteHints { get; set; }
|
||||
public bool OnChainWithLnInvoiceFallback { get; set; }
|
||||
@ -68,12 +66,6 @@ namespace BTCPayServer.Client.Models
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
}
|
||||
|
||||
public enum CheckoutType
|
||||
{
|
||||
V1,
|
||||
V2
|
||||
}
|
||||
|
||||
public enum NetworkFeeMode
|
||||
{
|
||||
MultiplePaymentsOnly,
|
||||
|
@ -1,10 +0,0 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class StoreRateConfiguration
|
||||
{
|
||||
public decimal Spread { get; set; }
|
||||
public bool IsCustomScript { get; set; }
|
||||
public string EffectiveScript { get; set; }
|
||||
public string PreferredSource { get; set; }
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
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; }
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class StoreRateResult
|
||||
{
|
||||
public string CurrencyPair { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -16,7 +19,6 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonProperty(Order = 1)] public string StoreId { get; set; }
|
||||
[JsonProperty(Order = 2)] public string InvoiceId { get; set; }
|
||||
[JsonProperty(Order = 3)] public JObject Metadata { get; set; }
|
||||
}
|
||||
|
||||
public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent
|
||||
|
@ -0,0 +1,28 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitMonetaryUnit()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MUE");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "MonetaryUnit",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}" : "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"MUE_X = MUE_BTC * BTC_X",
|
||||
"MUE_BTC = bittrex(MUE_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/monetaryunit.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("31'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -137,7 +137,6 @@ namespace BTCPayServer
|
||||
|
||||
public string CryptoImagePath { get; set; }
|
||||
public string[] DefaultRateRules { get; set; } = Array.Empty<string>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
|
@ -58,7 +58,7 @@ namespace BTCPayServer
|
||||
InitZcash();
|
||||
InitChaincoin();
|
||||
// InitArgoneum();//their rate source is down 9/15/20.
|
||||
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
|
||||
InitMonetaryUnit();
|
||||
|
||||
// Assume that electrum mappings are same as BTC if not specified
|
||||
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
|
||||
|
@ -4,8 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.1" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Compile Remove="Altcoins\**\*.cs"></Compile>
|
||||
|
@ -180,7 +180,7 @@ namespace BTCPayServer.Logging
|
||||
logBuilder.Append(": ");
|
||||
var lenAfter = logBuilder.ToString().Length;
|
||||
while (lenAfter++ < 18)
|
||||
logBuilder.Append(' ');
|
||||
logBuilder.Append(" ");
|
||||
// scope information
|
||||
GetScopeInformation(logBuilder);
|
||||
|
||||
|
@ -59,11 +59,7 @@ namespace BTCPayServer.Data
|
||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
||||
public DbSet<UserStore> UserStore { get; set; }
|
||||
[Obsolete]
|
||||
public DbSet<WalletData> Wallets { get; set; }
|
||||
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
||||
public DbSet<WalletObjectLinkData> WalletObjectLinks { get; set; }
|
||||
[Obsolete]
|
||||
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
|
||||
public DbSet<WebhookDeliveryData> WebhookDeliveries { get; set; }
|
||||
public DbSet<WebhookData> Webhooks { get; set; }
|
||||
@ -105,19 +101,15 @@ namespace BTCPayServer.Data
|
||||
//PlannedTransaction.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder);
|
||||
RefundData.OnModelCreating(builder);
|
||||
SettingData.OnModelCreating(builder, Database);
|
||||
//SettingData.OnModelCreating(builder);
|
||||
StoreSettingData.OnModelCreating(builder, Database);
|
||||
StoreWebhookData.OnModelCreating(builder);
|
||||
StoreData.OnModelCreating(builder, Database);
|
||||
//StoreData.OnModelCreating(builder);
|
||||
U2FDevice.OnModelCreating(builder);
|
||||
Fido2Credential.OnModelCreating(builder);
|
||||
BTCPayServer.Data.UserStore.OnModelCreating(builder);
|
||||
//WalletData.OnModelCreating(builder);
|
||||
WalletObjectData.OnModelCreating(builder, Database);
|
||||
WalletObjectLinkData.OnModelCreating(builder, Database);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
WalletTransactionData.OnModelCreating(builder);
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
WebhookDeliveryData.OnModelCreating(builder);
|
||||
LightningAddressData.OnModelCreating(builder);
|
||||
PayoutProcessorData.OnModelCreating(builder);
|
||||
|
@ -3,11 +3,11 @@
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.7" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
|
@ -1,12 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data.Data;
|
||||
|
||||
public class FormData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Config { get; set; }
|
||||
}
|
@ -1,6 +1,3 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class SettingData
|
||||
@ -8,15 +5,5 @@ namespace BTCPayServer.Data
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<SettingData>()
|
||||
.Property(o => o.Value)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -38,7 +36,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
[NotMapped] public string Role { get; set; }
|
||||
|
||||
public string StoreBlob { get; set; }
|
||||
public byte[] StoreBlob { get; set; }
|
||||
|
||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
@ -50,15 +48,5 @@ namespace BTCPayServer.Data
|
||||
public IEnumerable<PayoutData> Payouts { get; set; }
|
||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<StoreData>()
|
||||
.Property(o => o.StoreBlob)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
[Obsolete]
|
||||
public class WalletData
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
|
@ -1,87 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletObjectData
|
||||
{
|
||||
public class Types
|
||||
{
|
||||
public const string Label = "label";
|
||||
public const string Tx = "tx";
|
||||
public const string Payjoin = "payjoin";
|
||||
public const string Invoice = "invoice";
|
||||
public const string PaymentRequest = "payment-request";
|
||||
public const string App = "app";
|
||||
public const string PayjoinExposed = "pj-exposed";
|
||||
public const string Payout = "payout";
|
||||
public const string PullPayment = "pull-payment";
|
||||
}
|
||||
public string WalletId { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
public List<WalletObjectLinkData> Bs { get; set; }
|
||||
public List<WalletObjectLinkData> As { get; set; }
|
||||
|
||||
public IEnumerable<(string type, string id, string linkdata, string objectdata)> GetLinks()
|
||||
{
|
||||
if (Bs is not null)
|
||||
foreach (var c in Bs)
|
||||
{
|
||||
yield return (c.BType, c.BId, c.Data, c.B?.Data);
|
||||
}
|
||||
if (As is not null)
|
||||
foreach (var c in As)
|
||||
{
|
||||
yield return (c.AType, c.AId, c.Data, c.A?.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<WalletObjectData> GetNeighbours()
|
||||
{
|
||||
if (Bs != null)
|
||||
foreach (var c in Bs)
|
||||
{
|
||||
if (c.B != null)
|
||||
yield return c.B;
|
||||
}
|
||||
if (As != null)
|
||||
foreach (var c in As)
|
||||
{
|
||||
if (c.A != null)
|
||||
yield return c.A;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<WalletObjectData>().HasKey(o =>
|
||||
new
|
||||
{
|
||||
o.WalletId,
|
||||
o.Type,
|
||||
o.Id,
|
||||
});
|
||||
builder.Entity<WalletObjectData>().HasIndex(o =>
|
||||
new
|
||||
{
|
||||
o.Type,
|
||||
o.Id
|
||||
});
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<WalletObjectData>()
|
||||
.Property(o => o.Data)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletObjectLinkData
|
||||
{
|
||||
public string WalletId { get; set; }
|
||||
public string AType { get; set; }
|
||||
public string AId { get; set; }
|
||||
public string BType { get; set; }
|
||||
public string BId { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
public WalletObjectData A { get; set; }
|
||||
public WalletObjectData B { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<WalletObjectLinkData>().HasKey(o =>
|
||||
new
|
||||
{
|
||||
o.WalletId,
|
||||
o.AType,
|
||||
o.AId,
|
||||
o.BType,
|
||||
o.BId,
|
||||
});
|
||||
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
|
||||
{
|
||||
o.WalletId,
|
||||
o.BType,
|
||||
o.BId,
|
||||
});
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.A)
|
||||
.WithMany(o => o.Bs)
|
||||
.HasForeignKey(o => new { o.WalletId, o.AType, o.AId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.B)
|
||||
.WithMany(o => o.As)
|
||||
.HasForeignKey(o => new { o.WalletId, o.BType, o.BId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.Property(o => o.Data)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
[Obsolete]
|
||||
public class WalletTransactionData
|
||||
{
|
||||
public string WalletDataId { get; set; }
|
||||
|
@ -1,81 +0,0 @@
|
||||
// <auto-generated />
|
||||
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("20220929132704_label")]
|
||||
public partial class label : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WalletObjects",
|
||||
columns: table => new
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
table.PrimaryKey("PK_WalletObjects", x => new { x.WalletId, x.Type, x.Id });
|
||||
});
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WalletObjects_Type_Id",
|
||||
table: "WalletObjects",
|
||||
columns: new[] { "Type", "Id" });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WalletObjectLinks",
|
||||
columns: table => new
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.AType, x.AId, x.BType, x.BId });
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_BType_BId",
|
||||
columns: x => new { x.WalletId, x.BType, x.BId },
|
||||
principalTable: "WalletObjects",
|
||||
principalColumns: new[] { "WalletId", "Type", "Id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_AType_AId",
|
||||
columns: x => new { x.WalletId, x.AType, x.AId },
|
||||
principalTable: "WalletObjects",
|
||||
principalColumns: new[] { "WalletId", "Type", "Id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WalletObjectLinks_WalletId_BType_BId",
|
||||
table: "WalletObjectLinks",
|
||||
columns: new[] { "WalletId", "BType", "BId" });
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "WalletObjectLinks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WalletObjects");
|
||||
}
|
||||
}
|
||||
}
|
@ -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("20221128062447_jsonb")]
|
||||
public partial class jsonb : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("ALTER TABLE \"Settings\" ALTER COLUMN \"Value\" TYPE JSONB USING \"Value\"::JSONB");
|
||||
migrationBuilder.Sql("ALTER TABLE \"Stores\" ALTER COLUMN \"StoreBlob\" TYPE JSONB USING regexp_replace(convert_from(\"StoreBlob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ namespace BTCPayServer.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
@ -189,7 +189,6 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -846,54 +845,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Wallets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
|
||||
{
|
||||
b.Property<string>("WalletId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletId", "Type", "Id");
|
||||
|
||||
b.HasIndex("Type", "Id");
|
||||
|
||||
b.ToTable("WalletObjects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
{
|
||||
b.Property<string>("WalletId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("BType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("BId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletId", "AType", "AId", "BType", "BId");
|
||||
|
||||
b.HasIndex("WalletId", "BType", "BId");
|
||||
|
||||
b.ToTable("WalletObjectLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.Property<string>("WalletDataId")
|
||||
@ -1382,25 +1333,6 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "A")
|
||||
.WithMany("Bs")
|
||||
.HasForeignKey("WalletId", "AType", "AId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "B")
|
||||
.WithMany("As")
|
||||
.HasForeignKey("WalletId", "BType", "BId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("A");
|
||||
|
||||
b.Navigation("B");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletData", "WalletData")
|
||||
@ -1543,13 +1475,6 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("WalletTransactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
|
||||
{
|
||||
b.Navigation("As");
|
||||
|
||||
b.Navigation("Bs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
|
||||
{
|
||||
b.Navigation("Deliveries");
|
||||
|
@ -26,7 +26,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
@ -8,7 +8,6 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using McMaster.NETCore.Plugins;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Secp256k1;
|
||||
@ -34,8 +33,7 @@ namespace BTCPayServer.PluginPacker
|
||||
throw new Exception($"{rootDLLPath} could not be found");
|
||||
}
|
||||
|
||||
var plugin = PluginLoader.CreateFromAssemblyFile(rootDLLPath, false, new[] { typeof(IBTCPayServerPlugin) });
|
||||
var assembly = plugin.LoadAssembly(name);
|
||||
var assembly = Assembly.LoadFrom(rootDLLPath);
|
||||
var extension = GetAllExtensionTypesFromAssembly(assembly).FirstOrDefault();
|
||||
if (extension is null)
|
||||
{
|
||||
|
@ -13,7 +13,7 @@
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.14" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.10" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name":"Afghan Afghani",
|
||||
"name":"Afghani",
|
||||
"code":"AFN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -21,7 +21,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Albanian Lek",
|
||||
"name":"Lek",
|
||||
"code":"ALL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -42,7 +42,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Angolan Kwanza",
|
||||
"name":"Kwanza",
|
||||
"code":"AOA",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -84,7 +84,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Azerbaijani Manat",
|
||||
"name":"Azerbaijanian Manat",
|
||||
"code":"AZN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -105,14 +105,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Bangladeshi Taka",
|
||||
"name":"Taka",
|
||||
"code":"BDT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Barbadian Dollar",
|
||||
"name":"Barbados Dollar",
|
||||
"code":"BBD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -161,21 +161,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Bhutanese Ngultrum",
|
||||
"name":"Ngultrum",
|
||||
"code":"BTN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Bolivian Boliviano",
|
||||
"name":"Boliviano",
|
||||
"code":"BOB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Bolivian Mvdol",
|
||||
"name":"Mvdol",
|
||||
"code":"BOV",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -189,7 +189,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Botswana Pula",
|
||||
"name":"Pula",
|
||||
"code":"BWP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -224,21 +224,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Burundian Franc",
|
||||
"name":"Burundi Franc",
|
||||
"code":"BIF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Cape Verdean Escudo",
|
||||
"name":"Cabo Verde Escudo",
|
||||
"code":"CVE",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Cambodian Riel",
|
||||
"name":"Riel",
|
||||
"code":"KHR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -301,7 +301,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Comorian Franc",
|
||||
"name":"Comoro Franc",
|
||||
"code":"KMF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -329,7 +329,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Croatian Kuna",
|
||||
"name":"Kuna",
|
||||
"code":"HRK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -371,7 +371,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Djiboutian Franc",
|
||||
"name":"Djibouti Franc",
|
||||
"code":"DJF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -392,14 +392,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Salvadoran Colon",
|
||||
"name":"El Salvador Colon",
|
||||
"code":"SVC",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Eritrean Nakfa",
|
||||
"name":"Nakfa",
|
||||
"code":"ERN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -420,7 +420,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Fijian Dollar",
|
||||
"name":"Fiji Dollar",
|
||||
"code":"FJD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -434,21 +434,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Gambian Dalasi",
|
||||
"name":"Dalasi",
|
||||
"code":"GMD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Georgian Lari",
|
||||
"name":"Lari",
|
||||
"code":"GEL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ghanaian Cedi",
|
||||
"name":"Ghana Cedi",
|
||||
"code":"GHS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -462,7 +462,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guatemalan Quetzal",
|
||||
"name":"Quetzal",
|
||||
"code":"GTQ",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -476,28 +476,28 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guinean Franc",
|
||||
"name":"Guinea Franc",
|
||||
"code":"GNF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guyanese Dollar",
|
||||
"name":"Guyana Dollar",
|
||||
"code":"GYD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Haitian Gourde",
|
||||
"name":"Gourde",
|
||||
"code":"HTG",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Honduran Lempira",
|
||||
"name":"Lempira",
|
||||
"code":"HNL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -511,21 +511,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Hungarian Forint",
|
||||
"name":"Forint",
|
||||
"code":"HUF",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Icelandic Krona",
|
||||
"name":"Iceland Krona",
|
||||
"code":"ISK",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Indonesian Rupiah",
|
||||
"name":"Rupiah",
|
||||
"code":"IDR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -546,7 +546,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"New Israeli Shekel",
|
||||
"name":"New Israeli Sheqel",
|
||||
"code":"ILS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -560,7 +560,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Japanese Yen",
|
||||
"name":"Yen",
|
||||
"code":"JPY",
|
||||
"divisibility":0,
|
||||
"symbol":"¥",
|
||||
@ -574,7 +574,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kazakhstani Tenge",
|
||||
"name":"Tenge",
|
||||
"code":"KZT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -595,7 +595,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"South Korean Won",
|
||||
"name":"Won",
|
||||
"code":"KRW",
|
||||
"divisibility":0,
|
||||
"symbol":"₩",
|
||||
@ -609,14 +609,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kyrgyzstani Som",
|
||||
"name":"Som",
|
||||
"code":"KGS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lao Kip",
|
||||
"name":"Kip",
|
||||
"code":"LAK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -630,14 +630,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lesotho Loti",
|
||||
"name":"Loti",
|
||||
"code":"LSL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"South African Rand",
|
||||
"name":"Rand",
|
||||
"code":"ZAR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -665,14 +665,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Macanese Pataca",
|
||||
"name":"Pataca",
|
||||
"code":"MOP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Macedonian Denar",
|
||||
"name":"Denar",
|
||||
"code":"MKD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -686,7 +686,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Malawian Kwacha",
|
||||
"name":"Malawi Kwacha",
|
||||
"code":"MWK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -700,21 +700,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Maldivian Rufiyaa",
|
||||
"name":"Rufiyaa",
|
||||
"code":"MVR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mauritanian Ouguiya",
|
||||
"name":"Ouguiya",
|
||||
"code":"MRO",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mauritian Rupee",
|
||||
"name":"Mauritius Rupee",
|
||||
"code":"MUR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -742,7 +742,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mongolian Tugrik",
|
||||
"name":"Tugrik",
|
||||
"code":"MNT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -756,21 +756,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mozambican Metical",
|
||||
"name":"Mozambique Metical",
|
||||
"code":"MZN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Myanmar Kyat",
|
||||
"name":"Kyat",
|
||||
"code":"MMK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Namibian dollar",
|
||||
"name":"Namibia Dollar",
|
||||
"code":"NAD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -784,56 +784,56 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Nicaraguan Cordoba",
|
||||
"name":"Cordoba Oro",
|
||||
"code":"NIO",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Nigerian Naira",
|
||||
"name":"Naira",
|
||||
"code":"NGN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Omani Rial",
|
||||
"name":"Rial Omani",
|
||||
"code":"OMR",
|
||||
"divisibility":3,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pakistani Rupee",
|
||||
"name":"Pakistan Rupee",
|
||||
"code":"PKR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Panamanian Balboa",
|
||||
"name":"Balboa",
|
||||
"code":"PAB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Papua New Guinean Kina",
|
||||
"name":"Kina",
|
||||
"code":"PGK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Paraguayan Guarani",
|
||||
"name":"Guarani",
|
||||
"code":"PYG",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Peruvian Sol",
|
||||
"name":"Sol",
|
||||
"code":"PEN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -847,7 +847,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Polish Zloty",
|
||||
"name":"Zloty",
|
||||
"code":"PLN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -875,7 +875,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rwandan Franc",
|
||||
"name":"Rwanda Franc",
|
||||
"code":"RWF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -889,14 +889,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Samoan Tala",
|
||||
"name":"Tala",
|
||||
"code":"WST",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"São Tomé and Príncipe sDobra",
|
||||
"name":"Dobra",
|
||||
"code":"STD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -917,14 +917,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Seychellois Rupee",
|
||||
"name":"Seychelles Rupee",
|
||||
"code":"SCR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Sierra Leonean Leone",
|
||||
"name":"Leone",
|
||||
"code":"SLL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -959,7 +959,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Sri Lankan Rupee",
|
||||
"name":"Sri Lanka Rupee",
|
||||
"code":"LKR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -973,14 +973,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Surinamese Dollar",
|
||||
"name":"Surinam Dollar",
|
||||
"code":"SRD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Swazi Lilangeni",
|
||||
"name":"Lilangeni",
|
||||
"code":"SZL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1022,7 +1022,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Tajikistani Somoni",
|
||||
"name":"Somoni",
|
||||
"code":"TJS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1036,14 +1036,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Thai Baht",
|
||||
"name":"Baht",
|
||||
"code":"THB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Tongan paʻanga",
|
||||
"name":"Pa’anga",
|
||||
"code":"TOP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1071,21 +1071,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Turkmenistani Manat",
|
||||
"name":"Turkmenistan New Manat",
|
||||
"code":"TMT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ugandan Shilling",
|
||||
"name":"Uganda Shilling",
|
||||
"code":"UGX",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ukrainian Hryvnia",
|
||||
"name":"Hryvnia",
|
||||
"code":"UAH",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1106,7 +1106,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Uruguayan Peso",
|
||||
"name":"Peso Uruguayo",
|
||||
"code":"UYU",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1120,28 +1120,28 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Uzbekistani Sum",
|
||||
"name":"Uzbekistan Sum",
|
||||
"code":"UZS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Vanuatu Vatu",
|
||||
"name":"Vatu",
|
||||
"code":"VUV",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Venezuelan Bolívar",
|
||||
"name":"Bolívar",
|
||||
"code":"VEF",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Vietnamese Dong",
|
||||
"name":"Dong",
|
||||
"code":"VND",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -1162,7 +1162,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Zimbabwean Dollar",
|
||||
"name":"Zimbabwe Dollar",
|
||||
"code":"ZWL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
@ -57,13 +56,6 @@ namespace BTCPayServer.Rating
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (splitted.Length > 2)
|
||||
{
|
||||
// Some shitcoin have _ their own ticker name... Since we don't care about those, let's
|
||||
// parse it anyway assuming the first part is one currency.
|
||||
value = new CurrencyPair(splitted[0], string.Join("_", splitted.Skip(1).ToArray()));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -44,6 +44,9 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
if (notFoundSymbols.TryGetValue(ticker.Key, out _))
|
||||
return null;
|
||||
if (ticker.Key.Contains("XMR"))
|
||||
{
|
||||
}
|
||||
try
|
||||
{
|
||||
CurrencyPair pair;
|
||||
|
@ -168,7 +168,7 @@ namespace BTCPayServer.Services.Rates
|
||||
sb.Append(url);
|
||||
if (payload != null)
|
||||
{
|
||||
sb.Append('?');
|
||||
sb.Append("?");
|
||||
sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType<object>().ToArray()));
|
||||
}
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString());
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
@ -19,12 +19,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<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="105.0.5195.5200" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="104.0.5112.7900" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
|
@ -30,7 +30,7 @@ namespace BTCPayServer.Tests
|
||||
s.AddDerivationScheme();
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click();
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.Driver.FindElement(By.Name("command")).Click();
|
||||
|
||||
var emailAlreadyThereInvoiceId = s.CreateInvoice(100, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(emailAlreadyThereInvoiceId);
|
||||
|
@ -1,221 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using NBitcoin;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
||||
public class CheckoutV2Tests : UnitTestBase
|
||||
{
|
||||
private const int TestTimeout = TestUtils.TestTimeout;
|
||||
|
||||
public CheckoutV2Tests(ITestOutputHelper helper) : base(helper)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanConfigureCheckout()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.EnableCheckoutV2();
|
||||
s.AddLightningNode();
|
||||
s.AddDerivationScheme();
|
||||
|
||||
// Configure store url
|
||||
var storeUrl = "https://satoshisteaks.com/";
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
// Default payment method
|
||||
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
|
||||
Assert.DoesNotContain("LNURL", s.Driver.PageSource);
|
||||
var payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("lightning:", payUrl);
|
||||
|
||||
// Lightning amount in Sats
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Contains("Sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
||||
// Expire
|
||||
var expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
expirySeconds.Clear();
|
||||
expirySeconds.SendKeys("3");
|
||||
s.Driver.FindElement(By.Id("Expire")).Click();
|
||||
|
||||
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var expiredSection = s.Driver.FindElement(By.Id("expired"));
|
||||
Assert.True(expiredSection.Displayed);
|
||||
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||
});
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// Test payment
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
|
||||
// Details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
var details = s.Driver.FindElement(By.CssSelector(".payment-details"));
|
||||
Assert.Contains("Total Price", details.Text);
|
||||
Assert.Contains("Total Fiat", details.Text);
|
||||
Assert.Contains("Exchange Rate", details.Text);
|
||||
Assert.Contains("Amount Due", details.Text);
|
||||
Assert.Contains("Recommended Fee", details.Text);
|
||||
|
||||
// Pay partial amount
|
||||
await Task.Delay(200);
|
||||
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-destination");
|
||||
var amountFraction = "0.00001";
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Parse(amountFraction));
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
|
||||
// Fake Pay
|
||||
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountFraction);
|
||||
s.Driver.FindElement(By.Id("FakePay")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.Contains("Created transaction",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
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);
|
||||
});
|
||||
|
||||
// Mine
|
||||
s.Driver.FindElement(By.Id("Mine")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.Contains("Mined 1 block",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
});
|
||||
|
||||
// Pay full amount
|
||||
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
|
||||
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
|
||||
s.Driver.FindElement(By.Id("FakePay")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Server.ExplorerNode.Generate(1);
|
||||
var paidSection = s.Driver.WaitForElement(By.Id("paid"));
|
||||
Assert.True(paidSection.Displayed);
|
||||
Assert.Contains("Invoice Paid", paidSection.Text);
|
||||
});
|
||||
s.Driver.FindElement(By.Id("ReceiptLink"));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// BIP21
|
||||
s.GoToHome();
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
invoiceId = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&LIGHTNING=", payUrl);
|
||||
|
||||
// BIP21 with LN as default payment method
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&LIGHTNING=", payUrl);
|
||||
|
||||
// BIP21 with topup invoice (which is only available with Bitcoin onchain)
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(amount: null);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.DoesNotContain("&LIGHTNING=", payUrl);
|
||||
|
||||
// Expiry message should not show amount for topup invoice
|
||||
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
expirySeconds.Clear();
|
||||
expirySeconds.SendKeys("5");
|
||||
s.Driver.FindElement(By.Id("Expire")).Click();
|
||||
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseCheckoutAsModal()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
s.CreateNewStore();
|
||||
s.EnableCheckoutV2();
|
||||
s.GoToStore();
|
||||
s.AddDerivationScheme();
|
||||
var invoiceId = s.CreateInvoice(0.001m, "BTC", "a@x.com");
|
||||
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
||||
s.Driver.Navigate()
|
||||
.GoToUrl(new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}"));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed);
|
||||
});
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
|
||||
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
|
||||
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
|
||||
new Money(0.001m, MoneyUnit.BTC));
|
||||
|
||||
IWebElement closebutton = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
|
||||
var iframe = s.Driver.SwitchTo().Frame(frameElement);
|
||||
closebutton = iframe.FindElement(By.Id("close"));
|
||||
Assert.True(closebutton.Displayed);
|
||||
});
|
||||
closebutton.Click();
|
||||
s.Driver.AssertElementNotFound(By.Name("btcpay"));
|
||||
Assert.Equal(s.Driver.Url,
|
||||
new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.101-bullseye-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
@ -125,12 +125,6 @@ retry:
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
public static void FillIn(this IWebElement el, string text)
|
||||
{
|
||||
el.Clear();
|
||||
el.SendKeys(text);
|
||||
}
|
||||
|
||||
public static void ScrollTo(this IWebDriver driver, IWebElement element)
|
||||
{
|
||||
|
@ -483,6 +483,93 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void CanParseLegacyLabels()
|
||||
{
|
||||
static void AssertContainsRawLabel(WalletTransactionInfo info)
|
||||
{
|
||||
foreach (var item in new[] { "blah", "lol", "hello" })
|
||||
{
|
||||
Assert.True(info.Labels.ContainsKey(item));
|
||||
var rawLabel = Assert.IsType<RawLabel>(info.Labels[item]);
|
||||
Assert.Equal("raw", rawLabel.Type);
|
||||
Assert.Equal(item, rawLabel.Text);
|
||||
}
|
||||
}
|
||||
var data = new WalletTransactionData();
|
||||
data.Labels = "blah,lol,hello,lol";
|
||||
var info = data.GetBlobInfo();
|
||||
Assert.Equal(3, info.Labels.Count);
|
||||
AssertContainsRawLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
Assert.Contains("raw", data.Labels);
|
||||
Assert.Contains("{", data.Labels);
|
||||
Assert.Contains("[", data.Labels);
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsRawLabel(info);
|
||||
|
||||
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = "pos",
|
||||
Blob = Encoders.Hex.DecodeData("1f8b08000000000000037abf7b7fb592737e6e6e6a5e89929592522d000000ffff030036bc6ad911000000")
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
var label = Assert.Single(info.Labels);
|
||||
Assert.Equal("raw", label.Value.Type);
|
||||
Assert.Equal("pos", label.Value.Text);
|
||||
Assert.Equal("pos", label.Key);
|
||||
|
||||
|
||||
static void AssertContainsLabel(WalletTransactionInfo info)
|
||||
{
|
||||
Assert.Equal(2, info.Labels.Count);
|
||||
var invoiceLabel = Assert.IsType<ReferenceLabel>(info.Labels["invoice"]);
|
||||
Assert.Equal("BFm1MCJPBCDeRoWXvPcwnM", invoiceLabel.Reference);
|
||||
Assert.Equal("invoice", invoiceLabel.Text);
|
||||
Assert.Equal("invoice", invoiceLabel.Type);
|
||||
|
||||
var appLabel = Assert.IsType<ReferenceLabel>(info.Labels["app"]);
|
||||
Assert.Equal("87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe", appLabel.Reference);
|
||||
Assert.Equal("app", appLabel.Text);
|
||||
Assert.Equal("app", appLabel.Type);
|
||||
}
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = "[\"{\\n \\\"value\\\": \\\"invoice\\\",\\n \\\"id\\\": \\\"BFm1MCJPBCDeRoWXvPcwnM\\\"\\n}\",\"{\\n \\\"value\\\": \\\"app\\\",\\n \\\"id\\\": \\\"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\\\"\\n}\"]",
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsLabel(info);
|
||||
|
||||
static void AssertPayoutLabel(WalletTransactionInfo info)
|
||||
{
|
||||
Assert.Single(info.Labels);
|
||||
var l = Assert.IsType<PayoutLabel>(info.Labels["payout"]);
|
||||
Assert.Single(Assert.Single(l.PullPaymentPayouts, k => k.Key == "pullPaymentId").Value, "payoutId");
|
||||
Assert.Equal("walletId", l.WalletId);
|
||||
}
|
||||
|
||||
var payoutId = "payoutId";
|
||||
var pullPaymentId = "pullPaymentId";
|
||||
var walletId = "walletId";
|
||||
// How it was serialized before
|
||||
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = new JArray(JObject.FromObject(new { value = "payout", id = payoutId, pullPaymentId, walletId })).ToString()
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
AssertPayoutLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
info = data.GetBlobInfo();
|
||||
AssertPayoutLabel(info);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DeterministicUTXOSorter()
|
||||
{
|
||||
@ -1207,9 +1294,6 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanParseRateRules()
|
||||
{
|
||||
var pair = CurrencyPair.Parse("USD_EMAT_IC");
|
||||
Assert.Equal("USD", pair.Left);
|
||||
Assert.Equal("EMAT_IC", pair.Right);
|
||||
// Check happy path
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("// Some cool comments");
|
||||
@ -1305,7 +1389,7 @@ namespace BTCPayServer.Tests
|
||||
rule2.Reevaluate();
|
||||
Assert.False(rule2.HasError);
|
||||
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||
Assert.Equal(5000m * 2000.4m * 1.1m, rule2.BidAsk.Bid);
|
||||
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
|
||||
////////
|
||||
|
||||
// Make sure parenthesis are correctly calculated
|
||||
@ -1749,7 +1833,8 @@ namespace BTCPayServer.Tests
|
||||
PaymentMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike)
|
||||
}
|
||||
};
|
||||
var newBlob = new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\"");
|
||||
var newBlob = Encoding.UTF8.GetBytes(
|
||||
new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\""));
|
||||
Assert.Empty(StoreDataExtensions.GetStoreBlob(new StoreData() {StoreBlob = newBlob}).PaymentMethodCriteria);
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -89,7 +88,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("missing-permission", e.APIError.Code);
|
||||
Assert.NotNull(e.APIError.Message);
|
||||
GreenfieldPermissionAPIError permissionError = Assert.IsType<GreenfieldPermissionAPIError>(e.APIError);
|
||||
Assert.Equal(Policies.CanModifyStoreSettings, permissionError.MissingPermission);
|
||||
Assert.Equal(permissionError.MissingPermission, Policies.CanModifyStoreSettings);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -288,117 +287,6 @@ namespace BTCPayServer.Tests
|
||||
await client.GetApp(retrievedApp.Id);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateCrowdfundApp()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
|
||||
// Test validation for creating the app
|
||||
await AssertValidationError(new[] { "AppName" },
|
||||
async () => await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() {}));
|
||||
await AssertValidationError(new[] { "AppName" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
AppName = "this is a really long app name this is a really long app name this is a really long app name",
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "TargetCurrency" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
TargetCurrency = "fake currency"
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "PerksTemplate" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
PerksTemplate = "lol invalid template"
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "AppName", "TargetCurrency", "PerksTemplate" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
TargetCurrency = "fake currency",
|
||||
PerksTemplate = "lol invalid template"
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "AnimationColors" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
AnimationColors = new string[] {}
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "AnimationColors" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
AnimationColors = new string[] { " ", " " }
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "Sounds" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
Sounds = new string[] { " " }
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "Sounds" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
Sounds = new string[] { " ", " ", " " }
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "EndDate" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
StartDate = DateTime.Parse("1998-01-01"),
|
||||
EndDate = DateTime.Parse("1997-12-31")
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Test creating a crowdfund app
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
@ -908,100 +796,6 @@ namespace BTCPayServer.Tests
|
||||
await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanProcessPayoutsExternally()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var acc = tester.NewAccount();
|
||||
acc.Register();
|
||||
await acc.CreateStoreAsync();
|
||||
var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId;
|
||||
var client = await acc.CreateClient();
|
||||
var address = await tester.ExplorerNode.GetNewAddressAsync();
|
||||
var payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Approved = false,
|
||||
PaymentMethod = "BTC",
|
||||
Amount = 0.0001m,
|
||||
Destination = address.ToString()
|
||||
});
|
||||
await AssertAPIError("invalid-state", async () =>
|
||||
{
|
||||
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed});
|
||||
|
||||
});
|
||||
|
||||
await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
|
||||
|
||||
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed});
|
||||
Assert.Equal(PayoutState.Completed,(await client.GetStorePayouts(storeId,false)).Single(data => data.Id == payout.Id ).State );
|
||||
Assert.Null((await client.GetStorePayouts(storeId,false)).Single(data => data.Id == payout.Id ).PaymentProof );
|
||||
|
||||
foreach (var state in new []{ PayoutState.AwaitingApproval, PayoutState.Cancelled, PayoutState.Completed, PayoutState.AwaitingApproval, PayoutState.InProgress})
|
||||
{
|
||||
await AssertAPIError("invalid-state", async () =>
|
||||
{
|
||||
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = state});
|
||||
});
|
||||
}
|
||||
payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Amount = 0.0001m,
|
||||
Destination = address.ToString()
|
||||
});
|
||||
|
||||
payout = await client.GetStorePayout(storeId, payout.Id);
|
||||
Assert.NotNull(payout);
|
||||
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
|
||||
await AssertValidationError(new []{"PaymentProof"}, async () =>
|
||||
{
|
||||
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed, PaymentProof = JObject.FromObject(new
|
||||
{
|
||||
test = "zyx"
|
||||
})});
|
||||
});
|
||||
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.InProgress, PaymentProof = JObject.FromObject(new
|
||||
{
|
||||
proofType = "external-proof"
|
||||
})});
|
||||
payout = await client.GetStorePayout(storeId, payout.Id);
|
||||
Assert.NotNull(payout);
|
||||
Assert.Equal(PayoutState.InProgress, payout.State);
|
||||
Assert.True(payout.PaymentProof.TryGetValue("proofType", out var savedType));
|
||||
Assert.Equal("external-proof",savedType);
|
||||
|
||||
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.AwaitingPayment, PaymentProof = JObject.FromObject(new
|
||||
{
|
||||
proofType = "external-proof",
|
||||
id="finality proof",
|
||||
link="proof.com"
|
||||
})});
|
||||
payout = await client.GetStorePayout(storeId, payout.Id);
|
||||
Assert.NotNull(payout);
|
||||
Assert.Null(payout.PaymentProof);
|
||||
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
|
||||
|
||||
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed, PaymentProof = JObject.FromObject(new
|
||||
{
|
||||
proofType = "external-proof",
|
||||
id="finality proof",
|
||||
link="proof.com"
|
||||
})});
|
||||
payout = await client.GetStorePayout(storeId, payout.Id);
|
||||
Assert.NotNull(payout);
|
||||
Assert.Equal(PayoutState.Completed, payout.State);
|
||||
Assert.True(payout.PaymentProof.TryGetValue("proofType", out savedType));
|
||||
Assert.True(payout.PaymentProof.TryGetValue("link", out var savedLink));
|
||||
Assert.True(payout.PaymentProof.TryGetValue("id", out var savedId));
|
||||
Assert.Equal("external-proof",savedType);
|
||||
Assert.Equal("finality proof",savedId);
|
||||
Assert.Equal("proof.com",savedLink);
|
||||
}
|
||||
|
||||
private DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
return new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset);
|
||||
@ -1374,93 +1168,25 @@ namespace BTCPayServer.Tests
|
||||
await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
|
||||
Assert.DoesNotContain(paymentRequest.Id,
|
||||
(await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id));
|
||||
var archivedPrId = paymentRequest.Id;
|
||||
//let's test some payment stuff with the UI
|
||||
|
||||
//let's test some payment stuff
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
|
||||
|
||||
var invoiceId = Assert.IsType<string>(Assert.IsType<OkObjectResult>(await user.GetController<UIPaymentRequestController>()
|
||||
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
|
||||
|
||||
async Task Pay(string invoiceId, bool partialPayment = false)
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId);
|
||||
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||
{
|
||||
TestLogs.LogInformation($"Paying invoice {invoiceId}");
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId);
|
||||
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||
{
|
||||
TestLogs.LogInformation($"Paying address {invoice.BitcoinAddress}");
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||
});
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
||||
if (!partialPayment)
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
}
|
||||
await Pay(invoiceId);
|
||||
|
||||
//Same thing, but with the API
|
||||
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
|
||||
var paidPrId = paymentTestPaymentRequest.Id;
|
||||
var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
|
||||
await Pay(invoiceData.Id);
|
||||
|
||||
// Let's tests some unhappy path
|
||||
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" });
|
||||
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
|
||||
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m }));
|
||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
||||
{
|
||||
Amount = 0.1m,
|
||||
AllowCustomPaymentAmounts = true,
|
||||
Currency = "BTC",
|
||||
Title = "Payment test title"
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||
});
|
||||
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
|
||||
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m });
|
||||
Assert.Equal(0.04m, invoiceData.Amount);
|
||||
var firstPaymentId = invoiceData.Id;
|
||||
await AssertAPIError("archived", () => client.PayPaymentRequest(user.StoreId, archivedPrId, new PayPaymentRequestRequest()));
|
||||
|
||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
||||
{
|
||||
Amount = 0.1m,
|
||||
AllowCustomPaymentAmounts = true,
|
||||
Currency = "BTC",
|
||||
Title = "Payment test title",
|
||||
ExpiryDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(1.0)
|
||||
});
|
||||
|
||||
await AssertAPIError("expired", () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest()));
|
||||
await AssertAPIError("already-paid", () => client.PayPaymentRequest(user.StoreId, paidPrId, new PayPaymentRequestRequest()));
|
||||
|
||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
||||
{
|
||||
Amount = 0.1m,
|
||||
AllowCustomPaymentAmounts = true,
|
||||
Currency = "BTC",
|
||||
Title = "Payment test title",
|
||||
ExpiryDate = null
|
||||
});
|
||||
|
||||
await Pay(firstPaymentId, true);
|
||||
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
|
||||
|
||||
Assert.Equal(0.06m, invoiceData.Amount);
|
||||
Assert.Equal("BTC", invoiceData.Currency);
|
||||
|
||||
var expectedInvoiceId = invoiceData.Id;
|
||||
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { AllowPendingInvoiceReuse = true });
|
||||
Assert.Equal(expectedInvoiceId, invoiceData.Id);
|
||||
|
||||
var notExpectedInvoiceId = invoiceData.Id;
|
||||
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { AllowPendingInvoiceReuse = false });
|
||||
Assert.NotEqual(notExpectedInvoiceId, invoiceData.Id);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1561,127 +1287,6 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanRefundInvoice()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 5000.0m, Currency = "USD" });
|
||||
var methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||
var method = methods.First();
|
||||
var amount = method.Amount;
|
||||
Assert.Equal(amount, method.Due);
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
|
||||
Money.Coins(method.Due)
|
||||
);
|
||||
});
|
||||
|
||||
// test validation that the invoice exists
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, "lol fake invoice id", new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
|
||||
// test validation error for when invoice is not yet in the state in which it can be refunded
|
||||
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
}));
|
||||
Assert.Equal("Cannot refund this invoice", apiError.Message);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.True(invoice.Status == InvoiceStatus.Processing);
|
||||
});
|
||||
|
||||
// need to set the status to the one in which we can actually refund the invoice
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() {
|
||||
Status = InvoiceStatus.Settled
|
||||
});
|
||||
|
||||
// test validation for the payment method
|
||||
var validationError = await AssertValidationError(new[] { "PaymentMethod" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = "fake payment method",
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
Assert.Contains("PaymentMethod: Please select one of the payment methods which were available for the original invoice", validationError.Message);
|
||||
|
||||
// test RefundVariant.RateThen
|
||||
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(1, pp.Amount);
|
||||
Assert.Equal(pp.Name, $"Refund {invoice.Id}");
|
||||
|
||||
// test RefundVariant.CurrentRate
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.CurrentRate
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(1, pp.Amount);
|
||||
|
||||
// test RefundVariant.Fiat
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Fiat,
|
||||
Name = "my test name"
|
||||
});
|
||||
Assert.Equal("USD", pp.Currency);
|
||||
Assert.False(pp.AutoApproveClaims);
|
||||
Assert.Equal(5000, pp.Amount);
|
||||
Assert.Equal("my test name", pp.Name);
|
||||
|
||||
// test RefundVariant.Custom
|
||||
validationError = await AssertValidationError(new[] { "CustomAmount", "CustomCurrency" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
});
|
||||
});
|
||||
Assert.Contains("CustomAmount: Amount must be greater than 0", validationError.Message);
|
||||
Assert.Contains("CustomCurrency: Invalid currency", validationError.Message);
|
||||
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 69420,
|
||||
CustomCurrency = "JPY"
|
||||
});
|
||||
Assert.Equal("JPY", pp.Currency);
|
||||
Assert.False(pp.AutoApproveClaims);
|
||||
Assert.Equal(69420, pp.Amount);
|
||||
|
||||
// should auto-approve if currencies match
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 0.00069420m,
|
||||
CustomCurrency = "BTC"
|
||||
});
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceTests()
|
||||
@ -1719,7 +1324,7 @@ namespace BTCPayServer.Tests
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||
{
|
||||
RedirectAutomatically = true,
|
||||
RequiresRefundEmail = true,
|
||||
RequiresRefundEmail = true
|
||||
},
|
||||
AdditionalSearchTerms = new string[] { "Banana" }
|
||||
});
|
||||
@ -2048,35 +1653,25 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
user.GrantAccess(true);
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
|
||||
|
||||
var merchant = tester.NewAccount();
|
||||
await merchant.GrantAccessAsync(true);
|
||||
merchant.GrantAccess(true);
|
||||
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
|
||||
var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}");
|
||||
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60)));
|
||||
// The default client is using charge, so we should not be able to query channels
|
||||
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
|
||||
var info = await chargeClient.GetLightningNodeInfo("BTC");
|
||||
var info = await client.GetLightningNodeInfo("BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
Assert.NotNull(info.Alias);
|
||||
Assert.NotNull(info.Color);
|
||||
Assert.NotNull(info.Version);
|
||||
Assert.NotNull(info.PeersCount);
|
||||
Assert.NotNull(info.ActiveChannelsCount);
|
||||
Assert.NotNull(info.InactiveChannelsCount);
|
||||
Assert.NotNull(info.PendingChannelsCount);
|
||||
|
||||
var gex = await AssertAPIError("lightning-node-unavailable", () => chargeClient.ConnectToLightningNode("BTC", new ConnectToNodeRequest(NodeInfo.Parse($"{new Key().PubKey.ToHex()}@localhost:3827"))));
|
||||
Assert.Contains("NotSupported", gex.Message);
|
||||
|
||||
await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC"));
|
||||
await AssertAPIError("lightning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
|
||||
// Not permission for the store!
|
||||
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = LightMoney.Satoshis(1000),
|
||||
Description = "lol",
|
||||
@ -2084,17 +1679,9 @@ namespace BTCPayServer.Tests
|
||||
PrivateRouteHints = false
|
||||
});
|
||||
var chargeInvoice = invoiceData;
|
||||
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
|
||||
// check list for internal node
|
||||
var invoices = await chargeClient.GetLightningInvoices("BTC");
|
||||
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true);
|
||||
Assert.NotEmpty(invoices);
|
||||
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
|
||||
Assert.NotEmpty(pendingInvoices);
|
||||
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
|
||||
|
||||
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||
// Not permission for the server
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
|
||||
|
||||
@ -2112,22 +1699,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id));
|
||||
|
||||
// check pending list
|
||||
var merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
|
||||
Assert.NotEmpty(merchantPendingInvoices);
|
||||
Assert.Contains(merchantPendingInvoices, i => i.Id == merchantInvoice.Id);
|
||||
|
||||
var payResponse = await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest
|
||||
await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
{
|
||||
BOLT11 = merchantInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(merchantInvoice.BOLT11, payResponse.BOLT11);
|
||||
Assert.Equal(LightningPaymentStatus.Complete, payResponse.Status);
|
||||
Assert.NotNull(payResponse.Preimage);
|
||||
Assert.NotNull(payResponse.FeeAmount);
|
||||
Assert.NotNull(payResponse.TotalAmount);
|
||||
Assert.NotNull(payResponse.PaymentHash);
|
||||
|
||||
await Assert.ThrowsAsync<GreenfieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
{
|
||||
BOLT11 = "lol"
|
||||
@ -2144,15 +1719,6 @@ namespace BTCPayServer.Tests
|
||||
var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
|
||||
Assert.NotNull(invoice.PaidAt);
|
||||
Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount);
|
||||
|
||||
// check list for store with paid invoice
|
||||
var merchantInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC");
|
||||
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
|
||||
Assert.NotEmpty(merchantInvoices);
|
||||
Assert.Empty(merchantPendingInvoices);
|
||||
// if the test ran too many times the invoice might be on a later page
|
||||
if (merchantInvoices.Length < 100) Assert.Contains(merchantInvoices, i => i.Id == merchantInvoice.Id);
|
||||
|
||||
// Amount received might be bigger because of internal implementation shit from lightning
|
||||
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
|
||||
|
||||
@ -2160,6 +1726,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
|
||||
// As admin, can use the internal node through our store.
|
||||
await user.MakeAdmin(true);
|
||||
await user.RegisterInternalLightningNodeAsync("BTC");
|
||||
@ -2169,7 +1736,7 @@ namespace BTCPayServer.Tests
|
||||
await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
|
||||
// However, even as a guest, you should be able to create an invoice
|
||||
var guest = tester.NewAccount();
|
||||
await guest.GrantAccessAsync();
|
||||
guest.GrantAccess(false);
|
||||
await user.AddGuest(guest.UserId);
|
||||
client = await guest.CreateClient(Policies.CanCreateLightningInvoiceInStore);
|
||||
await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
|
||||
@ -2696,7 +2263,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
|
||||
Assert.Equal(String.Empty, transaction.Comment);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
Assert.Equal(new Dictionary<string, LabelData>(), transaction.Labels);
|
||||
|
||||
// transaction patch tests
|
||||
@ -2717,7 +2283,7 @@ namespace BTCPayServer.Tests
|
||||
}.ToJson(),
|
||||
patchedTransaction.Labels.ToJson()
|
||||
);
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode);
|
||||
@ -3108,134 +2674,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseWalletObjectsAPI()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
|
||||
var admin = tester.NewAccount();
|
||||
await admin.GrantAccessAsync(true);
|
||||
|
||||
var client = await admin.CreateClient(Policies.Unrestricted);
|
||||
|
||||
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
|
||||
var test = new OnChainWalletObjectId("test", "test");
|
||||
Assert.NotNull(await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test.Type, test.Id)));
|
||||
|
||||
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
|
||||
Assert.NotNull(await client.GetOnChainWalletObject(admin.StoreId, "BTC", test));
|
||||
Assert.Null(await client.GetOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test-wrong", "test")));
|
||||
Assert.Null(await client.GetOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test", "test-wrong")));
|
||||
|
||||
await client.RemoveOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test", "test"));
|
||||
|
||||
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
|
||||
|
||||
var test1 = new OnChainWalletObjectId("test", "test1");
|
||||
var test2 = new OnChainWalletObjectId("test", "test2");
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test.Type, test.Id));
|
||||
// Those links don't exists
|
||||
await AssertAPIError("wallet-object-not-found", () => client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test1.Type, test1.Id)));
|
||||
await AssertAPIError("wallet-object-not-found", () => client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test2.Type, test2.Id)));
|
||||
|
||||
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
|
||||
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test1.Type, test1.Id));
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test2.Type, test2.Id));
|
||||
|
||||
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test1.Type, test1.Id));
|
||||
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test2.Type, test2.Id));
|
||||
|
||||
var objs = await client.GetOnChainWalletObjects(admin.StoreId, "BTC");
|
||||
Assert.Equal(3, objs.Length);
|
||||
var middleObj = objs.Single(data => data.Id == "test" && data.Type == "test");
|
||||
Assert.Equal(2, middleObj.Links.Length);
|
||||
Assert.Contains("test1", middleObj.Links.Select(l => l.Id));
|
||||
Assert.Contains("test2", middleObj.Links.Select(l => l.Id));
|
||||
|
||||
var test1Obj = objs.Single(data => data.Id == "test1" && data.Type == "test");
|
||||
var test2Obj = objs.Single(data => data.Id == "test2" && data.Type == "test");
|
||||
Assert.Single(test1Obj.Links.Select(l => l.Id), l => l == "test");
|
||||
Assert.Single(test2Obj.Links.Select(l => l.Id), l => l == "test");
|
||||
|
||||
await client.RemoveOnChainWalletLinks(admin.StoreId, "BTC",
|
||||
test1,
|
||||
test);
|
||||
|
||||
var testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test);
|
||||
Assert.Single(testObj.Links.Select(l => l.Id), l => l == "test2");
|
||||
Assert.Single(testObj.Links);
|
||||
test1Obj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test1);
|
||||
Assert.Empty(test1Obj.Links);
|
||||
|
||||
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC",
|
||||
test1,
|
||||
new AddOnChainWalletObjectLinkRequest(test.Type, test.Id) { Data = new JObject() { ["testData"] = "lol" } });
|
||||
|
||||
// Add some data to test1
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = test1.Type, Id = test1.Id, Data = new JObject() { ["testData"] = "test1" } });
|
||||
|
||||
// Create a new type
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = "newtype", Id = test1.Id });
|
||||
|
||||
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test);
|
||||
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
|
||||
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData["testData"]?.Value<string>() == "test1"));
|
||||
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test, false);
|
||||
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
|
||||
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData is null));
|
||||
|
||||
async Task TestWalletRepository(bool useInefficient)
|
||||
{
|
||||
// We should have 4 nodes, two `test` type and one `newtype`
|
||||
// Only the node `test` `test` is connected to `test1`
|
||||
var wid = new WalletId(admin.StoreId, "BTC");
|
||||
var repo = tester.PayTester.GetService<WalletRepository>();
|
||||
var allObjects = await repo.GetWalletObjects((new(wid, null) { UseInefficientPath = useInefficient }));
|
||||
var allObjectsNoWallet = await repo.GetWalletObjects((new() { UseInefficientPath = useInefficient }));
|
||||
var allObjectsNoWalletAndType = await repo.GetWalletObjects((new() { Type = "test", UseInefficientPath = useInefficient }));
|
||||
var allTests = await repo.GetWalletObjects((new(wid, "test") { UseInefficientPath = useInefficient }));
|
||||
var twoTests2 = await repo.GetWalletObjects((new(wid, "test", new[] { "test1", "test2", "test-unk" }) { UseInefficientPath = useInefficient }));
|
||||
var oneTest = await repo.GetWalletObjects((new(wid, "test", new[] { "test" }) { UseInefficientPath = useInefficient }));
|
||||
var oneTestWithoutData = await repo.GetWalletObjects((new(wid, "test", new[] { "test" }) { UseInefficientPath = useInefficient, IncludeNeighbours = false }));
|
||||
var idsTypes = await repo.GetWalletObjects((new(wid) { TypesIds = new[] { new ObjectTypeId("test", "test1"), new ObjectTypeId("test", "test2") }, UseInefficientPath = useInefficient }));
|
||||
|
||||
Assert.Equal(4, allObjects.Count);
|
||||
// We are reusing a db in this test, as such we may have other wallets here.
|
||||
Assert.True(allObjectsNoWallet.Count >= 4);
|
||||
Assert.True(allObjectsNoWalletAndType.Count >= 3);
|
||||
Assert.Equal(3, allTests.Count);
|
||||
Assert.Equal(2, twoTests2.Count);
|
||||
Assert.Single(oneTest);
|
||||
Assert.NotNull(oneTest.First().Value.GetNeighbours().Select(n => n.Data).FirstOrDefault());
|
||||
Assert.Single(oneTestWithoutData);
|
||||
Assert.Null(oneTestWithoutData.First().Value.GetNeighbours().Select(n => n.Data).FirstOrDefault());
|
||||
Assert.Equal(2, idsTypes.Count);
|
||||
}
|
||||
await TestWalletRepository(false);
|
||||
await TestWalletRepository(true);
|
||||
|
||||
{
|
||||
var allObjects = await client.GetOnChainWalletObjects(admin.StoreId, "BTC");
|
||||
var allTests = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test" });
|
||||
var twoTests2 = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids = new[] { "test1", "test2", "test-unk" } });
|
||||
var oneTest = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids=new[] { "test" } });
|
||||
var oneTestWithoutData = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids = new[] { "test" }, IncludeNeighbourData = false });
|
||||
|
||||
Assert.Equal(4, allObjects.Length);
|
||||
Assert.Equal(3, allTests.Length);
|
||||
Assert.Equal(2, twoTests2.Length);
|
||||
Assert.Single(oneTest);
|
||||
Assert.NotNull(oneTest.First().Links.Select(n => n.ObjectData).FirstOrDefault());
|
||||
Assert.Single(oneTestWithoutData);
|
||||
Assert.Null(oneTestWithoutData.First().Links.Select(n => n.ObjectData).FirstOrDefault());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
@ -3254,67 +2693,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(custodians);
|
||||
Assert.NotEmpty(custodians);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreRateConfigTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
await AssertHttpError(401, async () => await unauthClient.GetRateSources());
|
||||
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
var clientBasic = await user.CreateClient();
|
||||
Assert.NotEmpty(await clientBasic.GetRateSources());
|
||||
var config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.NotNull(config);
|
||||
Assert.False(config.IsCustomScript);
|
||||
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
|
||||
Assert.Equal("coingecko", config.PreferredSource);
|
||||
|
||||
Assert.Equal(0.9m,
|
||||
Assert.Single(await clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() {IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1;", Spread = 10m,},
|
||||
new[] {"BTC_XYZ"})).Rate);
|
||||
|
||||
Assert.True((await clientBasic.UpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1", Spread = 10m,}))
|
||||
.IsCustomScript);
|
||||
|
||||
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.NotNull(config);
|
||||
Assert.NotNull(config.EffectiveScript);
|
||||
Assert.Equal("BTC_XYZ = 1;", config.EffectiveScript);
|
||||
Assert.Equal(10m, config.Spread);
|
||||
Assert.Null(config.PreferredSource);
|
||||
|
||||
Assert.NotNull((await clientBasic.GetStoreRateConfiguration(user.StoreId)).EffectiveScript);
|
||||
Assert.NotNull((await clientBasic.UpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko"}))
|
||||
.PreferredSource);
|
||||
|
||||
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
|
||||
|
||||
await AssertValidationError(new[] { "EffectiveScript", "PreferredSource" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, EffectiveScript = "BTC_XYZ = 1;" }));
|
||||
|
||||
await AssertValidationError(new[] { "EffectiveScript" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ rg8w*# 1;" }));
|
||||
await AssertValidationError(new[] { "PreferredSource" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "", PreferredSource = "coingecko" }));
|
||||
|
||||
await AssertValidationError(new[] { "PreferredSource", "Spread" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO", Spread = -1m }));
|
||||
|
||||
await AssertValidationError(new[] { "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
await AssertValidationError(new[] { "PreferredSource", "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CustodianAccountControllerTests()
|
||||
|
@ -185,15 +185,6 @@ namespace BTCPayServer.Tests
|
||||
return (name, storeId);
|
||||
}
|
||||
|
||||
public void EnableCheckoutV2(bool bip21 = false)
|
||||
{
|
||||
GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
Driver.SetCheckbox(By.Id("UseNewCheckout"), true);
|
||||
Driver.WaitForElement(By.Id("OnChainWithLnInvoiceFallback"));
|
||||
Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), bip21);
|
||||
Driver.FindElement(By.Id("Save")).Click();
|
||||
}
|
||||
|
||||
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool? importkeys = null, bool isHotWallet = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
|
||||
{
|
||||
var isImport = !string.IsNullOrEmpty(seed);
|
||||
|
@ -64,61 +64,6 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.Quit();
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseForms()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet(isHotWallet: true);
|
||||
|
||||
// Point Of Sale
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
|
||||
new SelectElement(s.Driver.FindElement(By.Id("SelectedAppType"))).SelectByValue("PointOfSale");
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.FindElement(By.CssSelector("button[type='submit']")).Click();
|
||||
|
||||
Assert.Contains("Enter your email", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
|
||||
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
|
||||
|
||||
s.PayInvoice(true);
|
||||
var invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
|
||||
s.GoToInvoice(invoiceId);
|
||||
Assert.Contains("aa@aa.com", s.Driver.PageSource);
|
||||
|
||||
// Payment Request
|
||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
|
||||
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
var editUrl = s.Driver.Url;
|
||||
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='form-button']")).Click();
|
||||
Assert.Contains("Enter your email", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
|
||||
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
|
||||
s.Driver.Navigate().GoToUrl(editUrl);
|
||||
Assert.Contains("aa@aa.com", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseCPFP()
|
||||
{
|
||||
@ -699,26 +644,15 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("enable-pay-button")).Click();
|
||||
s.Driver.FindElement(By.Id("disable-pay-button")).Click();
|
||||
s.FindAlertMessage();
|
||||
s.GoToStore();
|
||||
s.GoToStore(StoreNavPages.General);
|
||||
Assert.False(s.Driver.FindElement(By.Id("AnyoneCanCreateInvoice")).Selected);
|
||||
s.Driver.SetCheckbox(By.Id("AnyoneCanCreateInvoice"), true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.FindAlertMessage();
|
||||
Assert.True(s.Driver.FindElement(By.Id("AnyoneCanCreateInvoice")).Selected);
|
||||
|
||||
// Store settings: Set and unset brand color
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("BrandColor")).SendKeys("#f7931a");
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
Assert.Equal("#f7931a", s.Driver.FindElement(By.Id("BrandColor")).GetAttribute("value"));
|
||||
s.Driver.FindElement(By.Id("BrandColor")).Clear();
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
Assert.Equal(string.Empty, s.Driver.FindElement(By.Id("BrandColor")).GetAttribute("value"));
|
||||
|
||||
// Alice should be able to delete the store
|
||||
s.GoToStore();
|
||||
s.GoToStore(StoreNavPages.General);
|
||||
s.Driver.FindElement(By.Id("DeleteStore")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
|
||||
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||
@ -926,8 +860,9 @@ namespace BTCPayServer.Tests
|
||||
var viewUrl = s.Driver.Url;
|
||||
|
||||
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
|
||||
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
|
||||
Assert.Equal("Pay Invoice",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
|
||||
// expire
|
||||
s.GoToUrl(editUrl);
|
||||
s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'");
|
||||
@ -945,13 +880,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.GoToUrl(viewUrl);
|
||||
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
|
||||
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
|
||||
// test invoice creation, click with JS, because the button is inside a sticky header
|
||||
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
|
||||
// checkout v1
|
||||
s.Driver.WaitForElement(By.CssSelector("invoice"));
|
||||
Assert.Contains("Awaiting Payment", s.Driver.PageSource);
|
||||
Assert.Equal("Pay Invoice",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
|
||||
// archive (from details page)
|
||||
s.GoToUrl(editUrl);
|
||||
@ -1080,10 +1010,10 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
// This one should be checked
|
||||
Assert.Contains("value=\"InvoiceProcessing\" checked", s.Driver.PageSource);
|
||||
Assert.Contains("value=\"InvoiceCreated\" checked", s.Driver.PageSource);
|
||||
Assert.Contains($"value=\"InvoiceProcessing\" checked", s.Driver.PageSource);
|
||||
Assert.Contains($"value=\"InvoiceCreated\" checked", s.Driver.PageSource);
|
||||
// This one never been checked
|
||||
Assert.DoesNotContain("value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource);
|
||||
Assert.DoesNotContain($"value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Name("update")).Click();
|
||||
s.FindAlertMessage();
|
||||
@ -1114,7 +1044,6 @@ namespace BTCPayServer.Tests
|
||||
s.GoToStore(StoreNavPages.Webhooks);
|
||||
s.Driver.FindElement(By.LinkText("Modify")).Click();
|
||||
var elements = s.Driver.FindElements(By.ClassName("redeliver"));
|
||||
|
||||
// One worked, one failed
|
||||
s.Driver.FindElement(By.ClassName("fa-times"));
|
||||
s.Driver.FindElement(By.ClassName("fa-check"));
|
||||
|
@ -75,11 +75,11 @@ namespace BTCPayServer.Tests
|
||||
public async Task CanQueryDirectProviders()
|
||||
{
|
||||
// TODO: Check once in a while whether or not they are working again
|
||||
string[] brokenShitcoinCasinos = {};
|
||||
var skipped = 0;
|
||||
string[] brokenShitcoinCasinos = { };
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct)
|
||||
.Select(s => s.Id).ToHashSet();
|
||||
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray());
|
||||
foreach (var result in factory
|
||||
.Providers
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider bf &&
|
||||
@ -91,26 +91,14 @@ namespace BTCPayServer.Tests
|
||||
var name = result.ExpectedName;
|
||||
if (brokenShitcoinCasinos.Contains(name))
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {name}: Broken shitcoin casino");
|
||||
skipped++;
|
||||
TestLogs.LogInformation($"Skipping {name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
TestLogs.LogInformation($"Testing {name}");
|
||||
|
||||
result.Fetcher.InvalidateCache();
|
||||
|
||||
ExchangeRates exchangeRates = null;
|
||||
try
|
||||
{
|
||||
exchangeRates = new ExchangeRates(name, result.ResultAsync.Result);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {name}: {exception.Message}");
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
var exchangeRates = new ExchangeRates(name, result.ResultAsync.Result);
|
||||
result.Fetcher.InvalidateCache();
|
||||
Assert.NotNull(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates);
|
||||
@ -172,12 +160,11 @@ namespace BTCPayServer.Tests
|
||||
// Kraken emit one request only after first GetRates
|
||||
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
|
||||
|
||||
|
||||
var p = new KrakenExchangeRateProvider();
|
||||
var rates = await p.GetRatesAsync(default);
|
||||
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
|
||||
|
||||
// Check we didn't skip too many exchanges
|
||||
Assert.InRange(skipped, 0, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -311,39 +298,17 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
// This test verify that no malicious js is added in the minified files.
|
||||
// We should extend the tests to other js files, but we can do as we go...
|
||||
using var client = new HttpClient();
|
||||
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
|
||||
|
||||
using HttpClient client = new HttpClient();
|
||||
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js");
|
||||
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
var expected = await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync();
|
||||
Assert.Equal(expected, actual.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
|
||||
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
|
||||
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
expected = await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync();
|
||||
Assert.Equal(expected, actual.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
string GetFileContent(params string[] path)
|
||||
{
|
||||
var l = path.ToList();
|
||||
|
@ -40,7 +40,6 @@ using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Storage.Models;
|
||||
@ -52,7 +51,6 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
@ -654,8 +652,8 @@ namespace BTCPayServer.Tests
|
||||
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout * 2)]
|
||||
[Trait("Flaky", "Flaky")]
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseTorClient()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
@ -787,9 +785,9 @@ namespace BTCPayServer.Tests
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
Assert.Contains("test", tx.Tags.Select(l => l.Text));
|
||||
Assert.Contains("test2", tx.Tags.Select(l => l.Text));
|
||||
Assert.Equal(2, tx.Tags.GroupBy(l => l.Color).Count());
|
||||
Assert.Contains("test", tx.Labels.Select(l => l.Text));
|
||||
Assert.Contains("test2", tx.Labels.Select(l => l.Text));
|
||||
Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count());
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
||||
@ -799,9 +797,12 @@ namespace BTCPayServer.Tests
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
Assert.Contains("test", tx.Tags.Select(l => l.Text));
|
||||
Assert.DoesNotContain("test2", tx.Tags.Select(l => l.Text));
|
||||
Assert.Single(tx.Tags.GroupBy(l => l.Color));
|
||||
Assert.Contains("test", tx.Labels.Select(l => l.Text));
|
||||
Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Text));
|
||||
Assert.Single(tx.Labels.GroupBy(l => l.Color));
|
||||
|
||||
var walletInfo = await tester.PayTester.GetService<WalletRepository>().GetWalletInfo(walletId);
|
||||
Assert.Single(walletInfo.LabelColors); // the test2 color should have been removed
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
@ -1752,7 +1753,7 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
var invoice = user.BitPay.CreateInvoice(
|
||||
new Invoice
|
||||
{
|
||||
Price = 10,
|
||||
@ -1767,7 +1768,7 @@ namespace BTCPayServer.Tests
|
||||
var jsonResult = user.GetController<UIInvoiceController>().Export("json").GetAwaiter().GetResult();
|
||||
var result = Assert.IsType<ContentResult>(jsonResult);
|
||||
Assert.Equal("application/json", result.ContentType);
|
||||
Assert.Single(JArray.Parse(result.Content));
|
||||
Assert.Equal(1, JArray.Parse(result.Content).Count);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
@ -2520,79 +2521,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(lnMethod.IsInternalNode);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Obsolete]
|
||||
public async Task CanDoLabelMigrations()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
await tester.StartAsync();
|
||||
var dbf = tester.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
int walletCount = 1000;
|
||||
var wallet = "walletttttttttttttttttttttttttttt";
|
||||
using (var db = dbf.CreateContext())
|
||||
{
|
||||
for (int i = 0; i < walletCount; i++)
|
||||
{
|
||||
var walletData = new WalletData() { Id = $"S-{wallet}{i}-BTC" };
|
||||
walletData.Blob = ZipUtils.Zip("{\"LabelColors\": { \"label1\" : \"black\", \"payout\":\"green\" }}");
|
||||
db.Wallets.Add(walletData);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
uint256 firstTxId = null;
|
||||
using (var db = dbf.CreateContext())
|
||||
{
|
||||
int transactionCount = 10_000;
|
||||
for (int i = 0; i < transactionCount; i++)
|
||||
{
|
||||
var txId = RandomUtils.GetUInt256();
|
||||
var wt = new WalletTransactionData()
|
||||
{
|
||||
WalletDataId = $"S-{wallet}{i % walletCount}-BTC",
|
||||
TransactionId = txId.ToString(),
|
||||
};
|
||||
firstTxId ??= txId;
|
||||
if (i != 10)
|
||||
wt.Blob = ZipUtils.Zip("{\"Comment\":\"test\"}");
|
||||
if (i % 1240 != 0)
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"}]";
|
||||
}
|
||||
else if (i == 0)
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}, " +
|
||||
"{\"type\":\"payout\", \"text\":\"payout\", \"pullPaymentPayouts\":{\"pp1\":[\"p1\",\"p2\"],\"pp2\":[\"p3\"]}}]";
|
||||
}
|
||||
else
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}]";
|
||||
}
|
||||
db.WalletTransactions.Add(wt);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
await RestartMigration(tester);
|
||||
var migrator = tester.PayTester.GetService<IEnumerable<IHostedService>>().OfType<DbMigrationsHostedService>().First();
|
||||
await migrator.MigratedTransactionLabels(0);
|
||||
|
||||
var walletRepo = tester.PayTester.GetService<WalletRepository>();
|
||||
var wi1 = await walletRepo.GetWalletLabels(new WalletId($"{wallet}0", "BTC"));
|
||||
Assert.Equal(3, wi1.Length);
|
||||
Assert.Contains(wi1, o => o.Label == "label1" && o.Color == "black");
|
||||
Assert.Contains(wi1, o => o.Label == "labelo0" && o.Color == "#000");
|
||||
Assert.Contains(wi1, o => o.Label == "payout" && o.Color == "green");
|
||||
|
||||
var txInfo = await walletRepo.GetWalletTransactionsInfo(new WalletId($"{wallet}0", "BTC"), new[] { firstTxId.ToString() });
|
||||
Assert.Equal("test", txInfo.Values.First().Comment);
|
||||
// Should have the 2 raw labels, and one legacy label for payouts
|
||||
Assert.Equal(3, txInfo.Values.First().LegacyLabels.Count);
|
||||
var payoutLabel = txInfo.Values.First().LegacyLabels.Select(l => l.Value).OfType<PayoutLabel>().First();
|
||||
Assert.Equal(2, payoutLabel.PullPaymentPayouts.Count);
|
||||
Assert.Equal(2, payoutLabel.PullPaymentPayouts["pp1"].Count);
|
||||
Assert.Single(payoutLabel.PullPaymentPayouts["pp2"]);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
|
@ -1,67 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Creates a 2-of-3 multisig setup, following the procedure described here:
|
||||
# https://github.com/bitcoin/bitcoin/blob/master/doc/multisig-tutorial.md
|
||||
# https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
|
||||
#
|
||||
# Usage:
|
||||
# ./docker-bitcoin-multisig-setup.sh custom-name
|
||||
#
|
||||
# The custom name/prefix is optional and defaults to "multisig".
|
||||
prefix="${1:-"multi_sig"}"
|
||||
|
||||
declare -A xpubs
|
||||
|
||||
printf "\n👛 Create descriptor wallets\n\n"
|
||||
for ((n=1;n<=3;n++)); do
|
||||
# Create descriptor wallets, surpress error output in case wallet already exists
|
||||
./docker-bitcoin-cli.sh -named createwallet wallet_name="${prefix}_part_${n}" descriptors=true > /dev/null 2>&1
|
||||
|
||||
# Collect xpubs
|
||||
./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors > /dev/null 2>&1
|
||||
xpubs["internal_xpub_${n}"]=$(./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/1/*"))][0] | .desc' | grep -Po '(?<=\().*(?=\))')
|
||||
xpubs["external_xpub_${n}"]=$(./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/0/*"))][0] | .desc' | grep -Po '(?<=\().*(?=\))')
|
||||
done
|
||||
|
||||
for x in "${!xpubs[@]}"; do
|
||||
printf "[%s]=%s\n" "$x" "${xpubs[$x]}";
|
||||
done
|
||||
|
||||
external_desc="wsh(sortedmulti(2,${xpubs["external_xpub_1"]},${xpubs["external_xpub_2"]},${xpubs["external_xpub_3"]}))"
|
||||
internal_desc="wsh(sortedmulti(2,${xpubs["internal_xpub_1"]},${xpubs["internal_xpub_2"]},${xpubs["internal_xpub_3"]}))"
|
||||
|
||||
external_desc_sum=$(./docker-bitcoin-cli.sh getdescriptorinfo $external_desc | jq '.descriptor')
|
||||
internal_desc_sum=$(./docker-bitcoin-cli.sh getdescriptorinfo $internal_desc | jq '.descriptor')
|
||||
|
||||
multisig_ext_desc="{\"desc\": $external_desc_sum, \"active\": true, \"internal\": false, \"timestamp\": \"now\"}"
|
||||
multisig_int_desc="{\"desc\": $internal_desc_sum, \"active\": true, \"internal\": true, \"timestamp\": \"now\"}"
|
||||
|
||||
multisig_desc="[$multisig_ext_desc, $multisig_int_desc]"
|
||||
|
||||
# Create multisig wallet, surpress error output in case wallet already exists
|
||||
|
||||
printf "\n🔐 Create multisig wallet\n"
|
||||
printf "\nExternal descriptor: $external_desc\n"
|
||||
printf "\nInternal descriptor: $internal_desc\n"
|
||||
|
||||
multisig_name="${prefix}_wallet"
|
||||
./docker-bitcoin-cli.sh -named createwallet wallet_name="$multisig_name" disable_private_keys=true blank=true descriptors=true > /dev/null 2>&1
|
||||
./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" importdescriptors "$multisig_desc" > /dev/null 2>&1
|
||||
|
||||
# Fund the wallet from the default wallet
|
||||
printf "\n💰 Fund multisig wallet\n"
|
||||
|
||||
newaddress=$(./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" getnewaddress "MultiSig Funding" | tr -d "[:cntrl:]")
|
||||
txid=$(./docker-bitcoin-cli.sh -rpcwallet="" sendtoaddress "$newaddress" 0.615)
|
||||
printf "\nReceiving address: $newaddress\n"
|
||||
printf "\nTransaction ID: $txid\n"
|
||||
|
||||
# Confirm everything worked
|
||||
printf "\nℹ️ Multisig wallet info\n\n"
|
||||
./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" getwalletinfo
|
||||
|
||||
# Unload wallets to prevent having to specify which wallet to use in BTCPay, NBXplorer etc.
|
||||
for ((n=1;n<=3;n++)); do
|
||||
./docker-bitcoin-cli.sh unloadwallet "${prefix}_part_${n}" > /dev/null 2>&1
|
||||
done
|
||||
./docker-bitcoin-cli.sh unloadwallet "$multisig_name" > /dev/null 2>&1
|
@ -71,7 +71,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -90,7 +90,7 @@ services:
|
||||
expose:
|
||||
- "4444"
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.40
|
||||
image: nicolasdorier/nbxplorer:2.3.14
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -126,7 +126,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -237,7 +237,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.15.4-beta-1
|
||||
image: btcpayserver/lnd:v0.15.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -272,7 +272,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.15.4-beta-1
|
||||
image: btcpayserver/lnd:v0.15.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -68,7 +68,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -87,7 +87,7 @@ services:
|
||||
expose:
|
||||
- "4444"
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.40
|
||||
image: nicolasdorier/nbxplorer:2.3.14
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -113,7 +113,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -225,7 +225,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.15.4-beta-1
|
||||
image: btcpayserver/lnd:v0.15.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -262,7 +262,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.15.4-beta-1
|
||||
image: btcpayserver/lnd:v0.15.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -1,26 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<RunAnalyzersDuringLiveAnalysis>False</RunAnalyzersDuringLiveAnalysis>
|
||||
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Condition="'$(GitCommit)' != ''" Include="BTCPayServer.GitCommitAttribute">
|
||||
<_Parameter1>$(GitCommit)</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\**" />
|
||||
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Remove="Build\**" />
|
||||
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<EmbeddedResource Remove="Build\**" />
|
||||
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<None Remove="Build\**" />
|
||||
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Update="Views\UIStorePullPayments\NewPullPayment.cshtml">
|
||||
<Pack>false</Pack>
|
||||
@ -35,6 +33,9 @@
|
||||
<ItemGroup>
|
||||
<None Remove="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="bundleconfig.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Content Remove="Services\Altcoins\**\*" />
|
||||
@ -47,15 +48,23 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.9" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.4" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="LNURL" Version="0.0.26" />
|
||||
<PackageReference Include="LNURL" Version="0.0.24" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="3.3.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
|
||||
@ -63,10 +72,15 @@
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="Serilog" Version="2.9.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
||||
<PackageReference Include="Text.Analyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -76,8 +90,8 @@
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,63 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ColorPalette
|
||||
{
|
||||
public const string Pattern = "^#[0-9a-fA-F]{6}$";
|
||||
public static bool IsValid(string color)
|
||||
{
|
||||
return Regex.Match(color, Pattern).Success;
|
||||
}
|
||||
public string TextColor(string bgColor)
|
||||
{
|
||||
int nThreshold = 105;
|
||||
var bg = ColorTranslator.FromHtml(bgColor);
|
||||
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
|
||||
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
public static readonly ColorPalette Default = new ColorPalette(new string[] {
|
||||
"#fbca04",
|
||||
"#0e8a16",
|
||||
"#ff7619",
|
||||
"#84b6eb",
|
||||
"#5319e7",
|
||||
"#cdcdcd",
|
||||
"#cc317c",
|
||||
});
|
||||
private ColorPalette(string[] labels)
|
||||
{
|
||||
Labels = labels;
|
||||
}
|
||||
|
||||
public readonly string[] Labels;
|
||||
|
||||
public string DeterministicColor(string label)
|
||||
{
|
||||
switch (label)
|
||||
{
|
||||
case "payjoin":
|
||||
return "#51b13e";
|
||||
case "invoice":
|
||||
return "#cedc21";
|
||||
case "payment-request":
|
||||
return "#489D77";
|
||||
case "app":
|
||||
return "#5093B6";
|
||||
case "pj-exposed":
|
||||
return "#51b13e";
|
||||
case "payout":
|
||||
return "#3F88AF";
|
||||
default:
|
||||
var num = NBitcoin.Utils.ToUInt32(Hashes.SHA256(Encoding.UTF8.GetBytes(label)), 0, true);
|
||||
return Labels[num % Labels.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
@model BTCPayServer.Components.Icon.IconViewModel
|
||||
|
||||
<svg role="img" class="icon icon-@Model.Symbol">
|
||||
<use href="~/img/icon-sprite.svg#@Model.Symbol"></use>
|
||||
<use href="/img/icon-sprite.svg#@Model.Symbol"></use>
|
||||
</svg>
|
||||
|
@ -1,20 +0,0 @@
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@inject ThemeSettings Theme
|
||||
@inject IFileService FileService
|
||||
@model BTCPayServer.Components.MainLogo.MainLogoViewModel
|
||||
|
||||
@if (!string.IsNullOrEmpty(Theme.LogoFileId))
|
||||
{
|
||||
var logoSrc = await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Theme.LogoFileId);
|
||||
<img src="@logoSrc" alt="BTCPay Server" class="main-logo main-logo-custom @Model.CssClass" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="main-logo main-logo-btcpay @Model.CssClass">
|
||||
<use href="~/img/logo.svg#small" class="main-logo-btcpay--small"/>
|
||||
<use href="~/img/logo.svg#large" class="main-logo-btcpay--large"/>
|
||||
</svg>
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.MainLogo
|
||||
{
|
||||
public class MainLogo : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(string cssClass = null)
|
||||
{
|
||||
var vm = new MainLogoViewModel
|
||||
{
|
||||
CssClass = cssClass,
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace BTCPayServer.Components.MainLogo
|
||||
{
|
||||
public class MainLogoViewModel
|
||||
{
|
||||
public string CssClass { get; set; }
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Views.CustodianAccounts
|
||||
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject PoliciesSettings PoliciesSettings
|
||||
@ -228,7 +227,7 @@
|
||||
})()
|
||||
</script>
|
||||
}
|
||||
else if (Env.IsSecure(HttpContext.HttpContext))
|
||||
else if (Env.IsSecure)
|
||||
{
|
||||
<ul class="navbar-nav">
|
||||
@if (!PoliciesSettings.LockSubscription)
|
||||
|
@ -123,10 +123,7 @@
|
||||
</div>
|
||||
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
|
||||
{
|
||||
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 mt-3 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
|
||||
<vc:icon symbol="caret-down"/>
|
||||
<span class="ms-1">Details</span>
|
||||
</button>
|
||||
<a class="d-inline-block mt-3" role="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">Show details</a>
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -1,24 +1,19 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Services
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject IFileService FileService
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment _env
|
||||
@inject SignInManager<ApplicationUser> _signInManager
|
||||
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
|
||||
@functions {
|
||||
@* ReSharper disable once CSharpWarnings::CS1998 *@
|
||||
#pragma warning disable 1998
|
||||
private async Task LogoContent()
|
||||
{
|
||||
<vc:main-logo />
|
||||
@if (Env.NetworkType != NBitcoin.ChainName.Mainnet)
|
||||
var logoSrc = $"{ViewContext.HttpContext.Request.PathBase}/img/logo.svg";
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="logo"><use href="@logoSrc#small" class="logo-small" /><use href="@logoSrc#large" class="logo-large" /></svg>
|
||||
@if (_env.NetworkType != NBitcoin.ChainName.Mainnet)
|
||||
{
|
||||
var type = Env.NetworkType.ToString();
|
||||
<small class="badge bg-warning rounded-pill ms-1 ms-sm-0" title="@type">@type.Replace("Testnet", "TN").Replace("Regtest", "RT")</small>
|
||||
<span class="badge bg-warning ms-1 ms-sm-0" style="font-size:10px;">@_env.NetworkType.ToString()</span>
|
||||
}
|
||||
}
|
||||
private static string StoreName(string title)
|
||||
private string StoreName(string title)
|
||||
{
|
||||
return string.IsNullOrEmpty(title) ? "Unnamed Store" : title;
|
||||
}
|
||||
@ -26,15 +21,15 @@
|
||||
}
|
||||
@if (Model.CurrentStoreId == null)
|
||||
{
|
||||
<a asp-controller="UIHome" asp-action="Index" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
<a href="~/" class="navbar-brand py-2 js-scroll-trigger">@{await LogoContent();}</a>
|
||||
}
|
||||
else if (Model.CurrentStoreIsOwner)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.CurrentStoreId" class="navbar-brand py-2 js-scroll-trigger">@{await LogoContent();}</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.CurrentStoreId" class="navbar-brand py-2 js-scroll-trigger">@{await LogoContent();}</a>
|
||||
}
|
||||
|
||||
<div id="StoreSelector">
|
||||
@ -42,14 +37,7 @@ else
|
||||
{
|
||||
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
|
||||
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "text-secondary" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
|
||||
{
|
||||
<img class="logo" src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CurrentStoreLogoFileId))" alt="@Model.CurrentDisplayName" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<vc:icon symbol="store"/>
|
||||
}
|
||||
<vc:icon symbol="store"/>
|
||||
<span>@(Model.CurrentStoreId == null ? "Select Store" : Model.CurrentDisplayName)</span>
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
@ -72,7 +60,7 @@ else
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
else if (SignInManager.IsSignedIn(User))
|
||||
else if (_signInManager.IsSignedIn(User))
|
||||
{
|
||||
<a asp-controller="UIUserStores" asp-action="CreateStore" class="btn btn-primary w-100 rounded-pill" id="StoreSelectorCreate">Create Store</a>
|
||||
}
|
||||
|
@ -50,15 +50,12 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
.OrderBy(s => s.Text)
|
||||
.ToList();
|
||||
|
||||
var blob = currentStore?.GetStoreBlob();
|
||||
|
||||
var vm = new StoreSelectorViewModel
|
||||
{
|
||||
Options = options,
|
||||
CurrentStoreId = currentStore?.Id,
|
||||
CurrentDisplayName = currentStore?.StoreName,
|
||||
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner,
|
||||
CurrentStoreLogoFileId = blob?.LogoFileId
|
||||
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
|
@ -6,7 +6,6 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
{
|
||||
public List<StoreSelectorOption> Options { get; set; }
|
||||
public string CurrentStoreId { get; set; }
|
||||
public string CurrentStoreLogoFileId { get; set; }
|
||||
public string CurrentDisplayName { get; set; }
|
||||
public bool CurrentStoreIsOwner { get; set; }
|
||||
}
|
||||
|
@ -65,11 +65,11 @@ namespace BTCPayServer.Configuration
|
||||
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != ChainName.Regtest)
|
||||
throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");
|
||||
|
||||
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
|
||||
Logs.Configuration.LogWarning("SQLITE backend support is deprecated and will be soon out of support");
|
||||
if (conf.GetOrDefault<string>("MYSQL", null) != null)
|
||||
Logs.Configuration.LogWarning("MYSQL backend support is deprecated and will be soon out of support");
|
||||
|
||||
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
|
||||
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
TorServices = conf.GetOrDefault<string>("torservices", null)
|
||||
?.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
@ -112,9 +112,9 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
Logs.Configuration.LogWarning($"The SSH key is not supported ({ex.Message}), try to generate the key with ssh-keygen using \"-m PEM\". Skipping SSH configuration...");
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Logs.Configuration.LogWarning(ex, "Error while loading SSH settings");
|
||||
throw new ConfigException($"sshkeyfilepassword is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,15 +144,14 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
|
||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||
var pluginRemote = conf.GetOrDefault<string>("plugin-remote", null);
|
||||
if (pluginRemote != null)
|
||||
Logs.Configuration.LogWarning("plugin-remote is an obsolete configuration setting, please remove it from configuration");
|
||||
PluginRemote = conf.GetOrDefault("plugin-remote", "btcpayserver/btcpayserver-plugins");
|
||||
RecommendedPlugins = conf.GetOrDefault("recommended-plugins", "").ToLowerInvariant().Split('\r', '\n', '\t', ' ').Where(s => !string.IsNullOrEmpty(s)).Distinct().ToArray();
|
||||
CheatMode = conf.GetOrDefault("cheatmode", false);
|
||||
if (CheatMode && this.NetworkType == ChainName.Mainnet)
|
||||
throw new ConfigException($"cheatmode can't be used on mainnet");
|
||||
}
|
||||
|
||||
public string PluginRemote { get; set; }
|
||||
public string[] RecommendedPlugins { get; set; }
|
||||
public bool CheatMode { get; set; }
|
||||
|
||||
@ -193,7 +192,16 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public bool DockerDeployment { get; set; }
|
||||
public SSHSettings SSHSettings { get; set; }
|
||||
public bool BundleJsCss
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public SSHSettings SSHSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TorrcFile { get; set; }
|
||||
public string[] TorServices { get; set; }
|
||||
public Uri UpdateUrl { get; set; }
|
||||
|
@ -27,10 +27,11 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--signet | -signet", $"Use signet (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"DEPRECATED: Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
|
||||
app.Option("--sqlitefile", $"DEPRECATED: File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
@ -45,7 +46,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
app.Option("--plugin-remote", "Obsolete, do not use", CommandOptionType.SingleValue);
|
||||
app.Option("--plugin-remote", "Which github repository to fetch the available plugins list (default:btcpayserver/btcpayserver-plugins)", CommandOptionType.SingleValue);
|
||||
app.Option("--recommended-plugins", "Plugins which would be marked as recommended to be installed. Separated by newline or space", CommandOptionType.MultipleValue);
|
||||
app.Option("--xforwardedproto", "If specified, set X-Forwarded-Proto to the specified value, this may be useful if your reverse proxy handle https but is not configured to add X-Forwarded-Proto (example: --xforwardedproto https)", CommandOptionType.SingleValue);
|
||||
app.Option("--cheatmode", "Add some helper UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
|
||||
@ -138,7 +139,6 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.blockexplorerlink=https://mempool.space/tx/{{0}}");
|
||||
if (n.SupportLightning)
|
||||
{
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");
|
||||
|
@ -77,8 +77,7 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
if (new[] { ExternalServiceTypes.Charge, ExternalServiceTypes.RTL, ExternalServiceTypes.ThunderHub,
|
||||
ExternalServiceTypes.Spark, ExternalServiceTypes.Configurator, ExternalServiceTypes.Torq }.Contains(serviceType))
|
||||
if (new[] { ExternalServiceTypes.Charge, ExternalServiceTypes.RTL, ExternalServiceTypes.ThunderHub, ExternalServiceTypes.Spark, ExternalServiceTypes.Configurator }.Contains(serviceType))
|
||||
{
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
@ -98,8 +97,7 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
connectionString.CookieFilePath = null;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Configurator ||
|
||||
serviceType == ExternalServiceTypes.ThunderHub || serviceType == ExternalServiceTypes.Torq)
|
||||
if (serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Configurator || serviceType == ExternalServiceTypes.ThunderHub)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
|
@ -36,10 +36,6 @@ namespace BTCPayServer.Configuration
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"Ride The Lightning");
|
||||
Load(configuration, cryptoCode, "torq", ExternalServiceTypes.Torq, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/torq/cookie-login/;cookiefile=/etc/lnd_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"Torq");
|
||||
Load(configuration, cryptoCode, "thunderhub", ExternalServiceTypes.ThunderHub, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/thub/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
@ -97,8 +93,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
ExternalServiceTypes.Spark,
|
||||
ExternalServiceTypes.RTL,
|
||||
ExternalServiceTypes.ThunderHub,
|
||||
ExternalServiceTypes.Torq
|
||||
ExternalServiceTypes.ThunderHub
|
||||
};
|
||||
|
||||
public static readonly string[] LightningServiceNames =
|
||||
@ -135,7 +130,6 @@ namespace BTCPayServer.Configuration
|
||||
P2P,
|
||||
RPC,
|
||||
Configurator,
|
||||
CLightningRest,
|
||||
Torq
|
||||
CLightningRest
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
@ -39,37 +38,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_currencies = currencies;
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/apps/crowdfund")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreateCrowdfundApp(string storeId, CreateCrowdfundAppRequest request)
|
||||
{
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
if (store == null)
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
|
||||
// This is not obvious but we must have a non-null currency or else request validation may work incorrectly
|
||||
request.TargetCurrency = request.TargetCurrency ?? store.GetStoreBlob().DefaultCurrency;
|
||||
|
||||
var validationResult = ValidateCrowdfundAppRequest(request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
var appData = new AppData
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Name = request.AppName,
|
||||
AppType = AppType.Crowdfund.ToString()
|
||||
};
|
||||
|
||||
appData.SetSettings(ToCrowdfundSettings(request));
|
||||
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
return Ok(ToCrowdfundModel(appData));
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request)
|
||||
@ -98,7 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
return Ok(ToPointOfSaleModel(appData));
|
||||
return Ok(ToModel(appData));
|
||||
}
|
||||
|
||||
[HttpPut("~/api/v1/apps/pos/{appId}")]
|
||||
@ -127,7 +95,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
|
||||
return Ok(ToPointOfSaleModel(app));
|
||||
return Ok(ToModel(app));
|
||||
}
|
||||
|
||||
private RequiresRefundEmail? BoolToRequiresRefundEmail(bool? requiresRefundEmail)
|
||||
@ -147,7 +115,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null);
|
||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -175,43 +143,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
||||
}
|
||||
|
||||
private CrowdfundSettings ToCrowdfundSettings(CreateCrowdfundAppRequest request)
|
||||
{
|
||||
var parsedSounds = ValidateStringArray(request.Sounds);
|
||||
var parsedColors = ValidateStringArray(request.AnimationColors);
|
||||
|
||||
return new CrowdfundSettings
|
||||
{
|
||||
Title = request.Title?.Trim(),
|
||||
Enabled = request.Enabled ?? true,
|
||||
EnforceTargetAmount = request.EnforceTargetAmount ?? false,
|
||||
StartDate = request.StartDate?.UtcDateTime,
|
||||
TargetCurrency = request.TargetCurrency?.Trim(),
|
||||
Description = request.Description?.Trim(),
|
||||
EndDate = request.EndDate?.UtcDateTime,
|
||||
TargetAmount = request.TargetAmount,
|
||||
CustomCSSLink = request.CustomCSSLink?.Trim(),
|
||||
MainImageUrl = request.MainImageUrl?.Trim(),
|
||||
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
|
||||
NotificationUrl = request.NotificationUrl?.Trim(),
|
||||
Tagline = request.Tagline?.Trim(),
|
||||
PerksTemplate = request.PerksTemplate != null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate?.Trim(), request.TargetCurrency)) : null,
|
||||
// If Disqus shortname is not null or empty we assume that Disqus should be enabled
|
||||
DisqusEnabled = !string.IsNullOrEmpty(request.DisqusShortname?.Trim()),
|
||||
DisqusShortname = request.DisqusShortname?.Trim(),
|
||||
// If explicit parameter is not passed for enabling sounds/animations, turn them on if custom sounds/colors are passed
|
||||
SoundsEnabled = request.SoundsEnabled ?? parsedSounds != null,
|
||||
AnimationsEnabled = request.AnimationsEnabled ?? parsedColors != null,
|
||||
ResetEveryAmount = request.ResetEveryAmount ?? 1,
|
||||
ResetEvery = (Services.Apps.CrowdfundResetEvery)request.ResetEvery,
|
||||
DisplayPerksValue = request.DisplayPerksValue ?? false,
|
||||
DisplayPerksRanking = request.DisplayPerksRanking ?? false,
|
||||
SortPerksByPopularity = request.SortPerksByPopularity ?? false,
|
||||
Sounds = parsedSounds ?? new CrowdfundSettings().Sounds,
|
||||
AnimationColors = parsedColors ?? new CrowdfundSettings().AnimationColors
|
||||
};
|
||||
}
|
||||
|
||||
private PointOfSaleSettings ToPointOfSaleSettings(CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
return new PointOfSaleSettings()
|
||||
@ -233,25 +164,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
EmbeddedCSS = request.EmbeddedCSS,
|
||||
RedirectAutomatically = request.RedirectAutomatically,
|
||||
RequiresRefundEmail = BoolToRequiresRefundEmail(request.RequiresRefundEmail) ?? RequiresRefundEmail.InheritFromStore,
|
||||
FormId = request.FormId,
|
||||
CheckoutType = request.CheckoutType ?? CheckoutType.V1
|
||||
};
|
||||
}
|
||||
|
||||
private AppDataBase ToModel(AppData appData)
|
||||
private PointOfSaleAppData ToModel(AppData appData)
|
||||
{
|
||||
return new AppDataBase
|
||||
{
|
||||
Id = appData.Id,
|
||||
AppType = appData.AppType,
|
||||
Name = appData.Name,
|
||||
StoreId = appData.StoreDataId,
|
||||
Created = appData.Created,
|
||||
};
|
||||
}
|
||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
private PointOfSaleAppData ToPointOfSaleModel(AppData appData)
|
||||
{
|
||||
return new PointOfSaleAppData
|
||||
{
|
||||
Id = appData.Id,
|
||||
@ -290,84 +209,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private CrowdfundAppData ToCrowdfundModel(AppData appData)
|
||||
{
|
||||
return new CrowdfundAppData
|
||||
{
|
||||
Id = appData.Id,
|
||||
AppType = appData.AppType,
|
||||
Name = appData.Name,
|
||||
StoreId = appData.StoreDataId,
|
||||
Created = appData.Created
|
||||
};
|
||||
}
|
||||
|
||||
private string[]? ValidateStringArray(string[]? arr)
|
||||
{
|
||||
if (arr == null || !arr.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make sure it's not just an array of empty strings
|
||||
if (arr.All(s => string.IsNullOrEmpty(s.Trim())))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return arr.Select(s => s.Trim()).ToArray();
|
||||
}
|
||||
|
||||
private IActionResult? ValidateCrowdfundAppRequest(CreateCrowdfundAppRequest request)
|
||||
{
|
||||
var validationResult = ValidateCreateAppRequest(request);
|
||||
if (request.TargetCurrency != null && _currencies.GetCurrencyData(request.TargetCurrency, false) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.TargetCurrency), "Invalid currency");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, request.TargetCurrency));
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PerksTemplate), "Invalid template");
|
||||
}
|
||||
|
||||
if (request.ResetEvery != Client.Models.CrowdfundResetEvery.Never && request.StartDate == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.StartDate), "A start date is needed when the goal resets every X amount of time");
|
||||
}
|
||||
|
||||
if (request.ResetEvery != Client.Models.CrowdfundResetEvery.Never && request.ResetEveryAmount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.ResetEveryAmount), "You must reset the goal at a minimum of 1");
|
||||
}
|
||||
|
||||
if (request.Sounds != null && ValidateStringArray(request.Sounds) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Sounds), "Sounds must be a non-empty array of non-empty strings");
|
||||
}
|
||||
|
||||
if (request.AnimationColors != null && ValidateStringArray(request.AnimationColors) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.AnimationColors), "Animation colors must be a non-empty array of non-empty strings");
|
||||
}
|
||||
|
||||
if (request.StartDate != null && request.EndDate != null && DateTimeOffset.Compare((DateTimeOffset)request.StartDate, (DateTimeOffset)request.EndDate!) > 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.EndDate), "End date cannot be before start date");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
validationResult = this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private IActionResult? ValidateCreateAppRequest(CreateAppRequest request)
|
||||
{
|
||||
if (request is null)
|
||||
|
@ -2,19 +2,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Rating;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -37,19 +32,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly PullPaymentHostedService _pullPaymentService;
|
||||
private readonly RateFetcher _rateProvider;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
|
||||
public LanguageService LanguageService { get; }
|
||||
|
||||
public GreenfieldInvoiceController(UIInvoiceController invoiceController, InvoiceRepository invoiceRepository,
|
||||
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
CurrencyNameTable currencyNameTable, BTCPayNetworkProvider networkProvider, RateFetcher rateProvider,
|
||||
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory)
|
||||
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
@ -57,11 +45,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_eventAggregator = eventAggregator;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_networkProvider = networkProvider;
|
||||
_rateProvider = rateProvider;
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
LanguageService = languageService;
|
||||
}
|
||||
|
||||
@ -350,175 +333,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/refund")]
|
||||
public async Task<IActionResult> RefundInvoice(
|
||||
string storeId,
|
||||
string invoiceId,
|
||||
RefundInvoiceRequest request,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
{
|
||||
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
|
||||
}
|
||||
PaymentMethod? invoicePaymentMethod = null;
|
||||
PaymentMethodId? paymentMethodId = null;
|
||||
if (request.PaymentMethod is not null && PaymentMethodId.TryParse(request.PaymentMethod, out paymentMethodId))
|
||||
{
|
||||
invoicePaymentMethod = invoice.GetPaymentMethods().SingleOrDefault(method => method.GetId() == paymentMethodId);
|
||||
}
|
||||
if (invoicePaymentMethod is null)
|
||||
{
|
||||
this.ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
|
||||
}
|
||||
if (request.RefundVariant is null)
|
||||
this.ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
|
||||
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var cryptoPaid = invoicePaymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
|
||||
var rateResult = await _rateProvider.FetchRate(
|
||||
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency),
|
||||
store.GetStoreBlob().GetRateRules(_networkProvider),
|
||||
cancellationToken
|
||||
);
|
||||
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
|
||||
var createPullPayment = new HostedServices.CreatePullPayment()
|
||||
{
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
|
||||
Name = request.Name ?? $"Refund {invoice.Id}",
|
||||
Description = request.Description,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = new[] { paymentMethodId },
|
||||
};
|
||||
|
||||
if (request.RefundVariant != RefundVariant.Custom)
|
||||
{
|
||||
if (request.CustomAmount is not null)
|
||||
this.ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
|
||||
if (request.CustomCurrency is not null)
|
||||
this.ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
switch (request.RefundVariant)
|
||||
{
|
||||
case RefundVariant.RateThen:
|
||||
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
|
||||
createPullPayment.Amount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case RefundVariant.CurrentRate:
|
||||
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
|
||||
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case RefundVariant.Fiat:
|
||||
createPullPayment.Currency = invoice.Currency;
|
||||
createPullPayment.Amount = paidCurrency;
|
||||
createPullPayment.AutoApproveClaims = false;
|
||||
break;
|
||||
|
||||
case RefundVariant.Custom:
|
||||
if (request.CustomAmount is null || (request.CustomAmount is decimal v && v <= 0)) {
|
||||
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
|
||||
}
|
||||
|
||||
if (
|
||||
string.IsNullOrEmpty(request.CustomCurrency) ||
|
||||
_currencyNameTable.GetCurrencyData(request.CustomCurrency, false) == null
|
||||
)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.CustomCurrency), "Invalid currency");
|
||||
}
|
||||
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.RefundVariant),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid || request.CustomAmount is null)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
createPullPayment.Currency = request.CustomCurrency;
|
||||
createPullPayment.Amount = request.CustomAmount.Value;
|
||||
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == request.CustomCurrency;
|
||||
break;
|
||||
|
||||
default:
|
||||
ModelState.AddModelError(nameof(request.RefundVariant), "Please select a valid refund option");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);
|
||||
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
(await ctx.Invoices.FindAsync(new[] { invoice.Id }, cancellationToken))!.CurrentRefundId = ppId;
|
||||
ctx.Refunds.Add(new RefundData
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
PullPaymentDataId = ppId
|
||||
});
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
|
||||
|
||||
return this.Ok(CreatePullPaymentData(pp));
|
||||
}
|
||||
|
||||
private Client.Models.PullPaymentData CreatePullPaymentData(Data.PullPaymentData pp)
|
||||
{
|
||||
var ppBlob = pp.GetBlob();
|
||||
return new BTCPayServer.Client.Models.PullPaymentData()
|
||||
{
|
||||
Id = pp.Id,
|
||||
StartsAt = pp.StartDate,
|
||||
ExpiresAt = pp.EndDate,
|
||||
Amount = ppBlob.Limit,
|
||||
Name = ppBlob.Name,
|
||||
Description = ppBlob.Description,
|
||||
Currency = ppBlob.Currency,
|
||||
Period = ppBlob.Period,
|
||||
Archived = pp.Archived,
|
||||
AutoApproveClaims = ppBlob.AutoApproveClaims,
|
||||
BOLT11Expiration = ppBlob.BOLT11Expiration,
|
||||
ViewLink = _linkGenerator.GetUriByAction(
|
||||
nameof(UIPullPaymentController.ViewPullPayment),
|
||||
"UIPullPayment",
|
||||
new { pullPaymentId = pp.Id },
|
||||
Request.Scheme,
|
||||
Request.Host,
|
||||
Request.PathBase)
|
||||
};
|
||||
}
|
||||
|
||||
private IActionResult InvoiceNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");
|
||||
@ -623,7 +437,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
DefaultLanguage = entity.DefaultLanguage,
|
||||
RedirectAutomatically = entity.RedirectAutomatically,
|
||||
RequiresRefundEmail = entity.RequiresRefundEmail,
|
||||
CheckoutType = entity.CheckoutType,
|
||||
RedirectURL = entity.RedirectURLTemplate
|
||||
},
|
||||
Receipt = entity.ReceiptOptions
|
||||
|
@ -101,14 +101,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return base.GetInvoice(cryptoCode, id, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices")]
|
||||
public override Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")]
|
||||
|
@ -111,14 +111,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return base.GetInvoice(cryptoCode, id, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
|
||||
public override Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateLightningInvoiceInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user